diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlAbstractDecoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlAbstractDecoder.kt index 077520e8..e4802b86 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlAbstractDecoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlAbstractDecoder.kt @@ -1,8 +1,10 @@ package com.akuleshov7.ktoml.decoders -import com.akuleshov7.ktoml.exceptions.CastException import com.akuleshov7.ktoml.exceptions.IllegalTypeException import com.akuleshov7.ktoml.tree.nodes.TomlKeyValue +import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlLong +import com.akuleshov7.ktoml.utils.IntegerLimitsEnum +import com.akuleshov7.ktoml.utils.IntegerLimitsEnum.* import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -20,12 +22,12 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() { private val localDateTimeSerializer = LocalDateTime.serializer() private val localDateSerializer = LocalDate.serializer() - // Invalid Toml primitive types, we will simply throw an error for them - override fun decodeByte(): Byte = invalidType("Byte", "Long") - override fun decodeShort(): Short = invalidType("Short", "Long") - override fun decodeInt(): Int = invalidType("Int", "Long") - override fun decodeFloat(): Float = invalidType("Float", "Double") - override fun decodeChar(): Char = invalidType("Char", "String") + // Invalid Toml primitive types, but we anyway support them with some limitations + override fun decodeByte(): Byte = decodePrimitiveType() + override fun decodeShort(): Short = decodePrimitiveType() + override fun decodeInt(): Int = decodePrimitiveType() + override fun decodeFloat(): Float = decodePrimitiveType() + override fun decodeChar(): Char = decodePrimitiveType() // Valid Toml types that should be properly decoded override fun decodeBoolean(): Boolean = decodePrimitiveType() @@ -49,25 +51,63 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() { internal abstract fun decodeKeyValue(): TomlKeyValue - private fun invalidType(typeName: String, requiredType: String): Nothing { - val keyValue = decodeKeyValue() - throw IllegalTypeException( - "<$typeName> type is not allowed by toml specification," + - " use <$requiredType> instead" + - " (key = ${keyValue.key.content}; value = ${keyValue.value.content})", keyValue.lineNo - ) - } - + /** + * This is just an adapter from `kotlinx.serialization` to match the content with a type from a Toml Tree, + * that we have parsed to a type that is described in user's code. For example: + * >>> input: a = "5" + * >>> stored in Toml Tree: TomlString("5") + * >>> expected by user: data class A(val a: Int) + * >>> TomlString cannot be cast to Int, user made a mistake -> IllegalTypeException + */ private inline fun decodePrimitiveType(): T { val keyValue = decodeKeyValue() try { - return keyValue.value.content as T + return when (val value = keyValue.value) { + is TomlLong -> decodeInteger(value.content as Long, keyValue.lineNo) + else -> keyValue.value.content as T + } } catch (e: ClassCastException) { - throw CastException( + throw IllegalTypeException( "Cannot decode the key [${keyValue.key.content}] with the value [${keyValue.value.content}]" + " with the provided type [${T::class}]. Please check the type in your Serializable class or it's nullability", keyValue.lineNo ) } } + + /** + * After a lot of discussions (https://github.com/akuleshov7/ktoml/pull/153#discussion_r1003114861 and + * https://github.com/akuleshov7/ktoml/issues/163), we have finally decided to allow to use Integer types and not only Long. + * This method does simple validation of integer values to avoid overflow. For example, you really want to use byte, + * we will check here, that your byte value does not exceed 127 and so on. + */ + private inline fun decodeInteger(content: Long, lineNo: Int): T = when (T::class) { + Byte::class -> validateAndConvertInt(content, lineNo, BYTE) { num: Long -> num.toByte() as T } + Short::class -> validateAndConvertInt(content, lineNo, SHORT) { num: Long -> num.toShort() as T } + Int::class -> validateAndConvertInt(content, lineNo, INT) { num: Long -> num.toInt() as T } + Long::class -> validateAndConvertInt(content, lineNo, LONG) { num: Long -> num as T } + else -> invalidType(T::class.toString(), "Signed Type") + } + + private inline fun validateAndConvertInt( + content: Long, + lineNo: Int, + limits: IntegerLimitsEnum, + conversion: (Long) -> T, + ): T = if (content in limits.min..limits.max) { + conversion(content) + } else { + throw IllegalTypeException("The integer literal, that you have provided is <$content>, " + + "but the type for deserialization is <${T::class}>. You will get an overflow, " + + "so we advise you to check the data or use other type for deserialization (Long, for example)", lineNo) + } + + private fun invalidType(typeName: String, requiredType: String): Nothing { + val keyValue = decodeKeyValue() + throw IllegalTypeException( + "<$typeName> type is not allowed by toml specification," + + " use <$requiredType> instead" + + " (key = ${keyValue.key.content}; value = ${keyValue.value.content})", keyValue.lineNo + ) + } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlDecodingException.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlDecodingException.kt index a0d0357c..49092cd6 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlDecodingException.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/exceptions/TomlDecodingException.kt @@ -32,8 +32,6 @@ internal class NullValueException(propertyName: String, lineNo: Int) : TomlDecod " Please check the input (line: <$lineNo>) or make the property nullable" ) -internal class CastException(message: String, lineNo: Int) : TomlDecodingException("Line $lineNo: $message") - internal class IllegalTypeException(message: String, lineNo: Int) : TomlDecodingException("Line $lineNo: $message") internal class MissingRequiredPropertyException(message: String) : TomlDecodingException(message) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/Types.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/Types.kt new file mode 100644 index 00000000..f603f666 --- /dev/null +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/Types.kt @@ -0,0 +1,35 @@ +/** + * File contains enums with minimum and maximum values of corresponding types + */ + +package com.akuleshov7.ktoml.utils + +/** + * @property min + * @property max + */ +public enum class IntegerLimitsEnum(public val min: Long, public val max: Long) { + BYTE(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()), + INT(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()), + LONG(Long.MIN_VALUE, Long.MAX_VALUE), + SHORT(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()), + ; + // Unsigned values are not supported now, and I think + // that will not be supported, because TOML spec says the following: + // Arbitrary 64-bit signed integers (from −2^63 to 2^63−1) should be accepted and handled losslessly. + // If an integer cannot be represented losslessly, an error must be thrown. <- FixMe: this is still not supported + // U_BYTE(UByte.MIN_VALUE.toLong(), UByte.MAX_VALUE.toLong()), + // U_SHORT(UShort.MIN_VALUE.toLong(), UShort.MAX_VALUE.toLong()), + // U_INT(UInt.MIN_VALUE.toLong(), UInt.MAX_VALUE.toLong()), + // U_LONG(ULong.MIN_VALUE.toLong(), ULong.MAX_VALUE.toLong()), +} + +/** + * @property min + * @property max + */ +public enum class FloatLimitsEnums(public val min: Double, public val max: Double) { + DOUBLE(Double.MIN_VALUE, Double.MAX_VALUE), + FLOAT(Float.MIN_VALUE.toDouble(), Float.MAX_VALUE.toDouble()), + ; +} diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt index a912edad..cefb064a 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayDecoderTest.kt @@ -1,7 +1,7 @@ package com.akuleshov7.ktoml.decoders import com.akuleshov7.ktoml.Toml -import com.akuleshov7.ktoml.exceptions.CastException +import com.akuleshov7.ktoml.exceptions.IllegalTypeException import com.akuleshov7.ktoml.exceptions.ParseException import kotlinx.serialization.decodeFromString import kotlinx.serialization.SerialName @@ -79,7 +79,7 @@ class SimpleArrayDecoderTest { val testWithNullArray2: ClassWithMutableList = Toml.decodeFromString("field = null") assertEquals(null, testWithNullArray2.field) - assertFailsWith { Toml.decodeFromString("field = [null]").field } + assertFailsWith { Toml.decodeFromString("field = [null]").field } val testWithOnlyNullInArray: ClassWithImmutableList = Toml.decodeFromString("field = [null ]") assertEquals(listOf(null), testWithOnlyNullInArray.field) diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt index 987f58d0..82d0ddd3 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ArrayOfTablesDecoderTest.kt @@ -1,7 +1,6 @@ package com.akuleshov7.ktoml.decoders import com.akuleshov7.ktoml.Toml -import com.akuleshov7.ktoml.exceptions.CastException import kotlinx.serialization.decodeFromString import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/DecodingTypeTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/DecodingTypeTest.kt index 5f6b03da..7d832f84 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/DecodingTypeTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/DecodingTypeTest.kt @@ -2,7 +2,6 @@ package com.akuleshov7.ktoml.decoders import com.akuleshov7.ktoml.Toml import com.akuleshov7.ktoml.exceptions.IllegalTypeException -import com.akuleshov7.ktoml.exceptions.CastException import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlin.test.Test @@ -44,9 +43,9 @@ class DecodingTypeTest { assertFailsWith { Toml.decodeFromString("a = true") } assertFailsWith { Toml.decodeFromString("a = true") } - assertFailsWith { Toml.decodeFromString("a = \"test\"") } - assertFailsWith { Toml.decodeFromString("a = true") } - assertFailsWith { Toml.decodeFromString("a = 12.0") } - assertFailsWith { Toml.decodeFromString("a = 1") } + assertFailsWith { Toml.decodeFromString("a = \"test\"") } + assertFailsWith { Toml.decodeFromString("a = true") } + assertFailsWith { Toml.decodeFromString("a = 12.0") } + assertFailsWith { Toml.decodeFromString("a = 1") } } } diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/IntegersDecoderTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/IntegersDecoderTest.kt new file mode 100644 index 00000000..17248d22 --- /dev/null +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/IntegersDecoderTest.kt @@ -0,0 +1,70 @@ +package com.akuleshov7.ktoml.decoders + +import com.akuleshov7.ktoml.Toml +import com.akuleshov7.ktoml.exceptions.IllegalTypeException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.Serializable +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.serialization.ExperimentalSerializationApi +import kotlin.test.assertFailsWith + +class IntegersDecoderTest { + @Serializable + data class Integers( + val s: Short, + val b: Byte, + val i: Int, + val l: Long, + ) + + @Test + fun positiveScenario() { + var test = """ + s = 5 + b = 5 + i = 5 + l = 5 + """.trimMargin() + + var decoded = Toml.decodeFromString(test) + println(decoded) + assertEquals( + Integers(5, 5, 5, 5), + decoded + ) + + test = """ + s = 32767 + b = -128 + i = 5 + l = 5 + """.trimMargin() + + decoded = Toml.decodeFromString(test) + println(decoded) + assertEquals( + Integers(32767, -128, 5, 5), + decoded + ) + } + + @Test + fun negativeScenario() { + var test = """ + s = 32768 + b = 5 + i = 5 + l = 5 + """.trimMargin() + assertFailsWith { Toml.decodeFromString(test) } + + test = """ + s = -32769 + b = 5 + i = 5 + l = 5 + """.trimMargin() + assertFailsWith { Toml.decodeFromString(test) } + } +} diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ReadMeExampleTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ReadMeExampleTest.kt index 4c3e589c..0fcb3ce1 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ReadMeExampleTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/decoders/ReadMeExampleTest.kt @@ -22,10 +22,11 @@ class ReadMeExampleTest { data class Table1( // nullable values, from toml you can pass null/nil/empty value to this kind of a field val property1: Long?, - // please note, that according to the specification of toml integer values should be represented with Long - val property2: Long, - // no need to pass this value as it has the default value and is NOT REQUIRED - val property3: Long = 5 + // please note, that according to the specification of toml integer values should be represented with Long, + // but we allow to use Int/Short/etc. Just be careful with overflow + val property2: Int, + // no need to pass this value in the input as it has the default value and so it is NOT REQUIRED + val property3: Short = 5 ) @Serializable