From d79e80c967ee15fae5d28ddf91feca4cd854bf36 Mon Sep 17 00:00:00 2001 From: John DiSanti Date: Wed, 26 May 2021 12:49:22 -0700 Subject: [PATCH] Refactor smithy-json and fix several protocol tests for the new JsonSerializerGenerator (#418) * Split out a JsonValueWriter from JsonObjectWriter/JsonArrayWriter * Add document support to JsonSerializerGenerator * Add operation support to JsonSerializerGenerator * Fix some bugs found by protocol tests * Fix struct serializer function naming bug * Fix handling of sparse lists and maps * CR feedback --- .../rust/codegen/rustlang/RustWriter.kt | 10 - .../protocols/HttpTraitProtocolGenerator.kt | 3 +- .../smithy/protocols/InlineFunctionNamer.kt | 36 ++ .../parsers/JsonSerializerGenerator.kt | 325 +++++++------- .../XmlBindingTraitSerializerGenerator.kt | 22 +- .../parsers/JsonSerializerGeneratorTest.kt | 42 +- rust-runtime/smithy-json/src/serialize.rs | 405 +++++++++--------- 7 files changed, 451 insertions(+), 392 deletions(-) create mode 100644 codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/InlineFunctionNamer.kt diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt index 024cd33abd..3ae8fe9087 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/rustlang/RustWriter.kt @@ -73,16 +73,6 @@ fun T.rust( this.write(contents, *args) } -/** - * Convenience wrapper that tells Intellij that the contents of this block are Rust - */ -fun T.rustInline( - @Language("Rust", prefix = "macro_rules! foo { () => {{ ", suffix = "}}}") contents: String, - vararg args: Any -) { - this.writeInline(contents, *args) -} - /** * Sibling method to [rustBlock] that enables `#{variablename}` style templating */ diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpTraitProtocolGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpTraitProtocolGenerator.kt index ca01f6be13..df63cb9537 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpTraitProtocolGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/HttpTraitProtocolGenerator.kt @@ -152,8 +152,7 @@ class HttpTraitProtocolGenerator( payloadName: String, serializer: StructuredDataSerializerGenerator ): BodyMetadata { - val targetShape = model.expectShape(member.target) - return when (targetShape) { + return when (val targetShape = model.expectShape(member.target)) { // Write the raw string to the payload is StringShape -> { if (targetShape.hasTrait()) { diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/InlineFunctionNamer.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/InlineFunctionNamer.kt new file mode 100644 index 0000000000..dfb1076bdb --- /dev/null +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/InlineFunctionNamer.kt @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.rust.codegen.smithy.protocols + +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider +import software.amazon.smithy.rust.codegen.util.toSnakeCase + +/** + * Creates a unique name for a serialization function. + * + * The prefixes will look like the following (for grep): + * - serialize_operation + * - serialize_structure + * - serialize_union + * - serialize_payload + */ +fun RustSymbolProvider.serializeFunctionName(shape: Shape): String = shapeFunctionName("serialize", shape) + +private fun RustSymbolProvider.shapeFunctionName(prefix: String, shape: Shape): String { + val symbolNameSnakeCase = toSymbol(shape).name.toSnakeCase() + return prefix + "_" + when (shape) { + is OperationShape -> "operation_$symbolNameSnakeCase" + is StructureShape -> "structure_$symbolNameSnakeCase" + is UnionShape -> "union_$symbolNameSnakeCase" + is MemberShape -> "payload_${shape.target.name.toSnakeCase()}_${shape.container.name.toSnakeCase()}" + else -> TODO("SerializerFunctionNamer.name: $shape") + } +} diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGenerator.kt index e44fb2f0f7..10aa15f44a 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGenerator.kt @@ -5,8 +5,7 @@ package software.amazon.smithy.rust.codegen.smithy.protocols.parsers -import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.model.knowledge.HttpBinding +import software.amazon.smithy.model.knowledge.HttpBinding.Location import software.amazon.smithy.model.knowledge.HttpBindingIndex import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape @@ -31,97 +30,105 @@ import software.amazon.smithy.rust.codegen.rustlang.asType import software.amazon.smithy.rust.codegen.rustlang.rust import software.amazon.smithy.rust.codegen.rustlang.rustBlock import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate -import software.amazon.smithy.rust.codegen.rustlang.rustInline import software.amazon.smithy.rust.codegen.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.rustlang.withBlock import software.amazon.smithy.rust.codegen.smithy.RuntimeType import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.isOptional +import software.amazon.smithy.rust.codegen.smithy.protocols.serializeFunctionName import software.amazon.smithy.rust.codegen.smithy.rustType -import software.amazon.smithy.rust.codegen.smithy.traits.SyntheticInputTrait import software.amazon.smithy.rust.codegen.util.dq -import software.amazon.smithy.rust.codegen.util.expectTrait +import software.amazon.smithy.rust.codegen.util.expectMember import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.toPascalCase -import software.amazon.smithy.rust.codegen.util.toSnakeCase private data class SimpleContext( - /** Name of the JsonObjectWriter or JsonArrayWriter */ - val writerName: String, - val localName: String, + /** Expression that retrieves a JsonValueWriter from either a JsonObjectWriter or JsonArrayWriter */ + val writerExpression: String, + /** Expression representing the value to write to the JsonValueWriter */ + val valueExpression: ValueExpression, val shape: T, ) -private data class StructContext( - /** Name of the JsonObjectWriter */ - val objectName: String, - val localName: String, - val shape: StructureShape, - val symbolProvider: RustSymbolProvider, -) { - fun member(member: MemberShape): MemberContext = - MemberContext(objectName, MemberDestination.Object(), "$localName.${symbolProvider.toMemberName(member)}", member) -} - -private sealed class MemberDestination { - // Add unused parameter so that Kotlin generates equals/hashCode for us - data class Array(private val unused: Int = 0) : MemberDestination() - data class Object(val keyNameOverride: String? = null) : MemberDestination() -} +private typealias CollectionContext = SimpleContext +private typealias MapContext = SimpleContext +private typealias UnionContext = SimpleContext private data class MemberContext( - /** Name of the JsonObjectWriter or JsonArrayWriter */ - val writerName: String, - val destination: MemberDestination, - val valueExpression: String, + /** Expression that retrieves a JsonValueWriter from either a JsonObjectWriter or JsonArrayWriter */ + val writerExpression: String, + /** Expression representing the value to write to the JsonValueWriter */ + val valueExpression: ValueExpression, val shape: MemberShape, + /** Whether or not to serialize null values if the type is optional */ + val writeNulls: Boolean = false, ) { - val keyExpression: String = when (destination) { - is MemberDestination.Object -> - destination.keyNameOverride ?: (shape.getTrait()?.value ?: shape.memberName).dq() - is MemberDestination.Array -> "" - } + companion object { + fun collectionMember(context: CollectionContext, itemName: String): MemberContext = + MemberContext( + "${context.writerExpression}.value()", + ValueExpression.Reference(itemName), + context.shape.member, + writeNulls = true + ) - /** Generates an expression that serializes the given [value] expression to the object/array */ - fun writeValue(w: RustWriter, writerFn: JsonWriterFn, key: String, value: String) = when (destination) { - is MemberDestination.Object -> w.rust("$writerName.$writerFn($key, $value);") - is MemberDestination.Array -> w.rust("$writerName.$writerFn($value);") - } + fun mapMember(context: MapContext, key: String, value: String): MemberContext = + MemberContext( + "${context.writerExpression}.key($key)", + ValueExpression.Reference(value), + context.shape.value, + writeNulls = true + ) - /** Generates an expression that serializes the given [inner] expression to the object/array */ - fun writeInner(w: RustWriter, writerFn: JsonWriterFn, key: String, inner: RustWriter.() -> Unit) { - w.withBlock("$writerName.$writerFn(", ");") { - if (destination is MemberDestination.Object) { - w.writeInline("$key, ") - } - inner(w) - } - } + fun structMember(context: StructContext, member: MemberShape, symProvider: RustSymbolProvider): MemberContext = + MemberContext( + objectValueWriterExpression(context.objectName, member), + ValueExpression.Value("${context.localName}.${symProvider.toMemberName(member)}"), + member + ) - /** Generates a mutable declaration for serializing a new object */ - fun writeStartObject(w: RustWriter, decl: String, key: String) = when (destination) { - is MemberDestination.Object -> w.rust("let mut $decl = $writerName.start_object($key);") - is MemberDestination.Array -> w.rust("let mut $decl = $writerName.start_object();") - } + fun unionMember(context: UnionContext, variantReference: String, member: MemberShape): MemberContext = + MemberContext( + objectValueWriterExpression(context.writerExpression, member), + ValueExpression.Reference(variantReference), + member + ) - /** Generates a mutable declaration for serializing a new array */ - fun writeStartArray(w: RustWriter, decl: String, key: String) = when (destination) { - is MemberDestination.Object -> w.rust("let mut $decl = $writerName.start_array($key);") - is MemberDestination.Array -> w.rust("let mut $decl = $writerName.start_array();") + /** Returns an expression to get a JsonValueWriter from a JsonObjectWriter */ + private fun objectValueWriterExpression(objectWriterName: String, member: MemberShape): String { + val wireName = (member.getTrait()?.value ?: member.memberName).dq() + return "$objectWriterName.key($wireName)" + } } } -private enum class JsonWriterFn { - BOOLEAN, - INSTANT, - NUMBER, - STRING, - STRING_UNCHECKED; +// Specialized since it holds a JsonObjectWriter expression rather than a JsonValueWriter +private data class StructContext( + /** Name of the JsonObjectWriter */ + val objectName: String, + /** Name of the variable that holds the struct */ + val localName: String, + val shape: StructureShape, +) + +private sealed class ValueExpression { + abstract val name: String + + data class Reference(override val name: String) : ValueExpression() + data class Value(override val name: String) : ValueExpression() - override fun toString(): String = name.toLowerCase() + fun asValue(): String = when (this) { + is Reference -> "*$name" + is Value -> name + } + + fun asRef(): String = when (this) { + is Reference -> name + is Value -> "&$name" + } } class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSerializerGenerator { @@ -136,12 +143,13 @@ class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSe "Error" to serializerError, "SdkBody" to RuntimeType.sdkBody(runtimeConfig), "JsonObjectWriter" to smithyJson.member("serialize::JsonObjectWriter"), + "JsonValueWriter" to smithyJson.member("serialize::JsonValueWriter"), ) private val httpIndex = HttpBindingIndex.of(model) override fun payloadSerializer(member: MemberShape): RuntimeType { + val fnName = symbolProvider.serializeFunctionName(member) val target = model.expectShape(member.target, StructureShape::class.java) - val fnName = "serialize_payload_${target.id.name.toSnakeCase()}_${member.container.name.toSnakeCase()}" return RuntimeType.forInlineFun(fnName, "operation_ser") { writer -> writer.rustBlockTemplate( "pub fn $fnName(input: &#{target}) -> Result<#{SdkBody}, #{Error}>", @@ -150,7 +158,7 @@ class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSe ) { rust("let mut out = String::new();") rustTemplate("let mut object = #{JsonObjectWriter}::new(&mut out);", *codegenScope) - serializeStructure(StructContext("object", "input", target, symbolProvider)) + serializeStructure(StructContext("object", "input", target)) rust("object.finish();") rustTemplate("Ok(#{SdkBody}::from(out))", *codegenScope) } @@ -159,16 +167,28 @@ class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSe override fun operationSerializer(operationShape: OperationShape): RuntimeType? { val inputShape = operationShape.inputShape(model) - val inputShapeName = inputShape.expectTrait().originalId?.name - ?: throw CodegenException("operation must have a name if it has members") - val fnName = "serialize_operation_${inputShapeName.toSnakeCase()}" + + // Don't generate an operation JSON serializer if there is no JSON body + val httpBindings = httpIndex.getRequestBindings(operationShape) + val hasDocumentHttpBindings = httpBindings + .filter { it.value.location == Location.DOCUMENT } + .keys.map { inputShape.expectMember(it) } + .isNotEmpty() + if (inputShape.members().isEmpty() || httpBindings.isNotEmpty() && !hasDocumentHttpBindings) { + return null + } + + val fnName = symbolProvider.serializeFunctionName(operationShape) return RuntimeType.forInlineFun(fnName, "operation_ser") { it.rustBlockTemplate( "pub fn $fnName(input: &#{target}) -> Result<#{SdkBody}, #{Error}>", *codegenScope, "target" to symbolProvider.toSymbol(inputShape) ) { - // TODO: Implement operation serialization - rust("unimplemented!()") + rust("let mut out = String::new();") + rustTemplate("let mut object = #{JsonObjectWriter}::new(&mut out);", *codegenScope) + serializeStructure(StructContext("object", "input", inputShape)) + rust("object.finish();") + rustTemplate("Ok(#{SdkBody}::from(out))", *codegenScope) } } } @@ -177,10 +197,11 @@ class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSe val fnName = "serialize_document" return RuntimeType.forInlineFun(fnName, "operation_ser") { it.rustTemplate( - // TODO: Implement document parsing """ pub fn $fnName(input: &#{Document}) -> Result<#{SdkBody}, #{Error}> { - unimplemented!(); + let mut out = String::new(); + #{JsonValueWriter}::new(&mut out).document(input); + Ok(#{SdkBody}::from(out)) } """, "Document" to RuntimeType.Document(runtimeConfig), *codegenScope @@ -189,19 +210,21 @@ class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSe } private fun RustWriter.serializeStructure(context: StructContext) { - val fnName = "serialize_structure_${context.shape.id.name.toSnakeCase()}" + val fnName = symbolProvider.serializeFunctionName(context.shape) val structureSymbol = symbolProvider.toSymbol(context.shape) val structureSerializer = RuntimeType.forInlineFun(fnName, "json_ser") { writer -> writer.rustBlockTemplate( - "pub fn $fnName(${context.objectName}: &mut #{JsonObjectWriter}, input: &#{Shape})", - "Shape" to structureSymbol, + "pub fn $fnName(object: &mut #{JsonObjectWriter}, input: &#{Input})", + "Input" to structureSymbol, *codegenScope, ) { - if (context.shape.members().isEmpty()) { - rust("let _ = input;") // Suppress an unused argument warning - } - for (member in context.shape.members()) { - serializeMember(context.member(member)) + context.copy(objectName = "object", localName = "input").also { inner -> + if (inner.shape.members().isEmpty()) { + rust("let (_, _) = (object, input);") // Suppress unused argument warnings + } + for (member in inner.shape.members()) { + serializeMember(MemberContext.structMember(inner, member, symbolProvider)) + } } } } @@ -209,60 +232,71 @@ class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSe } private fun RustWriter.serializeMember(context: MemberContext) { - val target = model.expectShape(context.shape.target) - handleOptional(context) { inner -> - val key = inner.keyExpression - val value = "&${inner.valueExpression}" - when (target) { - is StringShape -> when (target.hasTrait()) { - true -> context.writeValue(this, JsonWriterFn.STRING, key, "$value.as_str()") - false -> context.writeValue(this, JsonWriterFn.STRING, key, value) - } - is BooleanShape -> context.writeValue(this, JsonWriterFn.BOOLEAN, key, value) - is NumberShape -> { - val numberType = when (symbolProvider.toSymbol(target).rustType()) { - is RustType.Float -> "Float" - is RustType.Integer -> "NegInt" - else -> throw IllegalStateException("unreachable") - } - context.writeInner(this, JsonWriterFn.NUMBER, key) { - rustInline("#T::$numberType(*${inner.valueExpression})", smithyTypes.member("Number")) - } - } - is BlobShape -> context.writeInner(this, JsonWriterFn.STRING_UNCHECKED, key) { - rustInline("&#T($value)", RuntimeType.Base64Encode(runtimeConfig)) + val targetShape = model.expectShape(context.shape.target) + if (symbolProvider.toSymbol(context.shape).isOptional()) { + safeName().also { local -> + rustBlock("if let Some($local) = ${context.valueExpression.asRef()}") { + val innerContext = context.copy(valueExpression = ValueExpression.Reference(local)) + serializeMemberValue(innerContext, targetShape) } - is TimestampShape -> { - val timestampFormat = - httpIndex.determineTimestampFormat(context.shape, HttpBinding.Location.DOCUMENT, EPOCH_SECONDS) - val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) - context.writeInner(this, JsonWriterFn.INSTANT, key) { - rustInline("$value, #T", timestampFormatType) + if (context.writeNulls) { + rustBlock("else") { + rust("${context.writerExpression}.null();") } } - is CollectionShape -> jsonArrayWriter(inner) { arrayName -> - serializeCollection(SimpleContext(arrayName, inner.valueExpression, target)) - } - is MapShape -> jsonObjectWriter(inner) { objectName -> - serializeMap(SimpleContext(objectName, inner.valueExpression, target)) - } - is StructureShape -> jsonObjectWriter(inner) { objectName -> - serializeStructure(StructContext(objectName, inner.valueExpression, target, symbolProvider)) - } - is UnionShape -> jsonObjectWriter(inner) { objectName -> - serializeUnion(SimpleContext(objectName, inner.valueExpression, target)) - } - is DocumentShape -> { - // TODO: Implement document shapes + } + } else { + serializeMemberValue(context, targetShape) + } + } + + private fun RustWriter.serializeMemberValue(context: MemberContext, target: Shape) { + val writer = context.writerExpression + val value = context.valueExpression + when (target) { + is StringShape -> when (target.hasTrait()) { + true -> rust("$writer.string(${value.name}.as_str());") + false -> rust("$writer.string(${value.name});") + } + is BooleanShape -> rust("$writer.boolean(${value.asValue()});") + is NumberShape -> { + val numberType = when (symbolProvider.toSymbol(target).rustType()) { + is RustType.Float -> "Float" + is RustType.Integer -> "NegInt" + else -> throw IllegalStateException("unreachable") } - else -> TODO(target.toString()) + rust("$writer.number(#T::$numberType((${value.asValue()}).into()));", smithyTypes.member("Number")) + } + is BlobShape -> rust( + "$writer.string_unchecked(&#T(${value.name}));", + RuntimeType.Base64Encode(runtimeConfig) + ) + is TimestampShape -> { + val timestampFormat = + httpIndex.determineTimestampFormat(context.shape, Location.DOCUMENT, EPOCH_SECONDS) + val timestampFormatType = RuntimeType.TimestampFormat(runtimeConfig, timestampFormat) + rust("$writer.instant(${value.name}, #T);", timestampFormatType) + } + is CollectionShape -> jsonArrayWriter(context) { arrayName -> + serializeCollection(CollectionContext(arrayName, context.valueExpression, target)) + } + is MapShape -> jsonObjectWriter(context) { objectName -> + serializeMap(MapContext(objectName, context.valueExpression, target)) + } + is StructureShape -> jsonObjectWriter(context) { objectName -> + serializeStructure(StructContext(objectName, context.valueExpression.name, target)) + } + is UnionShape -> jsonObjectWriter(context) { objectName -> + serializeUnion(UnionContext(objectName, context.valueExpression, target)) } + is DocumentShape -> rust("$writer.document(${value.asRef()});") + else -> TODO(target.toString()) } } private fun RustWriter.jsonArrayWriter(context: MemberContext, inner: RustWriter.(String) -> Unit) { safeName("array").also { arrayName -> - context.writeStartArray(this, arrayName, context.keyExpression) + rust("let mut $arrayName = ${context.writerExpression}.start_array();") inner(arrayName) rust("$arrayName.finish();") } @@ -270,61 +304,46 @@ class JsonSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSe private fun RustWriter.jsonObjectWriter(context: MemberContext, inner: RustWriter.(String) -> Unit) { safeName("object").also { objectName -> - context.writeStartObject(this, objectName, context.keyExpression) + rust("let mut $objectName = ${context.writerExpression}.start_object();") inner(objectName) rust("$objectName.finish();") } } - private fun RustWriter.serializeCollection(context: SimpleContext) { + private fun RustWriter.serializeCollection(context: CollectionContext) { val itemName = safeName("item") - rustBlock("for $itemName in ${context.localName}") { - serializeMember(MemberContext(context.writerName, MemberDestination.Array(), itemName, context.shape.member)) + rustBlock("for $itemName in ${context.valueExpression.asRef()}") { + serializeMember(MemberContext.collectionMember(context, itemName)) } } - private fun RustWriter.serializeMap(context: SimpleContext) { + private fun RustWriter.serializeMap(context: MapContext) { val keyName = safeName("key") val valueName = safeName("value") - val valueShape = context.shape.value - rustBlock("for ($keyName, $valueName) in ${context.localName}") { - serializeMember( - MemberContext(context.writerName, MemberDestination.Object(keyNameOverride = keyName), valueName, valueShape) - ) + rustBlock("for ($keyName, $valueName) in ${context.valueExpression.asRef()}") { + serializeMember(MemberContext.mapMember(context, keyName, valueName)) } } - private fun RustWriter.serializeUnion(context: SimpleContext) { - val fnName = "serialize_union_${context.shape.id.name.toSnakeCase()}" + private fun RustWriter.serializeUnion(context: UnionContext) { + val fnName = symbolProvider.serializeFunctionName(context.shape) val unionSymbol = symbolProvider.toSymbol(context.shape) val unionSerializer = RuntimeType.forInlineFun(fnName, "json_ser") { writer -> writer.rustBlockTemplate( - "pub fn $fnName(${context.writerName}: &mut #{JsonObjectWriter}, input: &#{Shape})", - "Shape" to unionSymbol, + "pub fn $fnName(${context.writerExpression}: &mut #{JsonObjectWriter}, input: &#{Input})", + "Input" to unionSymbol, *codegenScope, ) { rustBlock("match input") { for (member in context.shape.members()) { val variantName = member.memberName.toPascalCase() withBlock("#T::$variantName(inner) => {", "},", unionSymbol) { - serializeMember(MemberContext(context.writerName, MemberDestination.Object(), "inner", member)) + serializeMember(MemberContext.unionMember(context, "inner", member)) } } } } } - rust("#T(&mut ${context.writerName}, ${context.localName});", unionSerializer) - } - - private fun RustWriter.handleOptional(context: MemberContext, inner: RustWriter.(MemberContext) -> Unit) { - if (symbolProvider.toSymbol(context.shape).isOptional()) { - safeName().also { localDecl -> - rustBlock("if let Some($localDecl) = &${context.valueExpression}") { - inner(context.copy(valueExpression = localDecl)) - } - } - } else { - inner(context) - } + rust("#T(&mut ${context.writerExpression}, ${context.valueExpression.asRef()});", unionSerializer) } } diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/XmlBindingTraitSerializerGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/XmlBindingTraitSerializerGenerator.kt index e1bf8770b6..525c7f7dc2 100644 --- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/XmlBindingTraitSerializerGenerator.kt +++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/XmlBindingTraitSerializerGenerator.kt @@ -41,6 +41,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.ProtocolConfig import software.amazon.smithy.rust.codegen.smithy.isOptional import software.amazon.smithy.rust.codegen.smithy.protocols.XmlMemberIndex import software.amazon.smithy.rust.codegen.smithy.protocols.XmlNameIndex +import software.amazon.smithy.rust.codegen.smithy.protocols.serializeFunctionName import software.amazon.smithy.rust.codegen.smithy.rustType import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.expectMember @@ -48,7 +49,6 @@ import software.amazon.smithy.rust.codegen.util.getTrait import software.amazon.smithy.rust.codegen.util.hasTrait import software.amazon.smithy.rust.codegen.util.inputShape import software.amazon.smithy.rust.codegen.util.toPascalCase -import software.amazon.smithy.rust.codegen.util.toSnakeCase class XmlBindingTraitSerializerGenerator(protocolConfig: ProtocolConfig) : StructuredDataSerializerGenerator { private val symbolProvider = protocolConfig.symbolProvider @@ -95,7 +95,7 @@ class XmlBindingTraitSerializerGenerator(protocolConfig: ProtocolConfig) : Struc this.copy(input = "$input.${symbolProvider.toMemberName(member)}") override fun operationSerializer(operationShape: OperationShape): RuntimeType? { - val fnName = "serialize_operation_${operationShape.id.name.toSnakeCase()}" + val fnName = symbolProvider.serializeFunctionName(operationShape) val inputShape = operationShape.inputShape(model) val xmlMembers = operationShape.operationXmlMembers() if (!xmlMembers.isNotEmpty()) { @@ -132,8 +132,8 @@ class XmlBindingTraitSerializerGenerator(protocolConfig: ProtocolConfig) : Struc } override fun payloadSerializer(member: MemberShape): RuntimeType { + val fnName = symbolProvider.serializeFunctionName(member) val target = model.expectShape(member.target, StructureShape::class.java) - val fnName = "serialize_payload_${target.id.name.toSnakeCase()}_${member.container.name.toSnakeCase()}" return RuntimeType.forInlineFun(fnName, "xml_ser") { val t = symbolProvider.toSymbol(member).rustType().stripOuter().render(true) it.rustBlock( @@ -274,12 +274,12 @@ class XmlBindingTraitSerializerGenerator(protocolConfig: ProtocolConfig) : Struc members: XmlMemberIndex, ctx: Ctx.Element ) { - val fnName = "serialize_structure_${structureShape.id.name.toSnakeCase()}" val structureSymbol = symbolProvider.toSymbol(structureShape) + val fnName = symbolProvider.serializeFunctionName(structureShape) val structureSerializer = RuntimeType.forInlineFun(fnName, "xml_ser") { it.rustBlockTemplate( - "pub fn $fnName(input: &#{Shape}, writer: #{ElementWriter})", - "Shape" to structureSymbol, + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter})", + "Input" to structureSymbol, *codegenScope ) { if (!members.isNotEmpty()) { @@ -293,12 +293,12 @@ class XmlBindingTraitSerializerGenerator(protocolConfig: ProtocolConfig) : Struc } private fun RustWriter.serializeUnion(unionShape: UnionShape, ctx: Ctx.Element) { - val fnName = "serialize_union_${unionShape.id.name.toSnakeCase()}" + val fnName = symbolProvider.serializeFunctionName(unionShape) val unionSymbol = symbolProvider.toSymbol(unionShape) val structureSerializer = RuntimeType.forInlineFun(fnName, "xml_ser") { it.rustBlockTemplate( - "pub fn $fnName(input: &#{Shape}, writer: #{ElementWriter})", - "Shape" to unionSymbol, + "pub fn $fnName(input: &#{Input}, writer: #{ElementWriter})", + "Input" to unionSymbol, *codegenScope ) { rust("let mut scope_writer = writer.finish();") @@ -369,10 +369,10 @@ class XmlBindingTraitSerializerGenerator(protocolConfig: ProtocolConfig) : Struc } private fun OperationShape.operationXmlMembers(): XmlMemberIndex { - val outputShape = this.inputShape(model) + val inputShape = this.inputShape(model) val documentMembers = httpIndex.getRequestBindings(this).filter { it.value.location == HttpBinding.Location.DOCUMENT } - .keys.map { outputShape.expectMember(it) } + .keys.map { inputShape.expectMember(it) } return XmlMemberIndex.fromMembers(documentMembers) } diff --git a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGeneratorTest.kt b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGeneratorTest.kt index 55a2bfa2e8..311593f070 100644 --- a/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGeneratorTest.kt +++ b/codegen/src/test/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parsers/JsonSerializerGeneratorTest.kt @@ -26,15 +26,20 @@ class JsonSerializerGeneratorTest { use aws.protocols#restJson1 union Choice { - map: MyMap, - list: SomeList, - s: String, - enum: FooEnum, + blob: Blob, + boolean: Boolean, date: Timestamp, + document: Document, + enum: FooEnum, + int: Integer, + list: SomeList, + listSparse: SomeSparseList, + long: Long, + map: MyMap, + mapSparse: MySparseMap, number: Double, + s: String, top: Top, - blob: Blob, - document: Document, } @enum([{name: "FOO", value: "FOO"}]) @@ -45,10 +50,21 @@ class JsonSerializerGeneratorTest { value: Choice, } + @sparse + map MySparseMap { + key: String, + value: Choice, + } + list SomeList { member: Choice } + @sparse + list SomeSparseList { + member: Choice + } + structure Top { choice: Choice, field: String, @@ -64,8 +80,8 @@ class JsonSerializerGeneratorTest { structure OpInput { @httpHeader("x-test") someHeader: String, - @httpPayload - payload: Top + + top: Top } @http(uri: "/top", method: "POST") @@ -84,7 +100,6 @@ class JsonSerializerGeneratorTest { ) val symbolProvider = testSymbolProvider(model) val parserGenerator = JsonSerializerGenerator(testProtocolConfig(model)) - val payloadGenerator = parserGenerator.payloadSerializer(model.lookup("test#OpInput\$payload")) val operationGenerator = parserGenerator.operationSerializer(model.lookup("test#Op")) val documentGenerator = parserGenerator.documentSerializer() @@ -94,20 +109,19 @@ class JsonSerializerGeneratorTest { """ use model::Top; - // Generate the operation/document serializers even if they're not directly tested - // ${writer.format(operationGenerator!!)} + // Generate the document serializer even though it's not tested directly // ${writer.format(documentGenerator)} - let inp = crate::input::OpInput::builder().payload( + let input = crate::input::OpInput::builder().top( Top::builder() .field("hello!") .extra(45) .recursive(Top::builder().extra(55).build()) .build() ).build().unwrap(); - let serialized = ${writer.format(payloadGenerator)}(&inp.payload.unwrap()).unwrap(); + let serialized = ${writer.format(operationGenerator!!)}(&input).unwrap(); let output = std::str::from_utf8(serialized.bytes().unwrap()).unwrap(); - assert_eq!(output, r#"{"field":"hello!","extra":45,"rec":[{"extra":55}]}"#); + assert_eq!(output, r#"{"top":{"field":"hello!","extra":45,"rec":[{"extra":55}]}}"#); """ ) } diff --git a/rust-runtime/smithy-json/src/serialize.rs b/rust-runtime/smithy-json/src/serialize.rs index 8d8a793290..4eefce52e6 100644 --- a/rust-runtime/smithy-json/src/serialize.rs +++ b/rust-runtime/smithy-json/src/serialize.rs @@ -5,86 +5,131 @@ use crate::escape::escape_string; use smithy_types::instant::Format; -use smithy_types::{Instant, Number}; +use smithy_types::{Document, Instant, Number}; use std::borrow::Cow; -pub struct JsonObjectWriter<'a> { - json: &'a mut String, - started: bool, +pub struct JsonValueWriter<'a> { + output: &'a mut String, } -impl<'a> JsonObjectWriter<'a> { +impl<'a> JsonValueWriter<'a> { pub fn new(output: &'a mut String) -> Self { - output.push('{'); - Self { - json: output, - started: false, - } + JsonValueWriter { output } } - /// Writes a null value with the given `key`. - pub fn null(&mut self, key: &str) -> &mut Self { - self.key(key); - self.json.push_str("null"); - self + /// Writes a null value. + pub fn null(self) { + self.output.push_str("null"); } - /// Writes the boolean `value` with the given `key`. - pub fn boolean(&mut self, key: &str, value: bool) -> &mut Self { - self.key(key); - self.json.push_str(match value { + /// Writes the boolean `value`. + pub fn boolean(self, value: bool) { + self.output.push_str(match value { true => "true", _ => "false", }); - self } - /// Writes a string `value` with the given `key`. - pub fn string(&mut self, key: &str, value: &str) -> &mut Self { - self.key(key); - append_string(&mut self.json, value); - self + /// Writes a document `value`. + pub fn document(self, value: &Document) { + match value { + Document::Array(values) => { + let mut array = self.start_array(); + for value in values { + array.value().document(value); + } + array.finish(); + } + Document::Bool(value) => self.boolean(*value), + Document::Null => self.null(), + Document::Number(value) => self.number(*value), + Document::Object(values) => { + let mut object = self.start_object(); + for (key, value) in values { + object.key(key).document(value); + } + object.finish(); + } + Document::String(value) => self.string(&value), + } + } + + /// Writes a string `value`. + pub fn string(self, value: &str) { + self.output.push('"'); + self.output.push_str(&escape_string(value)); + self.output.push('"'); } - /// Writes a string `value` with the given `key` without escaping it. - pub fn string_unchecked(&mut self, key: &str, value: &str) -> &mut Self { - self.key(key); - append_string_unchecked(&mut self.json, value); - self + /// Writes a string `value` without escaping it. + pub fn string_unchecked(self, value: &str) { + // Verify in debug builds that we don't actually need to escape the string + debug_assert!(matches!(escape_string(value), Cow::Borrowed(_))); + + self.output.push('"'); + self.output.push_str(value); + self.output.push('"'); } - /// Writes a number `value` with the given `key`. - pub fn number(&mut self, key: &str, value: Number) -> &mut Self { - self.key(key); - append_number(&mut self.json, value); - self + /// Writes a number `value`. + pub fn number(self, value: Number) { + match value { + Number::PosInt(value) => { + // itoa::Buffer is a fixed-size stack allocation, so this is cheap + self.output.push_str(itoa::Buffer::new().format(value)); + } + Number::NegInt(value) => { + self.output.push_str(itoa::Buffer::new().format(value)); + } + Number::Float(value) => { + // If the value is NaN, Infinity, or -Infinity + if value.is_nan() || value.is_infinite() { + self.output.push_str("null"); + } else { + // ryu::Buffer is a fixed-size stack allocation, so this is cheap + self.output + .push_str(ryu::Buffer::new().format_finite(value)); + } + } + } } - /// Writes an Instant `value` with the given `key` and `format`. - pub fn instant(&mut self, key: &str, instant: &Instant, format: Format) -> &mut Self { - self.key(key); - append_instant(&mut self.json, instant, format); - self + /// Writes an Instant `value` with the given `format`. + pub fn instant(self, instant: &Instant, format: Format) { + let formatted = instant.fmt(format); + match format { + Format::EpochSeconds => self.output.push_str(&formatted), + _ => self.string(&formatted), + } } - /// Starts an array with the given `key`. - pub fn start_array(&mut self, key: &str) -> JsonArrayWriter { - self.key(key); - JsonArrayWriter::new(&mut self.json) + /// Starts an array. + pub fn start_array(self) -> JsonArrayWriter<'a> { + JsonArrayWriter::new(self.output) } - /// Starts an object with the given `key`. - pub fn start_object(&mut self, key: &str) -> JsonObjectWriter { - self.key(key); - JsonObjectWriter::new(&mut self.json) + /// Starts an object. + pub fn start_object(self) -> JsonObjectWriter<'a> { + JsonObjectWriter::new(self.output) } +} - /// Finishes the object. - pub fn finish(self) { - self.json.push('}'); +pub struct JsonObjectWriter<'a> { + json: &'a mut String, + started: bool, +} + +impl<'a> JsonObjectWriter<'a> { + pub fn new(output: &'a mut String) -> Self { + output.push('{'); + Self { + json: output, + started: false, + } } - fn key(&mut self, key: &str) { + /// Starts a value with the given `key`. + pub fn key(&mut self, key: &str) -> JsonValueWriter { if self.started { self.json.push(','); } @@ -93,6 +138,13 @@ impl<'a> JsonObjectWriter<'a> { self.json.push('"'); self.json.push_str(&escape_string(key)); self.json.push_str("\":"); + + JsonValueWriter::new(&mut self.json) + } + + /// Finishes the object. + pub fn finish(self) { + self.json.push('}'); } } @@ -110,61 +162,10 @@ impl<'a> JsonArrayWriter<'a> { } } - /// Writes a null value to the array. - pub fn null(&mut self) -> &mut Self { - self.comma_delimit(); - self.json.push_str("null"); - self - } - - /// Writes the boolean `value` to the array. - pub fn boolean(&mut self, value: bool) -> &mut Self { - self.comma_delimit(); - self.json.push_str(match value { - true => "true", - _ => "false", - }); - self - } - - /// Writes a string to the array. - pub fn string(&mut self, value: &str) -> &mut Self { - self.comma_delimit(); - append_string(&mut self.json, value); - self - } - - /// Writes a string `value` to the array without escaping it. - pub fn string_unchecked(&mut self, value: &str) -> &mut Self { - self.comma_delimit(); - append_string_unchecked(&mut self.json, value); - self - } - - /// Writes a number `value` to the array. - pub fn number(&mut self, value: Number) -> &mut Self { - self.comma_delimit(); - append_number(&mut self.json, value); - self - } - - /// Writes an Instant `value` using `format` to the array. - pub fn instant(&mut self, instant: &Instant, format: Format) -> &mut Self { + /// Starts a new value in the array. + pub fn value(&mut self) -> JsonValueWriter { self.comma_delimit(); - append_instant(&mut self.json, instant, format); - self - } - - /// Starts a nested array inside of the array. - pub fn start_array(&mut self) -> JsonArrayWriter { - self.comma_delimit(); - JsonArrayWriter::new(&mut self.json) - } - - /// Starts a nested object inside of the array. - pub fn start_object(&mut self) -> JsonObjectWriter { - self.comma_delimit(); - JsonObjectWriter::new(&mut self.json) + JsonValueWriter::new(&mut self.json) } /// Finishes the array. @@ -180,57 +181,13 @@ impl<'a> JsonArrayWriter<'a> { } } -fn append_string(json: &mut String, value: &str) { - json.push('"'); - json.push_str(&escape_string(value)); - json.push('"'); -} - -fn append_string_unchecked(json: &mut String, value: &str) { - // Verify in debug builds that we don't actually need to escape the string - debug_assert!(matches!(escape_string(value), Cow::Borrowed(_))); - - json.push('"'); - json.push_str(value); - json.push('"'); -} - -fn append_instant(json: &mut String, value: &Instant, format: Format) { - let formatted = value.fmt(format); - match format { - Format::EpochSeconds => json.push_str(&formatted), - _ => append_string(json, &formatted), - } -} - -fn append_number(json: &mut String, value: Number) { - match value { - Number::PosInt(value) => { - // itoa::Buffer is a fixed-size stack allocation, so this is cheap - json.push_str(itoa::Buffer::new().format(value)); - } - Number::NegInt(value) => { - json.push_str(itoa::Buffer::new().format(value)); - } - Number::Float(value) => { - // If the value is NaN, Infinity, or -Infinity - if value.is_nan() || value.is_infinite() { - json.push_str("null"); - } else { - // ryu::Buffer is a fixed-size stack allocation, so this is cheap - json.push_str(ryu::Buffer::new().format_finite(value)); - } - } - } -} - #[cfg(test)] mod tests { use super::{JsonArrayWriter, JsonObjectWriter}; - use crate::serialize::append_number; + use crate::serialize::JsonValueWriter; use proptest::proptest; use smithy_types::instant::Format; - use smithy_types::{Instant, Number}; + use smithy_types::{Document, Instant, Number}; #[test] fn empty() { @@ -247,9 +204,9 @@ mod tests { fn object_inside_array() { let mut output = String::new(); let mut array = JsonArrayWriter::new(&mut output); - array.start_object().finish(); - array.start_object().finish(); - array.start_object().finish(); + array.value().start_object().finish(); + array.value().start_object().finish(); + array.value().start_object().finish(); array.finish(); assert_eq!("[{},{},{}]", &output); } @@ -259,8 +216,8 @@ mod tests { let mut output = String::new(); let mut obj_1 = JsonObjectWriter::new(&mut output); - let mut obj_2 = obj_1.start_object("nested"); - obj_2.string("test", "test"); + let mut obj_2 = obj_1.key("nested").start_object(); + obj_2.key("test").string("test"); obj_2.finish(); obj_1.finish(); @@ -271,8 +228,8 @@ mod tests { fn array_inside_object() { let mut output = String::new(); let mut object = JsonObjectWriter::new(&mut output); - object.start_array("foo").finish(); - object.start_array("ba\nr").finish(); + object.key("foo").start_array().finish(); + object.key("ba\nr").start_array().finish(); object.finish(); assert_eq!(r#"{"foo":[],"ba\nr":[]}"#, &output); } @@ -283,11 +240,11 @@ mod tests { let mut arr_1 = JsonArrayWriter::new(&mut output); - let mut arr_2 = arr_1.start_array(); - arr_2.number(Number::PosInt(5)); + let mut arr_2 = arr_1.value().start_array(); + arr_2.value().number(Number::PosInt(5)); arr_2.finish(); - arr_1.start_array().finish(); + arr_1.value().start_array().finish(); arr_1.finish(); assert_eq!("[[5],[]]", &output); @@ -297,21 +254,20 @@ mod tests { fn object() { let mut output = String::new(); let mut object = JsonObjectWriter::new(&mut output); - object.boolean("true_val", true); - object.boolean("false_val", false); - object.string("some_string", "some\nstring\nvalue"); - object.string_unchecked("unchecked_str", "unchecked"); - object.number("some_number", Number::Float(3.5)); - object.null("some_null"); - - let mut array = object.start_array("some_mixed_array"); - array - .string("1") - .number(Number::NegInt(-2)) - .string_unchecked("unchecked") - .boolean(true) - .boolean(false) - .null(); + object.key("true_val").boolean(true); + object.key("false_val").boolean(false); + object.key("some_string").string("some\nstring\nvalue"); + object.key("unchecked_str").string_unchecked("unchecked"); + object.key("some_number").number(Number::Float(3.5)); + object.key("some_null").null(); + + let mut array = object.key("some_mixed_array").start_array(); + array.value().string("1"); + array.value().number(Number::NegInt(-2)); + array.value().string_unchecked("unchecked"); + array.value().boolean(true); + array.value().boolean(false); + array.value().null(); array.finish(); object.finish(); @@ -327,18 +283,14 @@ mod tests { let mut output = String::new(); let mut object = JsonObjectWriter::new(&mut output); - object.instant( - "epoch_seconds", - &Instant::from_f64(5.2), - Format::EpochSeconds, - ); - object.instant( - "date_time", + object + .key("epoch_seconds") + .instant(&Instant::from_f64(5.2), Format::EpochSeconds); + object.key("date_time").instant( &Instant::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), Format::DateTime, ); - object.instant( - "http_date", + object.key("http_date").instant( &Instant::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), Format::HttpDate, ); @@ -355,12 +307,14 @@ mod tests { let mut output = String::new(); let mut array = JsonArrayWriter::new(&mut output); - array.instant(&Instant::from_f64(5.2), Format::EpochSeconds); - array.instant( + array + .value() + .instant(&Instant::from_f64(5.2), Format::EpochSeconds); + array.value().instant( &Instant::from_str("2021-05-24T15:34:50.123Z", Format::DateTime).unwrap(), Format::DateTime, ); - array.instant( + array.value().instant( &Instant::from_str("Wed, 21 Oct 2015 07:28:00 GMT", Format::HttpDate).unwrap(), Format::HttpDate, ); @@ -372,26 +326,73 @@ mod tests { ) } + fn format_document(document: Document) -> String { + let mut output = String::new(); + JsonValueWriter::new(&mut output).document(&document); + output + } + + #[test] + fn document() { + assert_eq!("null", format_document(Document::Null)); + assert_eq!("true", format_document(Document::Bool(true))); + assert_eq!("false", format_document(Document::Bool(false))); + assert_eq!("5", format_document(Document::Number(Number::PosInt(5)))); + assert_eq!("\"test\"", format_document(Document::String("test".into()))); + assert_eq!( + "[null,true,\"test\"]", + format_document(Document::Array(vec![ + Document::Null, + Document::Bool(true), + Document::String("test".into()) + ])) + ); + assert_eq!( + r#"{"test":"foo"}"#, + format_document(Document::Object( + vec![("test".to_string(), Document::String("foo".into()))] + .into_iter() + .collect() + )) + ); + assert_eq!( + r#"{"test1":[{"num":1},{"num":2}]}"#, + format_document(Document::Object( + vec![( + "test1".to_string(), + Document::Array(vec![ + Document::Object( + vec![("num".to_string(), Document::Number(Number::PosInt(1))),] + .into_iter() + .collect() + ), + Document::Object( + vec![("num".to_string(), Document::Number(Number::PosInt(2))),] + .into_iter() + .collect() + ), + ]) + ),] + .into_iter() + .collect() + )) + ); + } + fn format_test_number(number: Number) -> String { let mut formatted = String::new(); - append_number(&mut formatted, number); + JsonValueWriter::new(&mut formatted).number(number); formatted } #[test] fn number_formatting() { - let format = |n: Number| { - let mut buffer = String::new(); - append_number(&mut buffer, n); - buffer - }; - - assert_eq!("1", format(Number::PosInt(1))); - assert_eq!("-1", format(Number::NegInt(-1))); - assert_eq!("1", format(Number::NegInt(1))); - assert_eq!("0.0", format(Number::Float(0.0))); - assert_eq!("10000000000.0", format(Number::Float(1e10))); - assert_eq!("-1.2", format(Number::Float(-1.2))); + assert_eq!("1", format_test_number(Number::PosInt(1))); + assert_eq!("-1", format_test_number(Number::NegInt(-1))); + assert_eq!("1", format_test_number(Number::NegInt(1))); + assert_eq!("0.0", format_test_number(Number::Float(0.0))); + assert_eq!("10000000000.0", format_test_number(Number::Float(1e10))); + assert_eq!("-1.2", format_test_number(Number::Float(-1.2))); // JSON doesn't support NaN, Infinity, or -Infinity, so we're matching // the behavior of the serde_json crate in these cases.