Skip to content

Commit 12cf916

Browse files
authored
Update serde implementation to support out of range floats (#3825)
## Motivation and Context Fix serde behavior to match generated code. This is important to avoid loosing data during serialization, especially as out-of-range floats often indicate an error. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent d8fbf47 commit 12cf916

File tree

3 files changed

+72
-9
lines changed

3 files changed

+72
-9
lines changed

codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SerializeImplGenerator.kt

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import software.amazon.smithy.model.shapes.BlobShape
1010
import software.amazon.smithy.model.shapes.BooleanShape
1111
import software.amazon.smithy.model.shapes.CollectionShape
1212
import software.amazon.smithy.model.shapes.DocumentShape
13+
import software.amazon.smithy.model.shapes.DoubleShape
14+
import software.amazon.smithy.model.shapes.FloatShape
1315
import software.amazon.smithy.model.shapes.MapShape
1416
import software.amazon.smithy.model.shapes.MemberShape
1517
import software.amazon.smithy.model.shapes.NumberShape
@@ -208,13 +210,45 @@ class SerializeImplGenerator(private val codegenContext: CodegenContext) {
208210
* For enums, it adds `as_str()` to convert it into a string directly.
209211
*/
210212
private fun serializeNumber(shape: NumberShape): RuntimeType {
213+
val numericType = SimpleShapes.getValue(shape::class)
214+
return when (shape) {
215+
is FloatShape, is DoubleShape -> serializeFloat(shape)
216+
else ->
217+
RuntimeType.forInlineFun(
218+
numericType.toString(),
219+
PrimitiveShapesModule,
220+
) {
221+
implSerializeConfigured(symbolBuilder(shape, numericType).build()) {
222+
rustTemplate("self.value.serialize(serializer)")
223+
}
224+
}
225+
}
226+
}
227+
228+
private fun serializeFloat(shape: NumberShape): RuntimeType {
211229
val numericType = SimpleShapes.getValue(shape::class)
212230
return RuntimeType.forInlineFun(
213231
numericType.toString(),
214232
PrimitiveShapesModule,
215233
) {
216234
implSerializeConfigured(symbolBuilder(shape, numericType).build()) {
217-
rustTemplate("self.value.serialize(serializer)")
235+
rustTemplate(
236+
"""
237+
if !self.settings.out_of_range_floats_as_strings {
238+
return self.value.serialize(serializer)
239+
}
240+
if self.value.is_nan() {
241+
serializer.serialize_str("NaN")
242+
} else if *self.value == #{ty}::INFINITY {
243+
serializer.serialize_str("Infinity")
244+
} else if *self.value == #{ty}::NEG_INFINITY {
245+
serializer.serialize_str("-Infinity")
246+
} else {
247+
self.value.serialize(serializer)
248+
}
249+
""",
250+
"ty" to numericType,
251+
)
218252
}
219253
}
220254
}

codegen-serde/src/main/kotlin/software/amazon/smithy/rust/codegen/serde/SupportStructures.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ object SupportStructures {
4747
{
4848
use #{serde}::Serialize;
4949
value
50-
.serialize_ref(&#{SerializationSettings} { redact_sensitive_fields: true })
50+
.serialize_ref(&#{SerializationSettings}::redact_sensitive_fields())
5151
.serialize(serializer)
5252
}
5353
""",
@@ -70,7 +70,7 @@ object SupportStructures {
7070
{
7171
use #{serde}::Serialize;
7272
value
73-
.serialize_ref(&#{SerializationSettings} { redact_sensitive_fields: false })
73+
.serialize_ref(&#{SerializationSettings}::leak_sensitive_fields())
7474
.serialize(serializer)
7575
}
7676
""",
@@ -211,7 +211,6 @@ object SupportStructures {
211211

212212
private fun serializationSettings() =
213213
RuntimeType.forInlineFun("SerializationSettings", supportModule) {
214-
// TODO(serde): Consider removing `derive(Default)`
215214
rustTemplate(
216215
"""
217216
/// Settings for use when serializing structures
@@ -220,17 +219,23 @@ object SupportStructures {
220219
pub struct SerializationSettings {
221220
/// Replace all sensitive fields with `<redacted>` during serialization
222221
pub redact_sensitive_fields: bool,
222+
223+
/// Serialize Nan, infinity and negative infinity as strings.
224+
///
225+
/// For protocols like JSON, this avoids the loss-of-information that occurs when these out-of-range values
226+
/// are serialized as null.
227+
pub out_of_range_floats_as_strings: bool,
223228
}
224229
225230
impl SerializationSettings {
226231
/// Replace all `@sensitive` fields with `<redacted>` when serializing.
227232
///
228233
/// Note: This may alter the type of the serialized output and make it impossible to deserialize as
229234
/// numerical fields will be replaced with strings.
230-
pub const fn redact_sensitive_fields() -> Self { Self { redact_sensitive_fields: true } }
235+
pub const fn redact_sensitive_fields() -> Self { Self { redact_sensitive_fields: true, out_of_range_floats_as_strings: false } }
231236
232237
/// Preserve the contents of sensitive fields during serializing
233-
pub const fn leak_sensitive_fields() -> Self { Self { redact_sensitive_fields: false } }
238+
pub const fn leak_sensitive_fields() -> Self { Self { redact_sensitive_fields: false, out_of_range_floats_as_strings: false } }
234239
}
235240
""",
236241
)

codegen-serde/src/test/kotlin/software/amazon/smithy/rust/codegen/serde/SerdeDecoratorTest.kt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ class SerdeDecoratorTest {
7070
blob: SensitiveBlob,
7171
constrained: Constrained,
7272
recursive: Recursive,
73-
map: EnumKeyedMap
73+
map: EnumKeyedMap,
74+
float: Float,
75+
double: Double
7476
}
7577
7678
structure Constrained {
@@ -134,6 +136,8 @@ class SerdeDecoratorTest {
134136
structure Nested {
135137
@required
136138
int: Integer,
139+
float: Float,
140+
double: Double,
137141
sensitive: Timestamps,
138142
notSensitive: AlsoTimestamps,
139143
manyEnums: TestEnumList,
@@ -202,8 +206,12 @@ class SerdeDecoratorTest {
202206
.e(Some(TestEnum::A))
203207
.document(Some(Document::String("hello!".into())))
204208
.blob(Some(Blob::new("hello")))
209+
.float(Some(f32::INFINITY))
210+
.double(Some(f64::NAN))
205211
.nested(Some(Nested::builder()
206212
.int(5)
213+
.float(Some(f32::NEG_INFINITY))
214+
.double(Some(f64::NEG_INFINITY))
207215
.sensitive(Some(sensitive_map.clone()))
208216
.not_sensitive(Some(sensitive_map))
209217
.many_enums(Some(vec![TestEnum::A]))
@@ -274,6 +282,8 @@ class SerdeDecoratorTest {
274282
"e": "A",
275283
"nested": {
276284
"int": 5,
285+
"float": "-Infinity",
286+
"double": "-Infinity",
277287
"sensitive": {
278288
"a": "1970-01-01T00:00:00Z"
279289
},
@@ -289,7 +299,9 @@ class SerdeDecoratorTest {
289299
"enum": "B"
290300
},
291301
"document": "hello!",
292-
"blob": "aGVsbG8="
302+
"blob": "aGVsbG8=",
303+
"float": "Infinity",
304+
"double": "NaN"
293305
}""".replace("\\s".toRegex(), "")
294306

295307
private val expectedRedacted =
@@ -298,6 +310,8 @@ class SerdeDecoratorTest {
298310
"e": "<redacted>",
299311
"nested": {
300312
"int": 5,
313+
"float": "-Infinity",
314+
"double": "-Infinity",
301315
"sensitive": {
302316
"a": "<redacted>"
303317
},
@@ -311,7 +325,9 @@ class SerdeDecoratorTest {
311325
},
312326
"union": "<redacted>",
313327
"document": "hello!",
314-
"blob": "<redacted>"
328+
"blob": "<redacted>",
329+
"float": "Infinity",
330+
"double": "NaN"
315331
}
316332
""".replace("\\s".toRegex(), "")
317333

@@ -343,8 +359,12 @@ class SerdeDecoratorTest {
343359
.e("A".into())
344360
.document(Document::String("hello!".into()))
345361
.blob(Blob::new("hello"))
362+
.float(f32::INFINITY)
363+
.double(f64::NAN)
346364
.nested(Nested::builder()
347365
.int(5)
366+
.float(f32::NEG_INFINITY)
367+
.double(f64::NEG_INFINITY)
348368
.sensitive("a", DateTime::from(UNIX_EPOCH))
349369
.not_sensitive("a", DateTime::from(UNIX_EPOCH))
350370
.many_enums("A".into())
@@ -355,11 +375,15 @@ class SerdeDecoratorTest {
355375
.build()
356376
.unwrap();
357377
let mut settings = #{crate}::serde::SerializationSettings::default();
378+
settings.out_of_range_floats_as_strings = true;
358379
let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize");
359380
assert_eq!(serialized, ${expectedNoRedactions.dq()});
360381
settings.redact_sensitive_fields = true;
361382
let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize");
362383
assert_eq!(serialized, ${expectedRedacted.dq()});
384+
settings.out_of_range_floats_as_strings = false;
385+
let serialized = #{serde_json}::to_string(&input.serialize_ref(&settings)).expect("failed to serialize");
386+
assert_ne!(serialized, ${expectedRedacted.dq()});
363387
""",
364388
*codegenScope,
365389
)

0 commit comments

Comments
 (0)