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-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/BigNumberPrecisionTest.kt b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/BigNumberPrecisionTest.kt new file mode 100644 index 00000000000..d50855f5090 --- /dev/null +++ b/codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/BigNumberPrecisionTest.kt @@ -0,0 +1,244 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.client.smithy + +import org.junit.jupiter.api.Test +import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest +import software.amazon.smithy.rust.codegen.core.rustlang.rawRust +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.testutil.unitTest + +class BigNumberPrecisionTest { + @Test + fun `test BigInteger and BigDecimal round trip through serializers with restJson1`() { + val model = + """ + namespace test + use aws.protocols#restJson1 + + @restJson1 + service TestService { + version: "2026-01-01", + operations: [TestOp] + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + bigInt: BigInteger, + bigDec: BigDecimal + } + + structure TestOutput { + bigInt: BigInteger, + bigDec: BigDecimal + } + """.asSmithyModel() + + clientIntegrationTest(model) { _, rustCrate -> + rustCrate.unitTest("big_number_round_trip") { + rawRust( + """ + use aws_smithy_types::{BigInteger, BigDecimal}; + use std::str::FromStr; + + // Test values that exceed native type limits + let big_int_str = "99999999999999999999999999"; // > u64::MAX + let big_dec_precision_str = "3.141592653589793238462643383279502884197"; // > f64 precision (15-17 digits) + let big_dec_magnitude_str = "1.8e308"; // > f64::MAX - tokenizer uses NaN for validation + + // Test 1: High precision BigDecimal + let input = crate::operation::test_op::TestOpInput::builder() + .big_int(BigInteger::from_str(big_int_str).unwrap()) + .big_dec(BigDecimal::from_str(big_dec_precision_str).unwrap()) + .build() + .unwrap(); + + let json_body = crate::protocol_serde::shape_test_op::ser_test_op_input(&input).unwrap(); + let serialized = String::from_utf8(json_body.bytes().unwrap().to_vec()).unwrap(); + + assert!(serialized.contains(big_int_str)); + assert!(serialized.contains(big_dec_precision_str)); + + // Test 2: Large magnitude BigDecimal (> f64::MAX) + let mut json_response = String::from(r#"{"bigInt":"#); + json_response.push_str(big_int_str); + json_response.push_str(r#","bigDec":"#); + json_response.push_str(big_dec_magnitude_str); + json_response.push('}'); + + let headers = ::aws_smithy_runtime_api::http::Headers::new(); + let output = crate::protocol_serde::shape_test_op::de_test_op_http_response( + 200, + &headers, + json_response.as_bytes() + ).unwrap(); + + assert_eq!(output.big_int.unwrap().as_ref(), big_int_str); + assert_eq!(output.big_dec.unwrap().as_ref(), big_dec_magnitude_str); + """, + ) + } + } + } + + @Test + fun `test BigInteger and BigDecimal round trip through serializers with restXml`() { + val model = + """ + namespace test + use aws.protocols#restXml + + @restXml + service TestService { + version: "2026-01-01", + operations: [TestOp] + } + + @http(uri: "/test", method: "POST") + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + bigInt: BigInteger, + bigDec: BigDecimal + } + + structure TestOutput { + bigInt: BigInteger, + bigDec: BigDecimal + } + """.asSmithyModel() + + clientIntegrationTest(model) { _, rustCrate -> + rustCrate.unitTest("big_number_round_trip_xml") { + rawRust( + """ + use aws_smithy_types::{BigInteger, BigDecimal}; + use std::str::FromStr; + + // Test values that exceed native type limits + let big_int_str = "99999999999999999999999999"; // > u64::MAX + let big_dec_precision_str = "3.141592653589793238462643383279502884197"; // > f64 precision (15-17 digits) + let big_dec_magnitude_str = "1.8e308"; // > f64::MAX (~1.7976931348623157e308) + + // Test 1: High precision BigDecimal + let input = crate::operation::test_op::TestOpInput::builder() + .big_int(BigInteger::from_str(big_int_str).unwrap()) + .big_dec(BigDecimal::from_str(big_dec_precision_str).unwrap()) + .build() + .unwrap(); + + let xml_body = crate::protocol_serde::shape_test_op::ser_test_op_op_input(&input).unwrap(); + let serialized = String::from_utf8(xml_body.bytes().unwrap().to_vec()).unwrap(); + + assert!(serialized.contains(big_int_str)); + assert!(serialized.contains(big_dec_precision_str)); + + // Test 2: Large magnitude BigDecimal - construct XML manually + let mut xml_response = String::from(r#""#); + xml_response.push_str(big_int_str); + xml_response.push_str(r#""#); + xml_response.push_str(big_dec_magnitude_str); + xml_response.push_str(r#""#); + + let headers = ::aws_smithy_runtime_api::http::Headers::new(); + let output = crate::protocol_serde::shape_test_op::de_test_op_http_response( + 200, + &headers, + xml_response.as_bytes() + ).unwrap(); + + assert_eq!(output.big_int.unwrap().as_ref(), big_int_str); + assert_eq!(output.big_dec.unwrap().as_ref(), big_dec_magnitude_str); + """, + ) + } + } + } + + @Test + fun `test BigInteger and BigDecimal round trip through serializers with awsJson1_1`() { + val model = + """ + namespace test + use aws.protocols#awsJson1_1 + + @awsJson1_1 + service TestService { + version: "2023-01-01", + operations: [TestOp] + } + + operation TestOp { + input: TestInput, + output: TestOutput + } + + structure TestInput { + bigInt: BigInteger, + bigDec: BigDecimal + } + + structure TestOutput { + bigInt: BigInteger, + bigDec: BigDecimal + } + """.asSmithyModel() + + clientIntegrationTest(model) { _, rustCrate -> + rustCrate.unitTest("big_number_round_trip_aws_json") { + rawRust( + """ + use aws_smithy_types::{BigInteger, BigDecimal}; + use std::str::FromStr; + + // Test values that exceed native type limits + let big_int_str = "99999999999999999999999999"; // > u64::MAX + let big_dec_precision_str = "3.141592653589793238462643383279502884197"; // > f64 precision + let big_dec_magnitude_str = "1.8e308"; // > f64::MAX + + // Test 1: High precision BigDecimal + let input = crate::operation::test_op::TestOpInput::builder() + .big_int(BigInteger::from_str(big_int_str).unwrap()) + .big_dec(BigDecimal::from_str(big_dec_precision_str).unwrap()) + .build() + .unwrap(); + + let json_body = crate::protocol_serde::shape_test_op::ser_test_op_input(&input).unwrap(); + let serialized = String::from_utf8(json_body.bytes().unwrap().to_vec()).unwrap(); + + assert!(serialized.contains(big_int_str)); + assert!(serialized.contains(big_dec_precision_str)); + + // Test 2: Large magnitude BigDecimal + let mut json_response = String::from(r#"{"bigInt":"#); + json_response.push_str(big_int_str); + json_response.push_str(r#","bigDec":"#); + json_response.push_str(big_dec_magnitude_str); + json_response.push('}'); + + let headers = ::aws_smithy_runtime_api::http::Headers::new(); + let output = crate::protocol_serde::shape_test_op::de_test_op_http_response( + 200, + &headers, + json_response.as_bytes() + ).unwrap(); + + assert_eq!(output.big_int.unwrap().as_ref(), big_int_str); + assert_eq!(output.big_dec.unwrap().as_ref(), big_dec_magnitude_str); + """, + ) + } + } + } +} 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..e631f87061f --- /dev/null +++ b/codegen-core/common-test-models/big-numbers.smithy @@ -0,0 +1,287 @@ +$version: "2.0" + +namespace com.amazonaws.bignumbers + +use aws.protocols#restJson1 +use smithy.test#httpRequestTests +use smithy.test#httpResponseTests + +// Protocol tests for BigInteger and BigDecimal types. +// +// LIMITATION: Protocol test infrastructure has two precision constraints: +// 1. Smithy model parser converts numeric literals in `params` to Java Number (f64), losing precision +// 2. Protocol test validator uses serde_json which also truncates to f64 +// +// Therefore these tests use: +// - BigInteger: 18446744073709551616 (u64::MAX + 1) - tests arbitrary precision for integers +// - BigDecimal: Values within f64 range - cannot test true arbitrary decimal precision here +// +// For comprehensive arbitrary precision testing including decimals > f64::MAX and high-precision +// decimals, see BigNumberPrecisionTest.kt integration tests which test actual serialization/ +// deserialization without protocol test infrastructure limitations. + +@restJson1 +service BigNumberService { + version: "2023-01-01" + operations: [ProcessBigNumbers, ProcessNestedBigNumbers] +} + +@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\":18446744073709551616,\"bigDec\":123456.789}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + bigInt: 18446744073709551616, + bigDec: 123456.789 + } + }, + { + id: "ScientificNotationBigNumbersInJsonRequest", + protocol: restJson1, + method: "POST", + uri: "/process", + body: "{\"bigInt\":12300000000,\"bigDec\":4.56e-5}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + bigInt: 12300000000, + bigDec: 0.0000456 + } + }, + { + id: "UppercaseScientificNotationBigNumbersInJsonRequest", + protocol: restJson1, + method: "POST", + uri: "/process", + body: "{\"bigInt\":9870000000000000,\"bigDec\":3.21E-10}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + bigInt: 9870000000000000, + bigDec: 0.000000000321 + } + } +]) +@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\":18446744073709551616,\"ratio\":123456.789}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + result: 18446744073709551616, + 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: {} + }, + { + id: "ScientificNotationBigNumbersInJsonResponse", + protocol: restJson1, + code: 200, + body: "{\"result\":1500000000000,\"ratio\":2.5E-8}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + result: 1500000000000, + ratio: 0.000000025 + } + }, + { + id: "UppercaseScientificNotationBigNumbersInJsonResponse", + protocol: restJson1, + code: 200, + body: "{\"result\":789000000000000000000,\"ratio\":1.23E-15}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + result: 789000000000000000000, + ratio: 0.00000000000000123 + } + } +]) + +operation ProcessBigNumbers { + input: BigNumberInput + output: BigNumberOutput +} + +structure BigNumberInput { + bigInt: BigInteger + bigDec: BigDecimal +} + +structure BigNumberOutput { + result: BigInteger + ratio: BigDecimal +} + +// Collections and nested structures +list BigIntegerList { + member: BigInteger +} + +list BigDecimalList { + member: BigDecimal +} + +map StringToBigIntegerMap { + key: String + value: BigInteger +} + +map StringToBigDecimalMap { + key: String + value: BigDecimal +} + +structure NestedBigNumbers { + numbers: BigIntegerList + ratios: BigDecimalList + intMap: StringToBigIntegerMap + decMap: StringToBigDecimalMap +} + +@http(uri: "/nested", method: "POST") +@httpRequestTests([ + { + id: "BigNumbersInCollectionsRequest", + protocol: restJson1, + method: "POST", + uri: "/nested", + body: "{\"numbers\":[1,2,3],\"ratios\":[1.1,2.2,3.3],\"intMap\":{\"a\":100,\"b\":200},\"decMap\":{\"x\":0.5,\"y\":1.5}}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + numbers: [1, 2, 3], + ratios: [1.1, 2.2, 3.3], + intMap: {a: 100, b: 200}, + decMap: {x: 0.5, y: 1.5} + } + }, + { + id: "LargeBigNumbersInCollectionsRequest", + protocol: restJson1, + method: "POST", + uri: "/nested", + body: "{\"numbers\":[18446744073709551616],\"ratios\":[123456.789],\"intMap\":{\"big\":18446744073709551616},\"decMap\":{\"precise\":0.123456789}}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + numbers: [18446744073709551616], + ratios: [123456.789], + intMap: {big: 18446744073709551616}, + decMap: {precise: 0.123456789} + } + } +]) +@httpResponseTests([ + { + id: "BigNumbersInCollectionsResponse", + protocol: restJson1, + code: 200, + body: "{\"numbers\":[10,20,30],\"ratios\":[0.1,0.2,0.3],\"intMap\":{\"x\":1000},\"decMap\":{\"y\":99.99}}", + bodyMediaType: "application/json", + headers: {"Content-Type": "application/json"}, + params: { + numbers: [10, 20, 30], + ratios: [0.1, 0.2, 0.3], + intMap: {x: 1000}, + decMap: {y: 99.99} + } + } +]) +operation ProcessNestedBigNumbers { + input: NestedBigNumbers + output: NestedBigNumbers +} 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 cdc1c621a57..b65cf23f381 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 @@ -512,6 +512,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..fce372c69c1 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()}).expect(\"Invalid string for BigInteger\")", + "BigInteger" to RuntimeType.bigInteger(runtimeConfig), + ) + } + is BigDecimalShape -> { + val value = data.toString() + rustTemplate( + "<#{BigDecimal} as ::std::str::FromStr>::from_str(${value.dq()}).expect(\"invalid string for BigDecimal\")", + "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..b699030dccc 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,20 @@ 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. " + + "See https://github.com/smithy-lang/smithy-rs/issues/4473", + ) + is BigDecimalShape -> + throw CodegenException( + "BigDecimal is not supported with Concise Binary Object Representation (CBOR) protocol. " + + "See https://github.com/smithy-lang/smithy-rs/issues/4473", + ) + // 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..7295494c07c 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 @@ -118,6 +120,7 @@ class JsonParserGenerator( "expect_bool_or_null" to smithyJson.resolve("deserialize::token::expect_bool_or_null"), "expect_document" to smithyJson.resolve("deserialize::token::expect_document"), "expect_number_or_null" to smithyJson.resolve("deserialize::token::expect_number_or_null"), + "expect_number_as_string_or_null" to smithyJson.resolve("deserialize::token::expect_number_as_string_or_null"), "expect_start_array" to smithyJson.resolve("deserialize::token::expect_start_array"), "expect_start_object" to smithyJson.resolve("deserialize::token::expect_start_object"), "expect_string_or_null" to smithyJson.resolve("deserialize::token::expect_string_or_null"), @@ -146,13 +149,15 @@ class JsonParserGenerator( return protocolFunctions.deserializeFn(shape, fnNameSuffix) { fnName -> val unusedMut = if (includedMembers.isEmpty()) "##[allow(unused_mut)] " else "" rustBlockTemplate( - "pub(crate) fn $fnName(value: &[u8], ${unusedMut}mut builder: #{Builder}) -> #{Result}<#{Builder}, #{Error}>", + """ + pub(crate) fn $fnName(_value: &[u8], ${unusedMut}mut builder: #{Builder}) -> #{Result}<#{Builder}, #{Error}> + """, "Builder" to builderSymbol, *codegenScope, ) { rustTemplate( """ - let mut tokens_owned = #{json_token_iter}(#{or_empty}(value)).peekable(); + let mut tokens_owned = #{json_token_iter}(#{or_empty}(_value)).peekable(); let tokens = &mut tokens_owned; #{expect_start_object}(tokens.next())?; """, @@ -173,15 +178,15 @@ class JsonParserGenerator( } return protocolFunctions.deserializeFn(shape, fnNameSuffix = "payload") { fnName -> rustBlockTemplate( - "pub(crate) fn $fnName(input: &[u8]) -> #{Result}<#{ReturnType}, #{Error}>", + "pub(crate) fn $fnName(_value: &[u8]) -> #{Result}<#{ReturnType}, #{Error}>", *codegenScope, "ReturnType" to returnSymbolToParse.symbol, ) { val input = if (shape is DocumentShape) { - "input" + "_value" } else { - "#{or_empty}(input)" + "#{or_empty}(_value)" } rustTemplate( @@ -296,6 +301,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 +381,36 @@ class JsonParserGenerator( } } + private fun RustWriter.deserializeBigInteger() { + // Use expect_number_as_string_or_null to preserve arbitrary precision + // by extracting the raw JSON number string without converting to u64/i64/f64 + rustTemplate( + """ + #{expect_number_as_string_or_null}(tokens.next(), _value)? + .map(<#{BigInteger} as ::std::str::FromStr>::from_str) + .transpose() + .map_err(|e| #{Error}::custom(format!("invalid BigInteger: {e}")))? + """, + "BigInteger" to RuntimeType.bigInteger(codegenContext.runtimeConfig), + *codegenScope, + ) + } + + private fun RustWriter.deserializeBigDecimal() { + // Use expect_number_as_string_or_null to preserve arbitrary precision + // by extracting the raw JSON number string without converting to u64/i64/f64 + rustTemplate( + """ + #{expect_number_as_string_or_null}(tokens.next(), _value)? + .map(<#{BigDecimal} as ::std::str::FromStr>::from_str) + .transpose() + .map_err(|e| #{Error}::custom(format!("invalid BigDecimal: {e}")))? + """, + "BigDecimal" to RuntimeType.bigDecimal(codegenContext.runtimeConfig), + *codegenScope, + ) + } + private fun RustWriter.deserializeTimestamp(member: MemberShape) { val timestampFormat = httpBindingResolver.timestampFormat( @@ -397,7 +434,7 @@ class JsonParserGenerator( protocolFunctions.deserializeFn(shape) { fnName -> rustBlockTemplate( """ - pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}) -> #{Result}, #{Error}> + pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}, _value: &'a [u8]) -> #{Result}, #{Error}> where I: Iterator, #{Error}>> """, "ReturnType" to returnSymbol, @@ -448,7 +485,7 @@ class JsonParserGenerator( } } } - rust("#T(tokens)?", parser) + rust("#T(tokens, _value)?", parser) } private fun RustWriter.deserializeMap(shape: MapShape) { @@ -459,7 +496,7 @@ class JsonParserGenerator( protocolFunctions.deserializeFn(shape) { fnName -> rustBlockTemplate( """ - pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}) -> #{Result}, #{Error}> + pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}, _value: &'a [u8]) -> #{Result}, #{Error}> where I: Iterator, #{Error}>> """, "ReturnType" to returnSymbolToParse.symbol, @@ -506,7 +543,7 @@ class JsonParserGenerator( } } } - rust("#T(tokens)?", parser) + rust("#T(tokens, _value)?", parser) } private fun RustWriter.deserializeStruct(shape: StructureShape) { @@ -515,7 +552,7 @@ class JsonParserGenerator( protocolFunctions.deserializeFn(shape) { fnName -> rustBlockTemplate( """ - pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}) -> #{Result}, #{Error}> + pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}, _value: &'a [u8]) -> #{Result}, #{Error}> where I: Iterator, #{Error}>> """, "ReturnType" to returnSymbolToParse.symbol, @@ -541,7 +578,7 @@ class JsonParserGenerator( } } } - rust("#T(tokens)?", nestedParser) + rust("#T(tokens, _value)?", nestedParser) } private fun RustWriter.deserializeUnion(shape: UnionShape) { @@ -550,7 +587,7 @@ class JsonParserGenerator( protocolFunctions.deserializeFn(shape) { fnName -> rustBlockTemplate( """ - pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}) -> #{Result}, #{Error}> + pub(crate) fn $fnName<'a, I>(tokens: &mut #{Peekable}, _value: &'a [u8]) -> #{Result}, #{Error}> where I: Iterator, #{Error}>> """, *codegenScope, @@ -657,7 +694,7 @@ class JsonParserGenerator( rust("Ok(variant)") } } - rust("#T(tokens)?", nestedParser) + rust("#T(tokens, _value)?", nestedParser) } private fun RustWriter.unwrapOrDefaultOrError( 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..c6c52a36c96 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,37 @@ class XmlBindingTraitParserGenerator( ) { when (val shape = model.expectShape(member.target)) { is StringShape -> parseStringInner(shape, provider) + + is BigIntegerShape -> { + rustBlock("") { + rustTemplate( + "<#{BigInteger} as ::std::str::FromStr>::from_str(", + "BigInteger" to RuntimeType.bigInteger(runtimeConfig), + *codegenScope, + ) + provider() + rustTemplate( + ").map_err(|e| #{XmlDecodeError}::custom(format!(\"invalid BigInteger: {}\", e)))", + *codegenScope, + ) + } + } + + is BigDecimalShape -> { + rustBlock("") { + rustTemplate( + "<#{BigDecimal} as ::std::str::FromStr>::from_str(", + "BigDecimal" to RuntimeType.bigDecimal(runtimeConfig), + *codegenScope, + ) + provider() + rustTemplate( + ").map_err(|e| #{XmlDecodeError}::custom(format!(\"invalid BigDecimal: {}\", e)))", + *codegenScope, + ) + } + } + 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..772f1fef77c 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,54 @@ 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##" + 99999999999999999999999999 + 3.141592653589793238462643383279502884197 + + "##; + let output = ${format(operationParser)}(xml, test_output::BigNumberOpOutput::builder()).unwrap().build(); + assert_eq!(output.big_int.as_ref().map(|v| v.as_ref()), Some("99999999999999999999999999")); + assert_eq!(output.big_dec.as_ref().map(|v| v.as_ref()), Some("3.141592653589793238462643383279502884197")); + """, + ) + } + unitTest(name = "parse_big_numbers_exceeding_f64_max") { + rustTemplate( + """ + let xml = br##" + 99999999999999999999999999 + 1.8e308 + + "##; + let output = ${format(operationParser)}(xml, test_output::BigNumberOpOutput::builder()).unwrap().build(); + assert_eq!(output.big_int.as_ref().map(|v| v.as_ref()), Some("99999999999999999999999999")); + assert_eq!(output.big_dec.as_ref().map(|v| v.as_ref()), Some("1.8e308")); + """, + ) + } + } + + 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..6cd61cf4a43 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,68 @@ 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}; + use std::str::FromStr; + + let input = crate::test_model::OpInput::builder() + .big_int(BigInteger::from_str("12345678901234567890").unwrap()) + .big_dec(BigDecimal::from_str("123.456").unwrap()) + .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..3344121a006 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,63 @@ 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}; + use std::str::FromStr; + + let input = crate::test_model::OpInput::builder() + .big_int(BigInteger::from_str("12345678901234567890").unwrap()) + .big_dec(BigDecimal::from_str("123.456").unwrap()) + .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 3a7dfd6d4f1..046ff4be6ca 100644 --- a/codegen-server-test/integration-tests/Cargo.lock +++ b/codegen-server-test/integration-tests/Cargo.lock @@ -35,7 +35,7 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-smithy-async" -version = "1.2.7" +version = "1.2.8" dependencies = [ "futures-util", "pin-project-lite", @@ -52,7 +52,7 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.14" +version = "0.60.15" dependencies = [ "aws-smithy-types", "bytes", @@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.5" +version = "1.1.6" dependencies = [ "aws-smithy-async", "aws-smithy-protocol-test", @@ -135,7 +135,7 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.0" +version = "0.62.1" dependencies = [ "aws-smithy-types", ] @@ -162,7 +162,7 @@ dependencies = [ [[package]] name = "aws-smithy-legacy-http-server" -version = "0.65.10" +version = "0.65.11" dependencies = [ "aws-smithy-cbor", "aws-smithy-json", @@ -190,14 +190,14 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.2.0" +version = "0.2.1" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-protocol-test" -version = "0.63.7" +version = "0.63.8" dependencies = [ "assert-json-diff", "aws-smithy-runtime-api", @@ -214,7 +214,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.5" +version = "1.10.0" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -238,7 +238,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.9.3" +version = "1.11.0" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -252,7 +252,7 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.5" +version = "1.4.1" dependencies = [ "base64-simd", "bytes", diff --git a/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerRuntimeType.kt b/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerRuntimeType.kt index b7957763fb7..ee657339de0 100644 --- a/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerRuntimeType.kt +++ b/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerRuntimeType.kt @@ -25,4 +25,10 @@ object PythonServerRuntimeType { fun document(runtimeConfig: RuntimeConfig) = PythonServerCargoDependency.smithyHttpServerPython(runtimeConfig).toType().resolve("types::Document") + + fun bigInteger(runtimeConfig: RuntimeConfig) = + PythonServerCargoDependency.smithyHttpServerPython(runtimeConfig).toType().resolve("types::BigInteger") + + fun bigDecimal(runtimeConfig: RuntimeConfig) = + PythonServerCargoDependency.smithyHttpServerPython(runtimeConfig).toType().resolve("types::BigDecimal") } diff --git a/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerSymbolProvider.kt b/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerSymbolProvider.kt index 70e055e23f8..805a2159a13 100644 --- a/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerSymbolProvider.kt +++ b/codegen-server/codegen-server-python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerSymbolProvider.kt @@ -7,6 +7,8 @@ package software.amazon.smithy.rust.codegen.server.python.smithy import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.Model +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.DocumentShape import software.amazon.smithy.model.shapes.ListShape @@ -99,6 +101,14 @@ class PythonServerSymbolVisitor( override fun documentShape(shape: DocumentShape?): Symbol { return PythonServerRuntimeType.document(runtimeConfig).toSymbol() } + + override fun bigIntegerShape(shape: BigIntegerShape?): Symbol { + return PythonServerRuntimeType.bigInteger(runtimeConfig).toSymbol() + } + + override fun bigDecimalShape(shape: BigDecimalShape?): Symbol { + return PythonServerRuntimeType.bigDecimal(runtimeConfig).toSymbol() + } } /** diff --git a/rust-runtime/Cargo.lock b/rust-runtime/Cargo.lock index 061d68198ee..f2813e51666 100644 --- a/rust-runtime/Cargo.lock +++ b/rust-runtime/Cargo.lock @@ -303,7 +303,7 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.8" +version = "1.2.9" dependencies = [ "futures-util", "pin-project-lite", @@ -323,7 +323,7 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.64.0" +version = "0.64.1" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -346,7 +346,7 @@ dependencies = [ [[package]] name = "aws-smithy-compression" -version = "0.1.0" +version = "0.1.1" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "aws-smithy-dns" -version = "0.1.6" +version = "0.1.7" dependencies = [ "aws-smithy-runtime-api", "criterion", @@ -375,7 +375,7 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.15" +version = "0.60.16" dependencies = [ "arbitrary", "aws-smithy-types", @@ -394,7 +394,7 @@ version = "0.2.2" [[package]] name = "aws-smithy-http" -version = "0.63.0" +version = "0.63.1" dependencies = [ "async-stream", "aws-smithy-eventstream", @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.6" +version = "1.1.7" dependencies = [ "aws-smithy-async", "aws-smithy-protocol-test", @@ -492,7 +492,7 @@ dependencies = [ [[package]] name = "aws-smithy-http-server-python" -version = "0.66.7" +version = "0.66.8" dependencies = [ "aws-smithy-http", "aws-smithy-json", @@ -531,7 +531,7 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.0" +version = "0.62.1" dependencies = [ "aws-smithy-types", "proptest", @@ -540,7 +540,7 @@ dependencies = [ [[package]] name = "aws-smithy-legacy-http" -version = "0.62.7" +version = "0.62.8" dependencies = [ "async-stream", "aws-smithy-eventstream", @@ -607,7 +607,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability" -version = "0.2.1" +version = "0.2.2" dependencies = [ "aws-smithy-runtime-api", "serial_test", @@ -615,7 +615,7 @@ dependencies = [ [[package]] name = "aws-smithy-observability-otel" -version = "0.1.5" +version = "0.1.6" dependencies = [ "async-global-executor", "async-task", @@ -630,7 +630,7 @@ dependencies = [ [[package]] name = "aws-smithy-protocol-test" -version = "0.63.8" +version = "0.63.9" dependencies = [ "assert-json-diff", "aws-smithy-runtime-api", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "aws-smithy-query" -version = "0.60.10" +version = "0.60.11" dependencies = [ "aws-smithy-types", "urlencoding", @@ -685,7 +685,7 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.0" +version = "1.11.1" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -701,7 +701,7 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.0" +version = "1.4.1" dependencies = [ "base64 0.13.1", "base64-simd", diff --git a/rust-runtime/aws-smithy-async/Cargo.toml b/rust-runtime/aws-smithy-async/Cargo.toml index 7264f64e6e2..258cfcf1409 100644 --- a/rust-runtime/aws-smithy-async/Cargo.toml +++ b/rust-runtime/aws-smithy-async/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-async" -version = "1.2.8" +version = "1.2.9" authors = ["AWS Rust SDK Team ", "John DiSanti "] description = "Async runtime agnostic abstractions for smithy-rs." edition = "2021" diff --git a/rust-runtime/aws-smithy-checksums/Cargo.toml b/rust-runtime/aws-smithy-checksums/Cargo.toml index c9bb97d256c..4b0fa6a7578 100644 --- a/rust-runtime/aws-smithy-checksums/Cargo.toml +++ b/rust-runtime/aws-smithy-checksums/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-checksums" -version = "0.64.0" +version = "0.64.1" authors = [ "AWS Rust SDK Team ", "Zelda Hessler ", diff --git a/rust-runtime/aws-smithy-compression/Cargo.toml b/rust-runtime/aws-smithy-compression/Cargo.toml index 849304c1d79..d21c7dd5afe 100644 --- a/rust-runtime/aws-smithy-compression/Cargo.toml +++ b/rust-runtime/aws-smithy-compression/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-compression" -version = "0.1.0" +version = "0.1.1" authors = [ "AWS Rust SDK Team ", "Zelda Hessler ", diff --git a/rust-runtime/aws-smithy-dns/Cargo.toml b/rust-runtime/aws-smithy-dns/Cargo.toml index c79a8010253..104458f584c 100644 --- a/rust-runtime/aws-smithy-dns/Cargo.toml +++ b/rust-runtime/aws-smithy-dns/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-dns" -version = "0.1.6" +version = "0.1.7" authors = [ "AWS Rust SDK Team ", ] diff --git a/rust-runtime/aws-smithy-eventstream/Cargo.toml b/rust-runtime/aws-smithy-eventstream/Cargo.toml index 5d5ef342a77..58f6e7bbeb8 100644 --- a/rust-runtime/aws-smithy-eventstream/Cargo.toml +++ b/rust-runtime/aws-smithy-eventstream/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "aws-smithy-eventstream" # Only patch releases can be made to this runtime crate until https://github.com/smithy-lang/smithy-rs/issues/3370 is resolved -version = "0.60.15" +version = "0.60.16" # authors = ["AWS Rust SDK Team ", "John DiSanti "] description = "Event stream logic for smithy-rs." diff --git a/rust-runtime/aws-smithy-http-client/Cargo.toml b/rust-runtime/aws-smithy-http-client/Cargo.toml index 2f85b45f61a..0d966a0205a 100644 --- a/rust-runtime/aws-smithy-http-client/Cargo.toml +++ b/rust-runtime/aws-smithy-http-client/Cargo.toml @@ -2,7 +2,7 @@ name = "aws-smithy-http-client" authors = ["AWS Rust SDK Team "] description = "HTTP client abstractions for generated smithy clients" -version = "1.1.6" +version = "1.1.7" license = "Apache-2.0" edition = "2021" repository = "https://github.com/smithy-lang/smithy-rs" diff --git a/rust-runtime/aws-smithy-http-server-python/Cargo.toml b/rust-runtime/aws-smithy-http-server-python/Cargo.toml index a2f1a437c1a..7a834cdef46 100644 --- a/rust-runtime/aws-smithy-http-server-python/Cargo.toml +++ b/rust-runtime/aws-smithy-http-server-python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-http-server-python" -version = "0.66.7" +version = "0.66.8" authors = ["Smithy Rust Server "] edition = "2021" license = "Apache-2.0" diff --git a/rust-runtime/aws-smithy-http-server-python/src/types.rs b/rust-runtime/aws-smithy-http-server-python/src/types.rs index 8cfa5bb5090..9bcbc545d53 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/types.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/types.rs @@ -103,6 +103,84 @@ impl<'blob> From<&'blob Blob> for &'blob aws_smithy_types::Blob { } } +/// Python Wrapper for [aws_smithy_types::BigInteger]. +#[pyclass] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct BigInteger(aws_smithy_types::BigInteger); + +#[pymethods] +impl BigInteger { + #[new] + pub fn pynew(value: String) -> PyResult { + value + .parse() + .map(Self) + .map_err(|e| PyTypeError::new_err(format!("{e}"))) + } + + #[getter(value)] + pub fn get_value(&self) -> &str { + self.0.as_ref() + } +} + +impl AsRef for BigInteger { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl From for BigInteger { + fn from(other: aws_smithy_types::BigInteger) -> BigInteger { + BigInteger(other) + } +} + +impl From for aws_smithy_types::BigInteger { + fn from(other: BigInteger) -> aws_smithy_types::BigInteger { + other.0 + } +} + +/// Python Wrapper for [aws_smithy_types::BigDecimal]. +#[pyclass] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct BigDecimal(aws_smithy_types::BigDecimal); + +#[pymethods] +impl BigDecimal { + #[new] + pub fn pynew(value: String) -> PyResult { + value + .parse() + .map(Self) + .map_err(|e| PyTypeError::new_err(format!("{e}"))) + } + + #[getter(value)] + pub fn get_value(&self) -> &str { + self.0.as_ref() + } +} + +impl AsRef for BigDecimal { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +impl From for BigDecimal { + fn from(other: aws_smithy_types::BigDecimal) -> BigDecimal { + BigDecimal(other) + } +} + +impl From for aws_smithy_types::BigDecimal { + fn from(other: BigDecimal) -> aws_smithy_types::BigDecimal { + other.0 + } +} + /// Python Wrapper for [aws_smithy_types::date_time::DateTime]. #[pyclass] #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/rust-runtime/aws-smithy-http/Cargo.toml b/rust-runtime/aws-smithy-http/Cargo.toml index c14e391ef61..64238a4377b 100644 --- a/rust-runtime/aws-smithy-http/Cargo.toml +++ b/rust-runtime/aws-smithy-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-http" -version = "0.63.0" +version = "0.63.1" authors = [ "AWS Rust SDK Team ", "Russell Cohen ", diff --git a/rust-runtime/aws-smithy-json/Cargo.toml b/rust-runtime/aws-smithy-json/Cargo.toml index a56686f792c..ef403bd6df2 100644 --- a/rust-runtime/aws-smithy-json/Cargo.toml +++ b/rust-runtime/aws-smithy-json/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-json" -version = "0.62.0" +version = "0.62.1" authors = ["AWS Rust SDK Team ", "John DiSanti "] description = "Token streaming JSON parser for smithy-rs." edition = "2021" diff --git a/rust-runtime/aws-smithy-json/src/deserialize.rs b/rust-runtime/aws-smithy-json/src/deserialize.rs index 4459767716f..fb870486e25 100644 --- a/rust-runtime/aws-smithy-json/src/deserialize.rs +++ b/rust-runtime/aws-smithy-json/src/deserialize.rs @@ -323,11 +323,7 @@ impl<'a> JsonTokenIterator<'a> { offset, value: if floating { Number::Float( - f64::from_str(number_str) - .map_err(|_| self.error_at(start, InvalidNumber)) - .and_then(|f| { - must_be_finite(f).map_err(|_| self.error_at(start, InvalidNumber)) - })?, + f64::from_str(number_str).map_err(|_| self.error_at(start, InvalidNumber))?, ) } else if negative { // If the negative value overflows, then stuff it into an f64 @@ -506,14 +502,6 @@ impl<'a> Iterator for JsonTokenIterator<'a> { } } -fn must_be_finite(f: f64) -> Result { - if f.is_finite() { - Ok(f) - } else { - Err(()) - } -} - fn must_not_be_finite(f: f64) -> Result { if !f.is_finite() { Ok(f) @@ -672,6 +660,41 @@ mod tests { ); } + #[test] + fn out_of_range_floats_produce_infinity() { + // Values exceeding f64::MAX should tokenize as infinity + // The consumer layer (token.rs) will convert to NaN for BigInteger/BigDecimal + let expect_infinity = |input, should_be_positive| { + let token = json_token_iter(input).next().unwrap().unwrap(); + if let Token::ValueNumber { + value: Number::Float(f), + .. + } = token + { + assert!( + f.is_infinite(), + "Expected infinity for out-of-range value, got {}", + f + ); + if should_be_positive { + assert!(f.is_sign_positive(), "Expected positive infinity"); + } else { + assert!(f.is_sign_negative(), "Expected negative infinity"); + } + } else { + panic!("Expected Float token, got {:?}", token); + } + }; + + // Values > f64::MAX + expect_infinity(b"1.8e308", true); + expect_infinity(b"9.9e999", true); + + // Negative values < -f64::MAX + expect_infinity(b"-1.8e308", false); + expect_infinity(b"-9.9e999", false); + } + // These cases actually shouldn't parse according to the spec, but it's easier // to be lenient on these, and it doesn't really impact the SDK use-case. #[test] diff --git a/rust-runtime/aws-smithy-json/src/deserialize/token.rs b/rust-runtime/aws-smithy-json/src/deserialize/token.rs index 445d3d8e1a8..c513d39f134 100644 --- a/rust-runtime/aws-smithy-json/src/deserialize/token.rs +++ b/rust-runtime/aws-smithy-json/src/deserialize/token.rs @@ -163,7 +163,15 @@ pub fn expect_number_or_null( ) -> Result, Error> { match token.transpose()? { Some(Token::ValueNull { .. }) => Ok(None), - Some(Token::ValueNumber { value, .. }) => Ok(Some(value)), + Some(Token::ValueNumber { value, offset, .. }) => { + // Validate finite numbers - error on infinity/NaN + match value { + Number::Float(f) if !f.is_finite() => { + Err(Error::custom("number must be finite").with_offset(offset.0)) + } + _ => Ok(Some(value)), + } + } Some(Token::ValueString { value, offset }) => match value.to_unescaped() { Err(err) => Err(Error::custom_source( "expected a valid string, escape was invalid", err).with_offset(offset.0)), Ok(v) => f64::parse_smithy_primitive(v.as_ref()) @@ -616,6 +624,10 @@ pub mod test { panic!("expected nan, found: {not_ok:?}") } } + + // Test that infinity in ValueNumber token returns an error + let result = expect_number_or_null(value_number(0, Number::Float(f64::INFINITY))); + assert!(result.is_err(), "Expected error for infinity token"); } #[test] @@ -819,6 +831,18 @@ pub mod test { let mut iter = json_token_iter(input); let result = expect_number_as_string_or_null(iter.next(), input).unwrap(); assert_eq!(result, Some("0")); + + // Test lowercase e in scientific notation is preserved + let input = b"2.5e-8"; + let mut iter = json_token_iter(input); + let result = expect_number_as_string_or_null(iter.next(), input).unwrap(); + assert_eq!(result, Some("2.5e-8")); + + // Test uppercase E in scientific notation is preserved + let input = b"2.5E-8"; + let mut iter = json_token_iter(input); + let result = expect_number_as_string_or_null(iter.next(), input).unwrap(); + assert_eq!(result, Some("2.5E-8")); } #[test] 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-legacy-http/Cargo.toml b/rust-runtime/aws-smithy-legacy-http/Cargo.toml index fd4667b9639..0b9bf84bbd7 100644 --- a/rust-runtime/aws-smithy-legacy-http/Cargo.toml +++ b/rust-runtime/aws-smithy-legacy-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-legacy-http" -version = "0.62.7" +version = "0.62.8" authors = [ "AWS Rust SDK Team ", "Russell Cohen ", diff --git a/rust-runtime/aws-smithy-observability-otel/Cargo.toml b/rust-runtime/aws-smithy-observability-otel/Cargo.toml index f1ca5201518..b2365c5d835 100644 --- a/rust-runtime/aws-smithy-observability-otel/Cargo.toml +++ b/rust-runtime/aws-smithy-observability-otel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-observability-otel" -version = "0.1.5" +version = "0.1.6" authors = [ "AWS Rust SDK Team ", ] diff --git a/rust-runtime/aws-smithy-observability/Cargo.toml b/rust-runtime/aws-smithy-observability/Cargo.toml index f3fcc18c69a..0c679997a8e 100644 --- a/rust-runtime/aws-smithy-observability/Cargo.toml +++ b/rust-runtime/aws-smithy-observability/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-observability" -version = "0.2.1" +version = "0.2.2" authors = [ "AWS Rust SDK Team ", ] diff --git a/rust-runtime/aws-smithy-protocol-test/Cargo.toml b/rust-runtime/aws-smithy-protocol-test/Cargo.toml index 9a695e71fcd..4ad83b18311 100644 --- a/rust-runtime/aws-smithy-protocol-test/Cargo.toml +++ b/rust-runtime/aws-smithy-protocol-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-protocol-test" -version = "0.63.8" +version = "0.63.9" authors = ["AWS Rust SDK Team ", "Russell Cohen "] description = "A collection of library functions to validate HTTP requests against Smithy protocol tests." edition = "2021" diff --git a/rust-runtime/aws-smithy-query/Cargo.toml b/rust-runtime/aws-smithy-query/Cargo.toml index bb42bbc193e..97058a6dac3 100644 --- a/rust-runtime/aws-smithy-query/Cargo.toml +++ b/rust-runtime/aws-smithy-query/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-query" -version = "0.60.10" +version = "0.60.11" authors = ["AWS Rust SDK Team ", "John DiSanti "] description = "AWSQuery and EC2Query Smithy protocol logic for smithy-rs." edition = "2021" diff --git a/rust-runtime/aws-smithy-runtime-api/Cargo.toml b/rust-runtime/aws-smithy-runtime-api/Cargo.toml index c140af044cb..2526c6c3593 100644 --- a/rust-runtime/aws-smithy-runtime-api/Cargo.toml +++ b/rust-runtime/aws-smithy-runtime-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-runtime-api" -version = "1.11.0" +version = "1.11.1" authors = ["AWS Rust SDK Team ", "Zelda Hessler "] description = "Smithy runtime types." edition = "2021" diff --git a/rust-runtime/aws-smithy-types/Cargo.toml b/rust-runtime/aws-smithy-types/Cargo.toml index b0324ebd7f7..df624be18d0 100644 --- a/rust-runtime/aws-smithy-types/Cargo.toml +++ b/rust-runtime/aws-smithy-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aws-smithy-types" -version = "1.4.0" +version = "1.4.1" authors = [ "AWS Rust SDK Team ", "Russell Cohen ", 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..b60f5d0eab2 --- /dev/null +++ b/rust-runtime/aws-smithy-types/src/big_number.rs @@ -0,0 +1,214 @@ +/* + * 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. + +/// Error type for BigInteger and BigDecimal parsing. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum BigNumberError { + /// The input string is not a valid number format. + InvalidFormat(String), +} + +impl std::fmt::Display for BigNumberError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BigNumberError::InvalidFormat(s) => write!(f, "invalid number format: {s}"), + } + } +} + +impl std::error::Error for BigNumberError {} + +/// Validates that a string is a valid BigInteger format. +/// Only allows digits and an optional leading sign. +fn is_valid_big_integer(s: &str) -> bool { + if s.is_empty() { + return false; + } + + let mut chars = s.chars(); + + // Check first character (can be sign or digit) + match chars.next() { + Some('-') | Some('+') | Some('0'..='9') => {} + _ => return false, + } + + // Rest must be digits only + chars.all(|c| c.is_ascii_digit()) +} + +/// Validates that a string is a valid BigDecimal format. +/// Allows digits, sign, decimal point, and scientific notation. +fn is_valid_big_decimal(s: &str) -> bool { + if s.is_empty() { + return false; + } + + s.chars() + .all(|c| matches!(c, '0'..='9' | '-' | '+' | '.' | 'e' | 'E')) +} + +/// 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 Default for BigInteger { + fn default() -> Self { + Self("0".to_string()) + } +} + +impl std::str::FromStr for BigInteger { + type Err = BigNumberError; + + fn from_str(s: &str) -> Result { + if !is_valid_big_integer(s) { + return Err(BigNumberError::InvalidFormat(s.to_string())); + } + Ok(Self(s.to_string())) + } +} + +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 Default for BigDecimal { + fn default() -> Self { + Self("0.0".to_string()) + } +} + +impl std::str::FromStr for BigDecimal { + type Err = BigNumberError; + + fn from_str(s: &str) -> Result { + if !is_valid_big_decimal(s) { + return Err(BigNumberError::InvalidFormat(s.to_string())); + } + Ok(Self(s.to_string())) + } +} + +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"); + } + + #[test] + fn big_integer_negative() { + let bi = BigInteger::from_str("-12345").unwrap(); + assert_eq!(bi.as_ref(), "-12345"); + } + + #[test] + fn big_decimal_scientific() { + let bd = BigDecimal::from_str("1.23e10").unwrap(); + assert_eq!(bd.as_ref(), "1.23e10"); + + let bd = BigDecimal::from_str("1.23E-10").unwrap(); + assert_eq!(bd.as_ref(), "1.23E-10"); + } + + #[test] + fn big_integer_rejects_json_injection() { + // Reject strings with JSON special characters + assert!(BigInteger::from_str("123, \"injected\": true").is_err()); + assert!(BigInteger::from_str("123}").is_err()); + assert!(BigInteger::from_str("{\"hacked\": 1}").is_err()); + assert!(BigInteger::from_str("123\"").is_err()); + assert!(BigInteger::from_str("123\\n456").is_err()); + } + + #[test] + fn big_decimal_rejects_json_injection() { + assert!(BigDecimal::from_str("123.45, \"injected\": true").is_err()); + assert!(BigDecimal::from_str("123.45}").is_err()); + assert!(BigDecimal::from_str("{\"hacked\": 1.0}").is_err()); + } + + #[test] + fn big_integer_rejects_invalid_chars() { + assert!(BigInteger::from_str("abc").is_err()); + assert!(BigInteger::from_str("123abc").is_err()); + assert!(BigInteger::from_str("12 34").is_err()); + assert!(BigInteger::from_str("").is_err()); + } + + #[test] + fn big_integer_rejects_decimal_and_scientific() { + // BigInteger should reject decimal points + assert!(BigInteger::from_str("123.45").is_err()); + assert!(BigInteger::from_str("123.0").is_err()); + + // BigInteger should reject scientific notation + assert!(BigInteger::from_str("1e10").is_err()); + assert!(BigInteger::from_str("1E10").is_err()); + assert!(BigInteger::from_str("1.23e10").is_err()); + } + + #[test] + fn big_integer_accepts_signs() { + assert!(BigInteger::from_str("+123").is_ok()); + assert!(BigInteger::from_str("-123").is_ok()); + assert_eq!(BigInteger::from_str("+123").unwrap().as_ref(), "+123"); + } + + #[test] + fn big_decimal_rejects_invalid_chars() { + assert!(BigDecimal::from_str("abc").is_err()); + assert!(BigDecimal::from_str("123.45abc").is_err()); + assert!(BigDecimal::from_str("12.34 56").is_err()); + assert!(BigDecimal::from_str("").is_err()); + } +} 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;