diff --git a/kuri-serde-kotlinx/src/commonMain/kotlin/org/dexpace/kuri/serde/QueryDecoder.kt b/kuri-serde-kotlinx/src/commonMain/kotlin/org/dexpace/kuri/serde/QueryDecoder.kt index 8bf0c99..5c8f64b 100644 --- a/kuri-serde-kotlinx/src/commonMain/kotlin/org/dexpace/kuri/serde/QueryDecoder.kt +++ b/kuri-serde-kotlinx/src/commonMain/kotlin/org/dexpace/kuri/serde/QueryDecoder.kt @@ -56,19 +56,25 @@ internal class QueryDecoder( override fun decodeBoolean(): Boolean = parseStrictBoolean(decodeString()) - override fun decodeInt(): Int = decodeString().toInt() + override fun decodeInt(): Int = scalarOrFail("Int", decodeString(), "value for '$currentName'", String::toIntOrNull) - override fun decodeLong(): Long = decodeString().toLong() + override fun decodeLong(): Long = + scalarOrFail("Long", decodeString(), "value for '$currentName'", String::toLongOrNull) - override fun decodeShort(): Short = decodeString().toShort() + override fun decodeShort(): Short = + scalarOrFail("Short", decodeString(), "value for '$currentName'", String::toShortOrNull) - override fun decodeByte(): Byte = decodeString().toByte() + override fun decodeByte(): Byte = + scalarOrFail("Byte", decodeString(), "value for '$currentName'", String::toByteOrNull) - override fun decodeDouble(): Double = decodeString().toDouble() + override fun decodeDouble(): Double = + scalarOrFail("Double", decodeString(), "value for '$currentName'", String::toDoubleOrNull) - override fun decodeFloat(): Float = decodeString().toFloat() + override fun decodeFloat(): Float = + scalarOrFail("Float", decodeString(), "value for '$currentName'", String::toFloatOrNull) - override fun decodeChar(): Char = decodeString().single() + override fun decodeChar(): Char = + scalarOrFail("Char", decodeString(), "value for '$currentName'") { it.singleOrNull() } override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = enumIndex(enumDescriptor, decodeString()) } @@ -95,23 +101,31 @@ internal class QueryListDecoder( override fun decodeBoolean(): Boolean = parseStrictBoolean(next()) - override fun decodeInt(): Int = next().toInt() + override fun decodeInt(): Int = scalarOrFail("Int", next(), "list element", String::toIntOrNull) - override fun decodeLong(): Long = next().toLong() + override fun decodeLong(): Long = scalarOrFail("Long", next(), "list element", String::toLongOrNull) - override fun decodeShort(): Short = next().toShort() + override fun decodeShort(): Short = scalarOrFail("Short", next(), "list element", String::toShortOrNull) - override fun decodeByte(): Byte = next().toByte() + override fun decodeByte(): Byte = scalarOrFail("Byte", next(), "list element", String::toByteOrNull) - override fun decodeDouble(): Double = next().toDouble() + override fun decodeDouble(): Double = scalarOrFail("Double", next(), "list element", String::toDoubleOrNull) - override fun decodeFloat(): Float = next().toFloat() + override fun decodeFloat(): Float = scalarOrFail("Float", next(), "list element", String::toFloatOrNull) - override fun decodeChar(): Char = next().single() + override fun decodeChar(): Char = scalarOrFail("Char", next(), "list element") { it.singleOrNull() } override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = enumIndex(enumDescriptor, next()) } +/** Converts [raw] with [convert], failing with a [SerializationException] describing [kind] and [context] on error. */ +private fun scalarOrFail( + kind: String, + raw: String, + context: String, + convert: (String) -> T?, +): T = convert(raw) ?: throw SerializationException("invalid $kind $context: '$raw'") + /** * Parses a query value as a boolean, accepting only `"true"`/`"false"` case-insensitively. * diff --git a/kuri-serde-kotlinx/src/commonTest/kotlin/org/dexpace/kuri/serde/SerdeTest.kt b/kuri-serde-kotlinx/src/commonTest/kotlin/org/dexpace/kuri/serde/SerdeTest.kt index 3e51e40..b22d325 100644 --- a/kuri-serde-kotlinx/src/commonTest/kotlin/org/dexpace/kuri/serde/SerdeTest.kt +++ b/kuri-serde-kotlinx/src/commonTest/kotlin/org/dexpace/kuri/serde/SerdeTest.kt @@ -296,6 +296,99 @@ class SerdeTest { } } + @Test + fun `decoding a malformed Int value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString("q=x&page=abc") + } + } + + @Test + fun `decoding an empty Int value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString("q=x&page=") + } + } + + @Test + fun `decoding a malformed Long value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "text=t&flag=true&byteValue=1&shortValue=1&intValue=1&longValue=notanumber" + + "&floatValue=1.0&doubleValue=1.0&charValue=a&sort=ASC", + ) + } + } + + @Test + fun `decoding a malformed Short value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "text=t&flag=true&byteValue=1&shortValue=notanumber&intValue=1&longValue=1" + + "&floatValue=1.0&doubleValue=1.0&charValue=a&sort=ASC", + ) + } + } + + @Test + fun `decoding a malformed Byte value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "text=t&flag=true&byteValue=notanumber&shortValue=1&intValue=1&longValue=1" + + "&floatValue=1.0&doubleValue=1.0&charValue=a&sort=ASC", + ) + } + } + + @Test + fun `decoding a malformed Double value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "text=t&flag=true&byteValue=1&shortValue=1&intValue=1&longValue=1" + + "&floatValue=1.0&doubleValue=notanumber&charValue=a&sort=ASC", + ) + } + } + + @Test + fun `decoding a malformed Float value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "text=t&flag=true&byteValue=1&shortValue=1&intValue=1&longValue=1" + + "&floatValue=notanumber&doubleValue=1.0&charValue=a&sort=ASC", + ) + } + } + + @Test + fun `decoding an empty Char value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "text=t&flag=true&byteValue=1&shortValue=1&intValue=1&longValue=1" + + "&floatValue=1.0&doubleValue=1.0&charValue=&sort=ASC", + ) + } + } + + @Test + fun `decoding a multi-character Char value throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "text=t&flag=true&byteValue=1&shortValue=1&intValue=1&longValue=1" + + "&floatValue=1.0&doubleValue=1.0&charValue=ab&sort=ASC", + ) + } + } + + @Test + fun `decoding a malformed list element throws a serialization exception`() { + assertFailsWith { + QueryParametersFormat.decodeFromQueryString( + "bytes=abc&shorts=1&ints=1&longs=1&floats=1.0&doubles=1.0&chars=a&flags=true&sorts=ASC", + ) + } + } + @Test fun `decoding a nested serializable object is rejected`() { assertFailsWith {