diff --git a/.changelog/1763738215.md b/.changelog/1763738215.md new file mode 100644 index 00000000000..8aed807df6a --- /dev/null +++ b/.changelog/1763738215.md @@ -0,0 +1,13 @@ +--- +applies_to: +- client +- aws-sdk-rust +authors: +- AmitKulkarni23 +references: +- smithy-rs#312 +breaking: false +new_feature: true +bug_fix: false +--- +Add support for Smithy bigInteger and bigDecimal types as string wrappers in aws-smithy-types, allowing users to parse with their preferred big number library. diff --git a/codegen-client-test/build.gradle.kts b/codegen-client-test/build.gradle.kts index 30997b3b1b0..9f919b0cbdd 100644 --- a/codegen-client-test/build.gradle.kts +++ b/codegen-client-test/build.gradle.kts @@ -62,6 +62,7 @@ data class ClientTest( val allCodegenTests = listOf( ClientTest("com.amazonaws.simple#SimpleService", "simple", dependsOn = listOf("simple.smithy")), + ClientTest("com.amazonaws.bignumbers#BigNumberService", "big_numbers", dependsOn = listOf("big-numbers.smithy")), ClientTest("com.amazonaws.dynamodb#DynamoDB_20120810", "dynamo"), ClientTest("com.amazonaws.ebs#Ebs", "ebs", dependsOn = listOf("ebs.json")), ClientTest("aws.protocoltests.json10#JsonRpc10", "json_rpc10"), diff --git a/codegen-core/common-test-models/big-numbers.smithy b/codegen-core/common-test-models/big-numbers.smithy new file mode 100644 index 00000000000..8d6a527648b --- /dev/null +++ b/codegen-core/common-test-models/big-numbers.smithy @@ -0,0 +1,143 @@ +$version: "2.0" + +namespace com.amazonaws.bignumbers + +use aws.protocols#restJson1 +use smithy.test#httpRequestTests +use smithy.test#httpResponseTests + +@restJson1 +service BigNumberService { + version: "2023-01-01" + operations: [ProcessBigNumbers] +} + +@http(uri: "/process", method: "POST") +@httpRequestTests([ + { + id: "BigNumbersInJsonRequest", + protocol: restJson1, + method: "POST", + uri: "/process", + body: "{\"bigInt\":123456789,\"bigDec\":123.456789}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + bigInt: 123456789, + bigDec: 123.456789 + } + }, + { + id: "NegativeBigNumbersInJsonRequest", + protocol: restJson1, + method: "POST", + uri: "/process", + body: "{\"bigInt\":-987654321,\"bigDec\":-0.000000001}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + bigInt: -987654321, + bigDec: -0.000000001 + } + }, + { + id: "ZeroBigNumbersInJsonRequest", + protocol: restJson1, + method: "POST", + uri: "/process", + body: "{\"bigInt\":0,\"bigDec\":0.0}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + bigInt: 0, + bigDec: 0.0 + } + }, + { + id: "VeryLargeBigNumbersInJsonRequest", + protocol: restJson1, + method: "POST", + uri: "/process", + body: "{\"bigInt\":9007199254740991,\"bigDec\":123456.789}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + bigInt: 9007199254740991, + bigDec: 123456.789 + } + } +]) +@httpResponseTests([ + { + id: "BigNumbersInJsonResponse", + protocol: restJson1, + code: 200, + body: "{\"result\":999999999,\"ratio\":0.123456789}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + result: 999999999, + ratio: 0.123456789 + } + }, + { + id: "NegativeBigNumbersInJsonResponse", + protocol: restJson1, + code: 200, + body: "{\"result\":-123456789,\"ratio\":-999.999}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + result: -123456789, + ratio: -999.999 + } + }, + { + id: "VeryLargeBigNumbersInJsonResponse", + protocol: restJson1, + code: 200, + body: "{\"result\":9007199254740991,\"ratio\":123456.789}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + result: 9007199254740991, + ratio: 123456.789 + } + }, + { + id: "ZeroBigNumbersInJsonResponse", + protocol: restJson1, + code: 200, + body: "{\"result\":0,\"ratio\":0.0}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + result: 0, + ratio: 0.0 + } + }, + { + id: "NullBigNumbersInJsonResponse", + protocol: restJson1, + code: 200, + body: "{\"result\":null,\"ratio\":null}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: {} + } +]) + +operation ProcessBigNumbers { + input: BigNumberInput + output: BigNumberOutput +} + +structure BigNumberInput { + bigInt: BigInteger + bigDec: BigDecimal +} + +structure BigNumberOutput { + result: BigInteger + ratio: BigDecimal +} diff --git a/codegen-core/common-test-models/misc.smithy b/codegen-core/common-test-models/misc.smithy index 42329ba1d27..47d8e06abee 100644 --- a/codegen-core/common-test-models/misc.smithy +++ b/codegen-core/common-test-models/misc.smithy @@ -95,12 +95,11 @@ structure InnermostShape { @required aDouble: Double, - // TODO(https://github.com/smithy-lang/smithy-rs/issues/312) - // @required - // aBigInteger: BigInteger, + @required + aBigInteger: BigInteger, - // @required - // aBigDecimal: BigDecimal, + @required + aBigDecimal: BigDecimal, @required aTimestamp: Timestamp, diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt index a5e4283137a..127a54048cd 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt @@ -425,6 +425,10 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null) fun dateTime(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("DateTime") + fun bigInteger(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("BigInteger") + + fun bigDecimal(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("BigDecimal") + fun document(runtimeConfig: RuntimeConfig): RuntimeType = smithyTypes(runtimeConfig).resolve("Document") fun format(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("date_time::Format") diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt index d2c30f6091e..7dc41523293 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt @@ -245,11 +245,11 @@ open class SymbolVisitor( } override fun bigIntegerShape(shape: BigIntegerShape?): Symbol { - TODO("Not yet implemented: https://github.com/smithy-lang/smithy-rs/issues/312") + return RuntimeType.bigInteger(config.runtimeConfig).toSymbol() } override fun bigDecimalShape(shape: BigDecimalShape?): Symbol { - TODO("Not yet implemented: https://github.com/smithy-lang/smithy-rs/issues/312") + return RuntimeType.bigDecimal(config.runtimeConfig).toSymbol() } override fun operationShape(shape: OperationShape): Symbol { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt index c09bc545fc9..fe7438e4c54 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt @@ -14,6 +14,8 @@ import software.amazon.smithy.model.node.NullNode import software.amazon.smithy.model.node.NumberNode import software.amazon.smithy.model.node.ObjectNode import software.amazon.smithy.model.node.StringNode +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.CollectionShape @@ -544,6 +546,20 @@ class PrimitiveInstantiator( } is StringShape -> renderString(shape, data as StringNode)(this) + is BigIntegerShape -> { + val value = data.toString() + rustTemplate( + "<#{BigInteger} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()", + "BigInteger" to RuntimeType.bigInteger(runtimeConfig), + ) + } + is BigDecimalShape -> { + val value = data.toString() + rustTemplate( + "<#{BigDecimal} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()", + "BigDecimal" to RuntimeType.bigDecimal(runtimeConfig), + ) + } is NumberShape -> when (data) { is StringNode -> { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGenerator.kt index 58dbb9c77fa..45985aa7c15 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGenerator.kt @@ -5,7 +5,10 @@ package software.amazon.smithy.rust.codegen.core.smithy.protocols.parse +import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.ByteShape @@ -579,6 +582,18 @@ class CborParserGenerator( is TimestampShape -> rust("decoder.timestamp()") + // BigInteger/BigDecimal are not supported with CBOR. + // The Smithy RPC v2 CBOR spec requires these to be encoded using CBOR tags 2/3/4 + // (binary bignum representation), but aws-smithy-cbor doesn't implement these tags yet. + is BigIntegerShape -> + throw CodegenException( + "BigInteger is not supported with Concise Binary Object Representation (CBOR) protocol", + ) + is BigDecimalShape -> + throw CodegenException( + "BigDecimal is not supported with Concise Binary Object Representation (CBOR) protocol", + ) + // Aggregate shapes: https://smithy.io/2.0/spec/aggregate-types.html is StructureShape -> deserializeStruct(target) is CollectionShape -> deserializeCollection(target) diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/JsonParserGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/JsonParserGenerator.kt index 50569ccc776..d0bbb8ff139 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/JsonParserGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/JsonParserGenerator.kt @@ -6,6 +6,8 @@ package software.amazon.smithy.rust.codegen.core.smithy.protocols.parse import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.CollectionShape @@ -296,6 +298,8 @@ class JsonParserGenerator( when (val target = model.expectShape(memberShape.target)) { is StringShape -> deserializeString(target) is BooleanShape -> rustTemplate("#{expect_bool_or_null}(tokens.next())?", *codegenScope) + is BigIntegerShape -> deserializeBigInteger() + is BigDecimalShape -> deserializeBigDecimal() is NumberShape -> deserializeNumber(target) is BlobShape -> deserializeBlob(memberShape) is TimestampShape -> deserializeTimestamp(memberShape) @@ -374,6 +378,63 @@ class JsonParserGenerator( } } + private fun RustWriter.deserializeBigInteger() { + // Match on Number enum to: + // 1. Validate only integers are accepted (reject floats) + // 2. Extract inner value and convert to string + + rustTemplate( + """ + #{expect_number_or_null}(tokens.next())? + .map(|v| { + let s = match v { + #{Number}::PosInt(n) => n.to_string(), + #{Number}::NegInt(n) => n.to_string(), + #{Number}::Float(_) => return Err(#{Error}::custom("expected integer, found float")), + }; + Ok(<#{BigInteger} as ::std::str::FromStr>::from_str(&s).expect("infallible")) + }) + .transpose()? + """, + "BigInteger" to RuntimeType.bigInteger(codegenContext.runtimeConfig), + "Number" to RuntimeType.smithyTypes(codegenContext.runtimeConfig).resolve("Number"), + *codegenScope, + ) + } + + private fun RustWriter.deserializeBigDecimal() { + // Match on Number enum to extract inner value and convert to string + // (Number doesn't implement Display, so we must match each variant) + // For floats, preserve decimal notation that f64::to_string() drops for whole numbers + + rustTemplate( + """ + #{expect_number_or_null}(tokens.next())? + .map(|v| { + let s = match v { + #{Number}::PosInt(n) => n.to_string(), + #{Number}::NegInt(n) => n.to_string(), + #{Number}::Float(f) => { + // Use format! to avoid scientific notation and preserve precision + let s = format!("{f}"); + // f64 formatting drops ".0" for whole numbers (0.0 -> "0") + // Restore it to preserve that the original JSON had decimal notation + if !s.contains('.') && !s.contains('e') && !s.contains('E') { + format!("{s}.0") + } else { + s + } + }, + }; + <#{BigDecimal} as ::std::str::FromStr>::from_str(&s).expect("infallible") + }) + """, + "BigDecimal" to RuntimeType.bigDecimal(codegenContext.runtimeConfig), + "Number" to RuntimeType.smithyTypes(codegenContext.runtimeConfig).resolve("Number"), + *codegenScope, + ) + } + private fun RustWriter.deserializeTimestamp(member: MemberShape) { val timestampFormat = httpBindingResolver.timestampFormat( diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt index b69a9f64fd3..fa73eeb881d 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt @@ -10,6 +10,8 @@ import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.model.Model import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.knowledge.HttpBindingIndex +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.CollectionShape @@ -372,6 +374,12 @@ class XmlBindingTraitParserGenerator( conditionalBlock("Some(", ")", forceOptional || symbol.isOptional()) { conditionalBlock("Box::new(", ")", symbol.isRustBoxed()) { when (target) { + is BigIntegerShape, is BigDecimalShape -> { + parsePrimitiveInner(memberShape) { + rustTemplate("#{try_data}(&mut ${ctx.tag})?.as_ref()", *codegenScope) + } + } + is StringShape, is BooleanShape, is NumberShape, is TimestampShape, is BlobShape -> parsePrimitiveInner(memberShape) { rustTemplate("#{try_data}(&mut ${ctx.tag})?.as_ref()", *codegenScope) @@ -396,6 +404,7 @@ class XmlBindingTraitParserGenerator( } is UnionShape -> parseUnion(target, ctx) + else -> PANIC("Unhandled: $target") } // each internal `parseT` function writes an `Result` expression, unwrap those: @@ -672,6 +681,31 @@ class XmlBindingTraitParserGenerator( ) { when (val shape = model.expectShape(member.target)) { is StringShape -> parseStringInner(shape, provider) + + is BigIntegerShape -> { + rustBlock("") { + rust("Ok(") + rustTemplate( + "<#{BigInteger} as ::std::str::FromStr>::from_str(", + "BigInteger" to RuntimeType.bigInteger(runtimeConfig), + ) + provider() + rust(").expect(\"infallible\"))") + } + } + + is BigDecimalShape -> { + rustBlock("") { + rust("Ok(") + rustTemplate( + "<#{BigDecimal} as ::std::str::FromStr>::from_str(", + "BigDecimal" to RuntimeType.bigDecimal(runtimeConfig), + ) + provider() + rust(").expect(\"infallible\"))") + } + } + is NumberShape, is BooleanShape -> { rustBlock("") { withBlockTemplate( diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt index 15834438009..fcfd28e0606 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGenerator.kt @@ -5,6 +5,9 @@ package software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.ByteShape @@ -493,6 +496,18 @@ class CborSerializerGenerator( is TimestampShape -> rust("$encoder.timestamp(${value.asRef()});") + // BigInteger/BigDecimal are not supported with CBOR. + // The Smithy RPC v2 CBOR spec requires these to be encoded using CBOR tags 2/3/4 + // (binary bignum representation), but aws-smithy-cbor doesn't implement these tags yet. + is BigIntegerShape -> + throw CodegenException( + "BigInteger is not supported with Concise Binary Object Representation (CBOR) protocol", + ) + is BigDecimalShape -> + throw CodegenException( + "BigDecimal is not supported with Concise Binary Object Representation (CBOR) protocol", + ) + is DocumentShape -> UNREACHABLE("Smithy RPC v2 CBOR does not support `document` shapes") // Aggregate shapes: https://smithy.io/2.0/spec/aggregate-types.html diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt index c3ce6241e38..ea7ceb513a0 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/JsonSerializerGenerator.kt @@ -5,6 +5,8 @@ package software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.ByteShape @@ -422,6 +424,16 @@ class JsonSerializerGenerator( when (target) { is StringShape -> rust("$writer.string(${value.name}.as_str());") is BooleanShape -> rust("$writer.boolean(${value.asValue()});") + is BigIntegerShape -> + rustTemplate( + "$writer.write_raw_value(${value.name}.as_ref());", + *codegenScope, + ) + is BigDecimalShape -> + rustTemplate( + "$writer.write_raw_value(${value.name}.as_ref());", + *codegenScope, + ) is NumberShape -> { val numberType = when (target) { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt index ef9cfa52b6f..5cebd99addc 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/QuerySerializerGenerator.kt @@ -5,6 +5,8 @@ package software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.CollectionShape @@ -231,6 +233,7 @@ abstract class QuerySerializerGenerator(private val codegenContext: CodegenConte } } is BooleanShape -> rust("$writer.boolean(${value.asValue()});") + is BigIntegerShape, is BigDecimalShape -> rust("$writer.string(${value.asRef()}.as_ref());") is NumberShape -> { val numberType = when (symbolProvider.toSymbol(target).rustType()) { diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt index 2cfd7674a85..889767624cf 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGenerator.kt @@ -6,6 +6,8 @@ package software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.model.shapes.BigDecimalShape +import software.amazon.smithy.model.shapes.BigIntegerShape import software.amazon.smithy.model.shapes.BlobShape import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.CollectionShape @@ -340,6 +342,9 @@ class XmlBindingTraitSerializerGenerator( } rust("$dereferenced.as_str()") } + is BigIntegerShape, is BigDecimalShape -> { + rust("$input.as_ref()") + } is BooleanShape, is NumberShape -> { rust( "#T::from(${autoDeref(input)}).encode()", @@ -384,6 +389,8 @@ class XmlBindingTraitSerializerGenerator( is NumberShape, is TimestampShape, is BlobShape, + is BigIntegerShape, + is BigDecimalShape, -> { rust( "let mut inner_writer = ${ctx.scopeWriter}.start_el(${xmlName.dq()})$ns.finish();", diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitorTest.kt index 4821ecaf0c6..df9a0496248 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitorTest.kt @@ -276,4 +276,32 @@ class SymbolVisitorTest { symbol.definitionFile shouldBe "src/test_operation.rs" symbol.name shouldBe "PutObject" } + + @Test + fun `handles bigInteger shapes`() { + val model = + """ + namespace test + + bigInteger MyBigInt + """.asSmithyModel() + val provider = testSymbolProvider(model) + val sym = provider.toSymbol(model.expectShape(ShapeId.from("test#MyBigInt"))) + sym.rustType().render(false) shouldBe "BigInteger" + sym.namespace shouldBe "::aws_smithy_types" + } + + @Test + fun `handles bigDecimal shapes`() { + val model = + """ + namespace test + + bigDecimal MyBigDecimal + """.asSmithyModel() + val provider = testSymbolProvider(model) + val sym = provider.toSymbol(model.expectShape(ShapeId.from("test#MyBigDecimal"))) + sym.rustType().render(false) shouldBe "BigDecimal" + sym.namespace shouldBe "::aws_smithy_types" + } } diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGeneratorTest.kt new file mode 100644 index 00000000000..5931b4712d2 --- /dev/null +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGeneratorTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.core.smithy.protocols.parse + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.rust.codegen.core.rustlang.writable +import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpTraitHttpBindingResolver +import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolContentTypes +import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer +import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest +import software.amazon.smithy.rust.codegen.core.testutil.renderWithModelBuilder +import software.amazon.smithy.rust.codegen.core.testutil.testCodegenContext +import software.amazon.smithy.rust.codegen.core.testutil.unitTest +import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.core.util.outputShape + +class CborParserGeneratorTest { + private val modelWithBigInteger = + """ + namespace test + use smithy.protocols#rpcv2Cbor + + @rpcv2Cbor + service TestService { + version: "test", + operations: [TestOp] + } + + structure TestOutput { + bigInt: BigInteger + } + + @http(uri: "/test", method: "POST") + operation TestOp { + output: TestOutput + } + """.asSmithyModel() + + private val modelWithBigDecimal = + """ + namespace test + use smithy.protocols#rpcv2Cbor + + @rpcv2Cbor + service TestService { + version: "test", + operations: [TestOp] + } + + structure TestOutput { + bigDec: BigDecimal + } + + @http(uri: "/test", method: "POST") + operation TestOp { + output: TestOutput + } + """.asSmithyModel() + + @Test + fun `throws CodegenException when deserializing BigInteger with CBOR`() { + val model = OperationNormalizer.transform(modelWithBigInteger) + val codegenContext = testCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val parserGenerator = + CborParserGenerator( + codegenContext, + HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/cbor")), + handleNullForNonSparseCollection = { _ -> writable { } }, + ) + val operationParser = parserGenerator.operationParser(model.lookup("test#TestOp")) + + val project = TestWorkspace.testProject(symbolProvider) + + val exception = + assertThrows { + project.lib { + unitTest( + "cbor_parser", + """ + let bytes = &[]; + let _output = ${format(operationParser!!)}; + """, + ) + } + + model.lookup("test#TestOp").outputShape(model).also { output -> + output.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } + + assert(exception.message!!.contains("BigInteger is not supported with Concise Binary Object Representation (CBOR)")) + } + + @Test + fun `throws CodegenException when deserializing BigDecimal with CBOR`() { + val model = OperationNormalizer.transform(modelWithBigDecimal) + val codegenContext = testCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val parserGenerator = + CborParserGenerator( + codegenContext, + HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/cbor")), + handleNullForNonSparseCollection = { _ -> writable { } }, + ) + val operationParser = parserGenerator.operationParser(model.lookup("test#TestOp")) + + val project = TestWorkspace.testProject(symbolProvider) + + val exception = + assertThrows { + project.lib { + unitTest( + "cbor_parser", + """ + let bytes = &[]; + let _output = ${format(operationParser!!)}; + """, + ) + } + + model.lookup("test#TestOp").outputShape(model).also { output -> + output.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } + + assert(exception.message!!.contains("BigDecimal is not supported with Concise Binary Object Representation (CBOR)")) + } +} diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt index 9099ebb9cb1..8c609d412a2 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/XmlBindingTraitParserGeneratorTest.kt @@ -92,6 +92,23 @@ internal class XmlBindingTraitParserGeneratorTest { } """.asSmithyModel() + private val bigNumberModel = + """ + namespace test + use aws.protocols#restXml + + structure BigNumberData { + bigInt: BigInteger, + bigDec: BigDecimal, + } + + @http(uri: "/bignumber", method: "POST") + operation BigNumberOp { + input: BigNumberData, + output: BigNumberData + } + """.asSmithyModel() + @Test fun `generates valid parsers`() { val model = RecursiveShapeBoxer().transform(OperationNormalizer.transform(baseModel)) @@ -218,4 +235,40 @@ internal class XmlBindingTraitParserGeneratorTest { } project.compileAndTest() } + + @Test + fun `parses BigInteger and BigDecimal from XML`() { + val model = RecursiveShapeBoxer().transform(OperationNormalizer.transform(bigNumberModel)) + val codegenContext = testCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val parserGenerator = + XmlBindingTraitParserGenerator( + codegenContext, + RuntimeType.wrappedXmlErrors(TestRuntimeConfig), + ) { _, inner -> inner("decoder") } + val operationParser = parserGenerator.operationParser(model.lookup("test#BigNumberOp"))!! + + val project = TestWorkspace.testProject(testSymbolProvider(model)) + project.lib { + unitTest(name = "parse_big_numbers") { + rustTemplate( + """ + let xml = br##" + 12345678901234567890 + 3.141592653589793238 + + "##; + let output = ${format(operationParser)}(xml, test_output::BigNumberOpOutput::builder()).unwrap().build(); + assert_eq!(output.big_int.as_ref().map(|v| v.as_ref()), Some("12345678901234567890")); + assert_eq!(output.big_dec.as_ref().map(|v| v.as_ref()), Some("3.141592653589793238")); + """, + ) + } + } + + model.lookup("test#BigNumberOp").outputShape(model).also { out -> + out.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } } diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt index aaf483fb526..fadcebe0c72 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/AwsQuerySerializerGeneratorTest.kt @@ -34,6 +34,8 @@ class AwsQuerySerializerGeneratorTest { use aws.protocols#restJson1 union Choice { + bigInt: BigInteger, + bigDec: BigDecimal, blob: Blob, boolean: Boolean, date: Timestamp, @@ -163,6 +165,8 @@ class AwsQuerySerializerGeneratorTest { use aws.protocols#restJson1 union Choice { + bigInt: BigInteger, + bigDec: BigDecimal, blob: Blob, boolean: Boolean, date: Timestamp, @@ -325,4 +329,67 @@ class AwsQuerySerializerGeneratorTest { } project.compileAndTest() } + + @ParameterizedTest + @CsvSource("true", "false") + fun `serializes big numbers correctly`(generateUnknownVariant: Boolean) { + val model = + """ + namespace test + use aws.protocols#awsQuery + + @awsQuery + @xmlNamespace(uri: "https://example.com") + service TestService { + version: "test", + operations: [Op] + } + + structure OpInput { + bigInt: BigInteger, + bigDec: BigDecimal, + } + + @http(uri: "/", method: "POST") + operation Op { + input: OpInput, + } + """.asSmithyModel() + + val codegenTarget = + when (generateUnknownVariant) { + true -> CodegenTarget.CLIENT + false -> CodegenTarget.SERVER + } + val codegenContext = testCodegenContext(model, codegenTarget = codegenTarget) + val symbolProvider = codegenContext.symbolProvider + val serializerGenerator = AwsQuerySerializerGenerator(codegenContext) + val operationGenerator = serializerGenerator.operationInputSerializer(model.lookup("test#Op")) + + val project = TestWorkspace.testProject(symbolProvider) + project.lib { + unitTest( + "big_number_serializer", + """ + use aws_smithy_types::{BigInteger, BigDecimal}; + + let input = crate::test_model::OpInput::builder() + .big_int(BigInteger::from("12345678901234567890".to_string())) + .big_dec(BigDecimal::from("123.456".to_string())) + .build(); + let serialized = ${format(operationGenerator!!)}(&input).unwrap(); + let output = std::str::from_utf8(serialized.bytes().unwrap()).unwrap(); + assert_eq!( + output, + "Action=Op&Version=test&bigInt=12345678901234567890&bigDec=123.456" + ); + """, + ) + } + + model.lookup("test#Op").inputShape(model).also { input -> + input.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } } diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGeneratorTest.kt new file mode 100644 index 00000000000..5f4afca59a3 --- /dev/null +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/CborSerializerGeneratorTest.kt @@ -0,0 +1,136 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.core.smithy.protocols.serialize + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import software.amazon.smithy.codegen.core.CodegenException +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.rust.codegen.core.smithy.protocols.HttpTraitHttpBindingResolver +import software.amazon.smithy.rust.codegen.core.smithy.protocols.ProtocolContentTypes +import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer +import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest +import software.amazon.smithy.rust.codegen.core.testutil.renderWithModelBuilder +import software.amazon.smithy.rust.codegen.core.testutil.testCodegenContext +import software.amazon.smithy.rust.codegen.core.testutil.unitTest +import software.amazon.smithy.rust.codegen.core.util.inputShape +import software.amazon.smithy.rust.codegen.core.util.lookup + +class CborSerializerGeneratorTest { + private val modelWithBigInteger = + """ + namespace test + use smithy.protocols#rpcv2Cbor + + @rpcv2Cbor + service TestService { + version: "test", + operations: [TestOp] + } + + structure TestInput { + bigInt: BigInteger + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput + } + """.asSmithyModel() + + private val modelWithBigDecimal = + """ + namespace test + use smithy.protocols#rpcv2Cbor + + @rpcv2Cbor + service TestService { + version: "test", + operations: [TestOp] + } + + structure TestInput { + bigDec: BigDecimal + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput + } + """.asSmithyModel() + + @Test + fun `throws CodegenException when serializing BigInteger with CBOR`() { + val model = OperationNormalizer.transform(modelWithBigInteger) + val codegenContext = testCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val serializerGenerator = + CborSerializerGenerator( + codegenContext, + HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/cbor")), + ) + val operationGenerator = serializerGenerator.operationInputSerializer(model.lookup("test#TestOp")) + + val project = TestWorkspace.testProject(symbolProvider) + + val exception = + assertThrows { + project.lib { + unitTest( + "cbor_serializer", + """ + let input = crate::test_input::TestOpInput::builder().build(); + let _serialized = ${format(operationGenerator!!)}(&input); + """, + ) + } + + model.lookup("test#TestOp").inputShape(model).also { input -> + input.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } + + assert(exception.message!!.contains("BigInteger is not supported with Concise Binary Object Representation (CBOR)")) + } + + @Test + fun `throws CodegenException when serializing BigDecimal with CBOR`() { + val model = OperationNormalizer.transform(modelWithBigDecimal) + val codegenContext = testCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val serializerGenerator = + CborSerializerGenerator( + codegenContext, + HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/cbor")), + ) + val operationGenerator = serializerGenerator.operationInputSerializer(model.lookup("test#TestOp")) + + val project = TestWorkspace.testProject(symbolProvider) + + val exception = + assertThrows { + project.lib { + unitTest( + "cbor_serializer", + """ + let input = crate::test_input::TestOpInput::builder().build(); + let _serialized = ${format(operationGenerator!!)}(&input); + """, + ) + } + + model.lookup("test#TestOp").inputShape(model).also { input -> + input.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } + + assert(exception.message!!.contains("BigDecimal is not supported with Concise Binary Object Representation (CBOR)")) + } +} diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt index 5012daba669..606684c4fb0 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/Ec2QuerySerializerGeneratorTest.kt @@ -34,6 +34,8 @@ class Ec2QuerySerializerGeneratorTest { namespace test union Choice { + bigInt: BigInteger, + bigDec: BigDecimal, blob: Blob, boolean: Boolean, date: Timestamp, @@ -307,4 +309,62 @@ class Ec2QuerySerializerGeneratorTest { } project.compileAndTest() } + + @ParameterizedTest + @CsvSource("true", "false") + fun `serializes big numbers correctly`(generateUnknownVariant: Boolean) { + val model = + """ + namespace test + use aws.protocols#ec2Query + + @ec2Query + @xmlNamespace(uri: "https://example.com") + service TestService { + version: "test", + operations: [Op] + } + + structure OpInput { + bigInt: BigInteger, + bigDec: BigDecimal, + } + + @http(uri: "/", method: "POST") + operation Op { + input: OpInput, + } + """.asSmithyModel() + + val codegenContext = testCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val serializerGenerator = Ec2QuerySerializerGenerator(codegenContext) + val operationGenerator = serializerGenerator.operationInputSerializer(model.lookup("test#Op")) + + val project = TestWorkspace.testProject(symbolProvider) + project.lib { + unitTest( + "big_number_serializer", + """ + use aws_smithy_types::{BigInteger, BigDecimal}; + + let input = crate::test_model::OpInput::builder() + .big_int(BigInteger::from("12345678901234567890".to_string())) + .big_dec(BigDecimal::from("123.456".to_string())) + .build(); + let serialized = ${format(operationGenerator!!)}(&input).unwrap(); + let output = std::str::from_utf8(serialized.bytes().unwrap()).unwrap(); + assert_eq!( + output, + "Action=Op&Version=test&BigInt=12345678901234567890&BigDec=123.456" + ); + """, + ) + } + + model.lookup("test#Op").inputShape(model).also { input -> + input.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } } diff --git a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt index e612b72da2c..67ca18b407a 100644 --- a/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt +++ b/codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/serialize/XmlBindingTraitSerializerGeneratorTest.kt @@ -109,6 +109,27 @@ internal class XmlBindingTraitSerializerGeneratorTest { } """.asSmithyModel() + private val bigNumberModel = + """ + namespace test + use aws.protocols#restXml + + structure BigNumberData { + bigInt: BigInteger, + bigDec: BigDecimal, + } + + structure BigNumberInput { + @httpPayload + payload: BigNumberData + } + + @http(uri: "/bignumber", method: "POST") + operation BigNumberOp { + input: BigNumberInput, + } + """.asSmithyModel() + @ParameterizedTest @CsvSource( "CLIENT", @@ -342,4 +363,45 @@ internal class XmlBindingTraitSerializerGeneratorTest { } project.compileAndTest() } + + @org.junit.jupiter.api.Test + fun `serializes BigInteger and BigDecimal to XML`() { + val model = RecursiveShapeBoxer().transform(OperationNormalizer.transform(bigNumberModel)) + val codegenContext = testCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val serializerGenerator = + XmlBindingTraitSerializerGenerator( + codegenContext, + HttpTraitHttpBindingResolver(model, ProtocolContentTypes.consistent("application/xml")), + ) + val operationSerializer = serializerGenerator.payloadSerializer(model.lookup("test#BigNumberInput\$payload")) + + val project = TestWorkspace.testProject(testSymbolProvider(model)) + project.lib { + unitTest( + "serialize_big_numbers", + """ + use aws_smithy_types::{BigInteger, BigDecimal}; + let input = crate::test_input::BigNumberOpInput::builder().payload( + crate::test_model::BigNumberData::builder() + .big_int("12345678901234567890".parse().unwrap()) + .big_dec("3.141592653589793238".parse().unwrap()) + .build() + ).build().unwrap(); + let serialized = ${format(operationSerializer)}(&input.payload.unwrap()).unwrap(); + let output = std::str::from_utf8(&serialized).unwrap(); + assert!(output.contains("12345678901234567890")); + assert!(output.contains("3.141592653589793238")); + """, + ) + } + + model.lookup("test#BigNumberData").also { struct -> + struct.renderWithModelBuilder(model, symbolProvider, project) + } + model.lookup("test#BigNumberOp").inputShape(model).also { input -> + input.renderWithModelBuilder(model, symbolProvider, project) + } + project.compileAndTest() + } } diff --git a/codegen-server-test/integration-tests/Cargo.lock b/codegen-server-test/integration-tests/Cargo.lock index 215926c097a..941a412b3c4 100644 --- a/codegen-server-test/integration-tests/Cargo.lock +++ b/codegen-server-test/integration-tests/Cargo.lock @@ -179,7 +179,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.4" +version = "1.9.5" dependencies = [ "aws-smithy-async", "aws-smithy-http", diff --git a/rust-runtime/aws-smithy-json/src/serialize.rs b/rust-runtime/aws-smithy-json/src/serialize.rs index 744028efe43..516ba55f925 100644 --- a/rust-runtime/aws-smithy-json/src/serialize.rs +++ b/rust-runtime/aws-smithy-json/src/serialize.rs @@ -72,6 +72,12 @@ impl<'a> JsonValueWriter<'a> { self.output.push('"'); } + /// Writes a raw value without any quoting or escaping. + /// Used for BigInteger/BigDecimal which are stored as strings but serialize as JSON numbers. + pub fn write_raw_value(self, value: &str) { + self.output.push_str(value); + } + /// Writes a number `value`. pub fn number(self, value: Number) { match value { @@ -314,6 +320,16 @@ mod tests { ) } + #[test] + fn write_raw_value() { + let mut output = String::new(); + let mut object = JsonObjectWriter::new(&mut output); + object.key("big_int").write_raw_value("123456789"); + object.key("big_dec").write_raw_value("123.456"); + object.finish(); + assert_eq!(r#"{"big_int":123456789,"big_dec":123.456}"#, &output); + } + #[test] fn array_date_times() { let mut output = String::new(); diff --git a/rust-runtime/aws-smithy-types/src/big_number.rs b/rust-runtime/aws-smithy-types/src/big_number.rs new file mode 100644 index 00000000000..2aec757ab14 --- /dev/null +++ b/rust-runtime/aws-smithy-types/src/big_number.rs @@ -0,0 +1,111 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +//! Big number types represented as strings. +//! +//! These types are simple string wrappers that allow users to parse and format +//! big numbers using their preferred library. + +/// A BigInteger represented as a string. +/// +/// This type does not perform arithmetic operations. Users should parse the string +/// with their preferred big integer library. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct BigInteger(String); + +impl BigInteger {} + +impl Default for BigInteger { + fn default() -> Self { + Self("0".to_string()) + } +} + +impl std::str::FromStr for BigInteger { + // Infallible because any string is valid - we just store it without validation + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +impl From for BigInteger { + fn from(value: String) -> Self { + Self(value) + } +} + +impl AsRef for BigInteger { + fn as_ref(&self) -> &str { + &self.0 + } +} + +/// A big decimal represented as a string. +/// +/// This type does not perform arithmetic operations. Users should parse the string +/// with their preferred big decimal library. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct BigDecimal(String); + +impl BigDecimal {} + +impl Default for BigDecimal { + fn default() -> Self { + Self("0.0".to_string()) + } +} + +impl std::str::FromStr for BigDecimal { + // Infallible because any string is valid - we just store it without validation + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(Self(s.to_string())) + } +} + +impl From for BigDecimal { + fn from(value: String) -> Self { + Self(value) + } +} + +impl AsRef for BigDecimal { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn big_integer_basic() { + let bi = BigInteger::from_str("12345678901234567890").unwrap(); + assert_eq!(bi.as_ref(), "12345678901234567890"); + } + + #[test] + fn big_integer_default() { + let bi = BigInteger::default(); + assert_eq!(bi.as_ref(), "0"); + } + + #[test] + fn big_decimal_basic() { + let bd = BigDecimal::from_str("123.456789").unwrap(); + assert_eq!(bd.as_ref(), "123.456789"); + } + + #[test] + fn big_decimal_default() { + let bd = BigDecimal::default(); + assert_eq!(bd.as_ref(), "0.0"); + } +} diff --git a/rust-runtime/aws-smithy-types/src/lib.rs b/rust-runtime/aws-smithy-types/src/lib.rs index dd5b4e14ada..6f893b70a1e 100644 --- a/rust-runtime/aws-smithy-types/src/lib.rs +++ b/rust-runtime/aws-smithy-types/src/lib.rs @@ -18,6 +18,7 @@ )] pub mod base64; +pub mod big_number; pub mod body; pub mod byte_stream; pub mod checksum_config; @@ -39,6 +40,7 @@ mod document; mod number; pub mod str_bytes; +pub use big_number::{BigDecimal, BigInteger}; pub use blob::Blob; pub use date_time::DateTime; pub use document::Document;