Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

quick POC at non-Long numerics (encoding and decoding) #164

Merged
merged 7 commits into from
Dec 28, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.akuleshov7.ktoml.decoders

import com.akuleshov7.ktoml.exceptions.CastException
import com.akuleshov7.ktoml.exceptions.IllegalTypeException
import com.akuleshov7.ktoml.exceptions.ParseException
import com.akuleshov7.ktoml.tree.nodes.TomlKeyValue
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
Expand All @@ -20,12 +19,46 @@ 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")
override fun decodeByte(): Byte = decodePrimitiveIntegerType { it.toByte() }

override fun decodeShort(): Short = decodePrimitiveIntegerType { it.toShort() }

override fun decodeInt(): Int = decodePrimitiveIntegerType { it.toInt() }

override fun decodeFloat(): Float {
val keyValue = decodeKeyValue()

val doubleValue = keyValue.castOrThrow<Double>()

val floatValue = doubleValue.toFloat()

if (floatValue.isInfinite()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Json apparently doesn't have any way to express infinity and NaN, so I guess that's why KxS has this exception. Toml does have inf and nan though

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I actually missed that, need to support it...

// maybe make this exception configurable? That's what KxS JSON does
throw ParseException(
"unexpected number - expected ${Float::class}, but actual value '$doubleValue' was not valid",
keyValue.lineNo
)
} else {
return floatValue
}
}

override fun decodeChar(): Char {
val keyValue = decodeKeyValue()

val stringValue = keyValue.castOrThrow<Char>()

// if (stringValue.length != 1) {
// throw ParseException(
// "Expected single Char, but actual value was a String size:${stringValue.length}",
// keyValue.lineNo
// )
// } else {
// return stringValue.first()
// }

return stringValue
}

// Valid Toml types that should be properly decoded
override fun decodeBoolean(): Boolean = decodePrimitiveType()
Expand All @@ -34,39 +67,54 @@ public abstract class TomlAbstractDecoder : AbstractDecoder() {
override fun decodeString(): String = decodePrimitiveType()

protected fun DeserializationStrategy<*>.isDateTime(): Boolean =
descriptor == instantSerializer.descriptor ||
descriptor == localDateTimeSerializer.descriptor ||
descriptor == localDateSerializer.descriptor
descriptor == instantSerializer.descriptor ||
descriptor == localDateTimeSerializer.descriptor ||
descriptor == localDateSerializer.descriptor

// Cases for date-time types
@Suppress("UNCHECKED_CAST")
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T = when (deserializer.descriptor) {
instantSerializer.descriptor -> decodePrimitiveType<Instant>() as T
localDateTimeSerializer.descriptor -> decodePrimitiveType<LocalDateTime>() as T
localDateSerializer.descriptor -> decodePrimitiveType<LocalDate>() as T
else -> super.decodeSerializableValue(deserializer)
}
override fun <T> decodeSerializableValue(deserializer: DeserializationStrategy<T>): T =
when (deserializer.descriptor) {
instantSerializer.descriptor -> decodePrimitiveType<Instant>() as T
localDateTimeSerializer.descriptor -> decodePrimitiveType<LocalDateTime>() as T
localDateSerializer.descriptor -> decodePrimitiveType<LocalDate>() as T
else -> super.decodeSerializableValue(deserializer)
}

internal abstract fun decodeKeyValue(): TomlKeyValue

private fun invalidType(typeName: String, requiredType: String): Nothing {
private inline fun <reified T> decodePrimitiveType(): T = decodeKeyValue().castOrThrow()

/** convert from a big ol' [Long] to a little tiny integer type, throwing an exception if this is not possible */
private inline fun <reified T> decodePrimitiveIntegerType(
converter: (Long) -> T
): T where T : Number, T : Comparable<T> {
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
)

val longValue = keyValue.castOrThrow<Long>()

val convertedValue = converter(longValue)

println("converted $longValue to $convertedValue")

if (longValue != convertedValue.toLong()) {
throw ParseException(
"unexpected number - expected ${T::class}, but actual value '$longValue' was out-of-bounds",
keyValue.lineNo
)
} else {
return convertedValue
}
}

private inline fun <reified T> decodePrimitiveType(): T {
val keyValue = decodeKeyValue()
try {
return keyValue.value.content as T
private inline fun <reified T> TomlKeyValue.castOrThrow(): T {
return try {
value.content as T
} catch (e: ClassCastException) {
throw CastException(
"Cannot decode the key [${keyValue.key.content}] with the value [${keyValue.value.content}]" +
throw ParseException(
"Cannot decode the key [${key.content}] with the value [${value.content}]" +
" with the provided type [${T::class}]. Please check the type in your Serializable class or it's nullability",
keyValue.lineNo
lineNo,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,24 +154,11 @@ public abstract class TomlAbstractEncoder protected constructor(
}
}

override fun encodeByte(value: Byte): Nothing = invalidType("Byte", "Long", value)
override fun encodeShort(value: Short): Nothing = invalidType("Short", "Long", value)
override fun encodeInt(value: Int): Nothing = invalidType("Int", "Long", value)
override fun encodeFloat(value: Float): Nothing = invalidType("Float", "Double", value)
override fun encodeChar(value: Char): Nothing = invalidType("Char", "String", value)

// Todo: Do we really want to make these invalid?
private fun invalidType(
typeName: String,
requiredType: String,
value: Any
): Nothing {
throw IllegalEncodingTypeException(
"<$typeName> is not allowed by the TOML specification, use <$requiredType>" +
" instead (key = ${attributes.getFullKey()}; value = $value)",
elementIndex
)
}
override fun encodeByte(value: Byte): Unit = encodeLong(value.toLong())
override fun encodeShort(value: Short): Unit = encodeLong(value.toLong())
override fun encodeInt(value: Int): Unit = encodeLong(value.toLong())
override fun encodeFloat(value: Float): Unit = encodeDouble(value.toDouble())
override fun encodeChar(value: Char): Unit = encodeString(value.toString())

// Structure

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ class GeneralDecoderTest {

@Test
fun testForSimpleNestedTable() {
val test = ("c = 5 \n" +
"[table1] \n" +
" b = 6 \n" +
" a = 5 \n ")
val test = """
c = 5
[table1]
b = 6
a = 5
""".trimIndent()
assertEquals(NestedSimpleTable(5, Table1(5, 6)), Toml.decodeFromString(test))
}

Expand Down Expand Up @@ -374,19 +376,20 @@ class GeneralDecoderTest {

@Test
fun severalTablesOnTheSameLevel() {
val test = """|[table]
|[table.in1]
| a = 1
| [table.in1.in1]
| a = 1
| [table.in1.in2]
| a = 1
|[table.in2]
| a = 1
| [table.in2.in1]
| a = 1
| [table.in2.in2]
| a = 1
val test = """
|[table]
|[table.in1]
| a = 1
| [table.in1.in1]
| a = 1
| [table.in1.in2]
| a = 1
|[table.in2]
| a = 1
| [table.in2.in1]
| a = 1
| [table.in2.in2]
| a = 1
""".trimMargin()

assertEquals(
Expand Down
Loading