Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -68,7 +68,7 @@ internal class QueryDecoder(

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
if (descriptor.kind == StructureKind.LIST) return QueryListDecoder(params.getAll(currentName))
if (entered) throw SerializationException("nested objects are not supported by the query format")
if (entered) throw SerializationException(NESTED_OBJECTS_REJECTED_MESSAGE)
entered = true
return this
}
Expand Down Expand Up @@ -126,6 +126,14 @@ internal class QueryListDecoder(
override fun decodeElementIndex(descriptor: SerialDescriptor): Int =
if (cursor < values.size) cursor else CompositeDecoder.DECODE_DONE

/**
* A list element is always a scalar/enum in this format's scope, so any call here — a nested
* `@Serializable` object or a nested list — is out of scope and rejected, mirroring
* [QueryDecoder.beginStructure]'s top-level guard.
*/
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
throw SerializationException(NESTED_OBJECTS_REJECTED_MESSAGE)

private fun next(): String = values[cursor++] ?: throw SerializationException("null list element")

override fun decodeString(): String = next()
Expand All @@ -149,6 +157,9 @@ internal class QueryListDecoder(
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = enumIndex(enumDescriptor, next())
}

/** The nesting-rejection message shared by [QueryDecoder.beginStructure] and [QueryListDecoder.beginStructure]. */
private const val NESTED_OBJECTS_REJECTED_MESSAGE: String = "nested objects are not supported by the query format"

/** Converts [raw] with [convert], failing with a [SerializationException] describing [kind] and [context] on error. */
private fun <T : Any> scalarOrFail(
kind: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal class QueryEncoder : AbstractEncoder() {
fun build(): QueryParameters = builder.build()

override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
if (entered) throw SerializationException("nested objects are not supported by the query format")
if (entered) throw SerializationException(NESTED_OBJECTS_REJECTED_MESSAGE)
entered = true
return this
}
Expand Down Expand Up @@ -98,6 +98,14 @@ internal class QueryListEncoder(
) : AbstractEncoder() {
override val serializersModule: SerializersModule = EmptySerializersModule()

/**
* A list element is always a scalar/enum in this format's scope, so any call here — a nested
* `@Serializable` object or a nested list — is out of scope and rejected, mirroring
* [QueryEncoder.beginStructure]'s top-level guard.
*/
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder =
throw SerializationException(NESTED_OBJECTS_REJECTED_MESSAGE)

override fun encodeValue(value: Any) {
builder.add(name, value.toString())
}
Expand All @@ -110,6 +118,9 @@ internal class QueryListEncoder(
}
}

/** The nesting-rejection message shared by [QueryEncoder.beginStructure] and [QueryListEncoder.beginStructure]. */
private const val NESTED_OBJECTS_REJECTED_MESSAGE: String = "nested objects are not supported by the query format"

/**
* Suffix marking the wire-level "present but empty" sentinel for a list property, appended to its
* declared name (e.g. `tags` -> `tags[]`). `[`/`]` are never percent-encoded by the query name encode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ private data class Nested(
val inner: Search,
)

@Serializable
private data class Holder(
val friends: List<Contact>,
)

@Serializable
private data class Matrix(
val rows: List<List<Int>>,
)

@Serializable
private data class Profile(
val handle: String,
val bio: String?,
)

@Serializable
private data class Foo(
val tags: List<String> = listOf("x"),
Expand Down Expand Up @@ -273,6 +289,36 @@ class SerdeTest {
assertEquals(withNickname, QueryParametersFormat.decodeFromQueryString<Contact>(query))
}

// Profile.bio has no default, unlike Contact.nickname: decodeElementIndex's isElementOptional
// short-circuit only applies to a defaulted element, so these cases force an actual
// decodeNotNullMark()/encodeNull() call instead of the property being skipped upstream.

@Test
fun `encoding a required nullable property that is null omits its key`() {
val query = QueryParametersFormat.encodeToQueryString(Profile(handle = "ada", bio = null))
assertEquals("handle=ada", query)
}

@Test
fun `decoding a required nullable property absent from the query yields null`() {
val profile = QueryParametersFormat.decodeFromQueryString<Profile>("handle=ada")
assertEquals(Profile(handle = "ada", bio = null), profile)
}

@Test
fun `decoding a required nullable property present with no value yields an empty string`() {
val profile = QueryParametersFormat.decodeFromQueryString<Profile>("handle=ada&bio=")
assertEquals(Profile(handle = "ada", bio = ""), profile)
}

@Test
fun `a required nullable property present but empty round-trips through encode and decode`() {
val original = Profile(handle = "ada", bio = "")
val query = QueryParametersFormat.encodeToQueryString(original)
assertEquals("handle=ada&bio=", query)
assertEquals(original, QueryParametersFormat.decodeFromQueryString<Profile>(query))
}

@Test
fun `decoding an unrecognized enum value throws`() {
assertFailsWith<SerializationException> {
Expand Down Expand Up @@ -454,4 +500,32 @@ class SerdeTest {
QueryParametersFormat.encodeToQueryParameters(Nested(Search(q = "x")))
}
}

@Test
fun `encoding a serializable object nested inside a list is rejected`() {
assertFailsWith<SerializationException> {
QueryParametersFormat.encodeToQueryParameters(Holder(listOf(Contact(name = "ada", nickname = "countess"))))
}
}

@Test
fun `decoding a serializable object nested inside a list is rejected`() {
assertFailsWith<SerializationException> {
QueryParametersFormat.decodeFromQueryString<Holder>("friends=ada")
}
}

@Test
fun `encoding a list nested inside a list is rejected`() {
assertFailsWith<SerializationException> {
QueryParametersFormat.encodeToQueryParameters(Matrix(listOf(listOf(1, 2))))
}
}

@Test
fun `decoding a list nested inside a list is rejected`() {
assertFailsWith<SerializationException> {
QueryParametersFormat.decodeFromQueryString<Matrix>("rows=1")
}
}
}
Loading