Skip to content

Commit 2547699

Browse files
authored
feat: implement serde for simple document type (#655)
1 parent a9ffb52 commit 2547699

File tree

15 files changed

+546
-1
lines changed

15 files changed

+546
-1
lines changed

runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smithy/Document.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ sealed class Document {
1414
* Wraps a [kotlin.Number] of arbitrary precision.
1515
*/
1616
data class Number(val value: kotlin.Number) : Document() {
17+
init {
18+
if (value is Double && !value.isFinite() || value is Float && !value.isFinite()) {
19+
throw IllegalArgumentException(
20+
"a document number cannot be $value, as its value cannot be preserved across serde"
21+
)
22+
}
23+
}
24+
1725
override fun toString() = value.toString()
1826
}
1927

runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/smithy/DocumentBuilder.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class DocumentBuilder internal constructor() {
2929
content[this] = value ?: Document.Null
3030
}
3131

32+
infix fun String.to(@Suppress("UNUSED_PARAMETER") value: Nothing?) {
33+
require(content[this] == null) { "Key $this is already registered in builder" }
34+
content[this] = Document.Null
35+
}
36+
3237
class ListBuilder internal constructor() {
3338
val content: MutableList<Document> = mutableListOf()
3439

@@ -40,6 +45,8 @@ class DocumentBuilder internal constructor() {
4045
content.add(if (value != null) Document(value) else Document.Null)
4146
fun add(value: Document?): Boolean =
4247
content.add(value ?: Document.Null)
48+
fun add(@Suppress("UNUSED_PARAMETER") value: Nothing?): Boolean =
49+
content.add(Document.Null)
4350

4451
@JvmName("addAllNumbers") fun addAll(value: List<Number?>) = value.forEach(::add)
4552
@JvmName("addAllStrings") fun addAll(value: List<String?>) = value.forEach(::add)

runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/DocumentBuilderTest.kt

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import kotlin.test.*
1111

1212
class DocumentBuilderTest {
1313
@Test
14-
fun buildsAnObject() {
14+
fun itBuildsAnObject() {
1515
val doc = buildDocument {
1616
"foo" to 1
1717
"baz" to buildList {
@@ -30,4 +30,58 @@ class DocumentBuilderTest {
3030
assertEquals(expected, "$doc")
3131
assertEquals(1, doc.asMap()["foo"]?.asInt())
3232
}
33+
34+
@Test
35+
fun itRejectsDoubleInfinity() {
36+
assertFailsWith<IllegalArgumentException>(
37+
"a document number cannot be Infinity, as its value cannot be preserved across serde"
38+
) {
39+
Document(Double.POSITIVE_INFINITY)
40+
}
41+
}
42+
43+
@Test
44+
fun itRejectsDoubleNegativeInfinity() {
45+
assertFailsWith<IllegalArgumentException>(
46+
"a document number cannot be -Infinity, as its value cannot be preserved across serde"
47+
) {
48+
Document(Double.NEGATIVE_INFINITY)
49+
}
50+
}
51+
52+
@Test
53+
fun itRejectsDoubleNaN() {
54+
assertFailsWith<IllegalArgumentException>(
55+
"a document number cannot be NaN, as its value cannot be preserved across serde"
56+
) {
57+
Document(Double.NaN)
58+
}
59+
}
60+
61+
@Test
62+
fun itRejectsFloatInfinity() {
63+
assertFailsWith<IllegalArgumentException>(
64+
"a document number cannot be Infinity, as its value cannot be preserved across serde"
65+
) {
66+
Document(Float.POSITIVE_INFINITY)
67+
}
68+
}
69+
70+
@Test
71+
fun itRejectsFloatNegativeInfinity() {
72+
assertFailsWith<IllegalArgumentException>(
73+
"a document number cannot be -Infinity, as its value cannot be preserved across serde"
74+
) {
75+
Document(Float.NEGATIVE_INFINITY)
76+
}
77+
}
78+
79+
@Test
80+
fun itRejectsFloatNaN() {
81+
assertFailsWith<IllegalArgumentException>(
82+
"a document number cannot be NaN, as its value cannot be preserved across serde"
83+
) {
84+
Document(Float.NaN)
85+
}
86+
}
3387
}

runtime/serde/common/src/aws/smithy/kotlin/runtime/serde/Deserializer.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*/
55
package aws.smithy.kotlin.runtime.serde
66

7+
import aws.smithy.kotlin.runtime.smithy.Document
8+
79
/**
810
* Deserializer is a format agnostic deserialization interface. Specific formats (e.g. JSON, XML, etc) implement
911
* this interface and handle the underlying raw decoding process and deal with details specific to that format.
@@ -190,6 +192,14 @@ interface PrimitiveDeserializer {
190192
*/
191193
fun deserializeBoolean(): Boolean
192194

195+
/**
196+
* Deserialize and return the next token as a [Document].
197+
*
198+
* If the document's value is a list or map, this method will deserialize all elements or fields recursively - the
199+
* caller need not further inspect the value to attempt to do so manually.
200+
*/
201+
fun deserializeDocument(): Document
202+
193203
/**
194204
* Consume the next token if represents a null value. Always returns null.
195205
*/

runtime/serde/common/src/aws/smithy/kotlin/runtime/serde/Serializer.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0.
44
*/
55
package aws.smithy.kotlin.runtime.serde
6+
import aws.smithy.kotlin.runtime.smithy.Document
67
import aws.smithy.kotlin.runtime.time.Instant
78
import aws.smithy.kotlin.runtime.time.TimestampFormat
89

@@ -129,6 +130,15 @@ interface StructSerializer : PrimitiveSerializer {
129130
*/
130131
fun field(descriptor: SdkFieldDescriptor, value: Instant, format: TimestampFormat)
131132

133+
/**
134+
* Writes the field name given in the descriptor, and then
135+
* serializes value.
136+
*
137+
* @param descriptor
138+
* @param value
139+
*/
140+
fun field(descriptor: SdkFieldDescriptor, value: Document)
141+
132142
/**
133143
* Writes the field name given in the descriptor, and then
134144
* serializes value.

runtime/serde/serde-form-url/common/src/aws/smithy/kotlin/runtime/serde/formurl/FormUrlSerializer.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import aws.smithy.kotlin.runtime.io.SdkByteBuffer
99
import aws.smithy.kotlin.runtime.io.bytes
1010
import aws.smithy.kotlin.runtime.io.write
1111
import aws.smithy.kotlin.runtime.serde.*
12+
import aws.smithy.kotlin.runtime.smithy.Document
1213
import aws.smithy.kotlin.runtime.time.Instant
1314
import aws.smithy.kotlin.runtime.time.TimestampFormat
1415
import aws.smithy.kotlin.runtime.util.text.urlEncodeComponent
@@ -130,6 +131,12 @@ private class FormUrlStructSerializer(
130131
serializeInstant(value, format)
131132
}
132133

134+
override fun field(descriptor: SdkFieldDescriptor, value: Document) {
135+
throw SerializationException(
136+
"cannot serialize field ${descriptor.serialName}; Document type is not supported by form-url encoding"
137+
)
138+
}
139+
133140
override fun field(descriptor: SdkFieldDescriptor, value: SdkSerializable) {
134141
val nestedPrefix = "${prefix}${descriptor.serialName}."
135142
// prepend the current prefix if one exists (e.g. deeply nested structures)

runtime/serde/serde-json/common/src/aws/smithy/kotlin/runtime/serde/json/JsonDeserializer.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package aws.smithy.kotlin.runtime.serde.json
66

77
import aws.smithy.kotlin.runtime.serde.*
8+
import aws.smithy.kotlin.runtime.smithy.Document
89

910
/**
1011
* Provides a deserializer for JSON documents
@@ -33,6 +34,10 @@ class JsonDeserializer(payload: ByteArray) : Deserializer, Deserializer.ElementI
3334

3435
override fun deserializeDouble(): Double = nextNumberValue { it.toDouble() }
3536

37+
// deserializes the next token as a number with the maximum discernible precision
38+
private fun deserializeNumber(): Number =
39+
nextNumberValue { if (it.contains('.')) it.toDouble() else it.toLong() }
40+
3641
// assert the next token is a Number and execute [block] with the raw value as a string. Returns result
3742
// of executing the block. This is mostly so that numeric conversions can keep as much precision as possible
3843
private fun <T> nextNumberValue(block: (value: String) -> T): T {
@@ -58,6 +63,41 @@ class JsonDeserializer(payload: ByteArray) : Deserializer, Deserializer.ElementI
5863
return token.value
5964
}
6065

66+
override fun deserializeDocument(): Document =
67+
when (val token = reader.peek()) {
68+
is JsonToken.Number -> Document(deserializeNumber())
69+
is JsonToken.String -> Document(deserializeString())
70+
is JsonToken.Bool -> Document(deserializeBoolean())
71+
JsonToken.Null -> {
72+
reader.nextToken()
73+
Document.Null
74+
}
75+
JsonToken.BeginArray ->
76+
deserializeList(SdkFieldDescriptor(SerialKind.Document)) {
77+
val values = mutableListOf<Document>()
78+
while (hasNextElement()) {
79+
values.add(deserializeDocument())
80+
}
81+
Document.List(values)
82+
}
83+
JsonToken.BeginObject ->
84+
deserializeMap(SdkFieldDescriptor(SerialKind.Document)) {
85+
val values = mutableMapOf<String, Document>()
86+
while (hasNextEntry()) {
87+
values[key()] = deserializeDocument()
88+
}
89+
Document.Map(values)
90+
}
91+
JsonToken.EndArray, JsonToken.EndObject, JsonToken.EndDocument ->
92+
throw DeserializationException(
93+
"encountered unexpected json token \"$token\" while deserializing document"
94+
)
95+
is JsonToken.Name ->
96+
throw DeserializationException(
97+
"encountered unexpected json field declaration \"${token.value}\" while deserializing document"
98+
)
99+
}
100+
61101
override fun deserializeNull(): Nothing? {
62102
reader.nextTokenOf<JsonToken.Null>()
63103
return null

runtime/serde/serde-json/common/src/aws/smithy/kotlin/runtime/serde/json/JsonEncoder.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ internal class JsonEncoder(private val pretty: Boolean = false) : JsonStreamWrit
108108

109109
private fun writeNumber(value: Number) = encodeValue(value.toString())
110110

111+
override fun writeValue(value: Number) = writeNumber(value)
111112
override fun writeValue(value: Byte) = writeNumber(value)
112113
override fun writeValue(value: Long) = writeNumber(value)
113114
override fun writeValue(value: Short) = writeNumber(value)

runtime/serde/serde-json/common/src/aws/smithy/kotlin/runtime/serde/json/JsonSerializer.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package aws.smithy.kotlin.runtime.serde.json
66

77
import aws.smithy.kotlin.runtime.serde.*
8+
import aws.smithy.kotlin.runtime.smithy.Document
89
import aws.smithy.kotlin.runtime.time.Instant
910
import aws.smithy.kotlin.runtime.time.TimestampFormat
1011

@@ -101,6 +102,11 @@ class JsonSerializer : Serializer, ListSerializer, MapSerializer, StructSerializ
101102
serializeInstant(value, format)
102103
}
103104

105+
override fun field(descriptor: SdkFieldDescriptor, value: Document) {
106+
jsonWriter.writeName(descriptor.serialName)
107+
serializeDocument(value)
108+
}
109+
104110
override fun nullField(descriptor: SdkFieldDescriptor) {
105111
jsonWriter.writeName(descriptor.serialName)
106112
serializeNull()
@@ -252,4 +258,26 @@ class JsonSerializer : Serializer, ListSerializer, MapSerializer, StructSerializ
252258
-> jsonWriter.writeValue(value.format(format))
253259
}
254260
}
261+
262+
fun serializeDocument(value: Document) {
263+
when (value) {
264+
is Document.Number -> jsonWriter.writeValue(value.value)
265+
is Document.String -> jsonWriter.writeValue(value.value)
266+
is Document.Boolean -> jsonWriter.writeValue(value.value)
267+
is Document.Null -> jsonWriter.writeNull()
268+
is Document.List -> {
269+
jsonWriter.beginArray()
270+
value.value.forEach(::serializeDocument)
271+
jsonWriter.endArray()
272+
}
273+
is Document.Map -> {
274+
jsonWriter.beginObject()
275+
value.value.entries.forEach {
276+
jsonWriter.writeName(it.key)
277+
serializeDocument(it.value)
278+
}
279+
jsonWriter.endObject()
280+
}
281+
}
282+
}
255283
}

runtime/serde/serde-json/common/src/aws/smithy/kotlin/runtime/serde/json/JsonStreamWriter.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ interface JsonStreamWriter {
5858
*/
5959
fun writeValue(bool: Boolean)
6060

61+
/**
62+
* Encodes {@code value}.
63+
*/
64+
fun writeValue(value: Number)
65+
6166
/**
6267
* Encodes {@code value}.
6368
*/

0 commit comments

Comments
 (0)