Skip to content

Commit 0f0fecf

Browse files
author
Amit Kulkarni
committed
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.
1 parent 2f93501 commit 0f0fecf

File tree

25 files changed

+1005
-8
lines changed

25 files changed

+1005
-8
lines changed

.changelog/1763738215.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
applies_to:
3+
- client
4+
- aws-sdk-rust
5+
authors:
6+
- AmitKulkarni23
7+
references:
8+
- smithy-rs#312
9+
breaking: false
10+
new_feature: true
11+
bug_fix: false
12+
---
13+
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.

codegen-client-test/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ data class ClientTest(
6262

6363
val allCodegenTests = listOf(
6464
ClientTest("com.amazonaws.simple#SimpleService", "simple", dependsOn = listOf("simple.smithy")),
65+
ClientTest("com.amazonaws.bignumbers#BigNumberService", "big_numbers", dependsOn = listOf("big-numbers.smithy")),
6566
ClientTest("com.amazonaws.dynamodb#DynamoDB_20120810", "dynamo"),
6667
ClientTest("com.amazonaws.ebs#Ebs", "ebs", dependsOn = listOf("ebs.json")),
6768
ClientTest("aws.protocoltests.json10#JsonRpc10", "json_rpc10"),
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
$version: "2.0"
2+
3+
namespace com.amazonaws.bignumbers
4+
5+
use aws.protocols#restJson1
6+
use smithy.test#httpRequestTests
7+
use smithy.test#httpResponseTests
8+
9+
@restJson1
10+
service BigNumberService {
11+
version: "2023-01-01"
12+
operations: [ProcessBigNumbers]
13+
}
14+
15+
@http(uri: "/process", method: "POST")
16+
@httpRequestTests([
17+
{
18+
id: "BigNumbersInJsonRequest",
19+
protocol: restJson1,
20+
method: "POST",
21+
uri: "/process",
22+
body: "{\"bigInt\":123456789,\"bigDec\":123.456789}",
23+
bodyMediaType: "application/json",
24+
headers: {"Content-Type": "application/json"},
25+
params: {
26+
bigInt: 123456789,
27+
bigDec: 123.456789
28+
}
29+
},
30+
{
31+
id: "NegativeBigNumbersInJsonRequest",
32+
protocol: restJson1,
33+
method: "POST",
34+
uri: "/process",
35+
body: "{\"bigInt\":-987654321,\"bigDec\":-0.000000001}",
36+
bodyMediaType: "application/json",
37+
headers: {"Content-Type": "application/json"},
38+
params: {
39+
bigInt: -987654321,
40+
bigDec: -0.000000001
41+
}
42+
},
43+
{
44+
id: "ZeroBigNumbersInJsonRequest",
45+
protocol: restJson1,
46+
method: "POST",
47+
uri: "/process",
48+
body: "{\"bigInt\":0,\"bigDec\":0.0}",
49+
bodyMediaType: "application/json",
50+
headers: {"Content-Type": "application/json"},
51+
params: {
52+
bigInt: 0,
53+
bigDec: 0.0
54+
}
55+
},
56+
{
57+
id: "VeryLargeBigNumbersInJsonRequest",
58+
protocol: restJson1,
59+
method: "POST",
60+
uri: "/process",
61+
body: "{\"bigInt\":9007199254740991,\"bigDec\":123456.789}",
62+
bodyMediaType: "application/json",
63+
headers: {"Content-Type": "application/json"},
64+
params: {
65+
bigInt: 9007199254740991,
66+
bigDec: 123456.789
67+
}
68+
}
69+
])
70+
@httpResponseTests([
71+
{
72+
id: "BigNumbersInJsonResponse",
73+
protocol: restJson1,
74+
code: 200,
75+
body: "{\"result\":999999999,\"ratio\":0.123456789}",
76+
bodyMediaType: "application/json",
77+
headers: {"Content-Type": "application/json"},
78+
params: {
79+
result: 999999999,
80+
ratio: 0.123456789
81+
}
82+
},
83+
{
84+
id: "NegativeBigNumbersInJsonResponse",
85+
protocol: restJson1,
86+
code: 200,
87+
body: "{\"result\":-123456789,\"ratio\":-999.999}",
88+
bodyMediaType: "application/json",
89+
headers: {"Content-Type": "application/json"},
90+
params: {
91+
result: -123456789,
92+
ratio: -999.999
93+
}
94+
},
95+
{
96+
id: "VeryLargeBigNumbersInJsonResponse",
97+
protocol: restJson1,
98+
code: 200,
99+
body: "{\"result\":9007199254740991,\"ratio\":123456.789}",
100+
bodyMediaType: "application/json",
101+
headers: {"Content-Type": "application/json"},
102+
params: {
103+
result: 9007199254740991,
104+
ratio: 123456.789
105+
}
106+
},
107+
{
108+
id: "ZeroBigNumbersInJsonResponse",
109+
protocol: restJson1,
110+
code: 200,
111+
body: "{\"result\":0,\"ratio\":0.0}",
112+
bodyMediaType: "application/json",
113+
headers: {"Content-Type": "application/json"},
114+
params: {
115+
result: 0,
116+
ratio: 0.0
117+
}
118+
},
119+
{
120+
id: "NullBigNumbersInJsonResponse",
121+
protocol: restJson1,
122+
code: 200,
123+
body: "{\"result\":null,\"ratio\":null}",
124+
bodyMediaType: "application/json",
125+
headers: {"Content-Type": "application/json"},
126+
params: {}
127+
}
128+
])
129+
130+
operation ProcessBigNumbers {
131+
input: BigNumberInput
132+
output: BigNumberOutput
133+
}
134+
135+
structure BigNumberInput {
136+
bigInt: BigInteger
137+
bigDec: BigDecimal
138+
}
139+
140+
structure BigNumberOutput {
141+
result: BigInteger
142+
ratio: BigDecimal
143+
}

codegen-core/common-test-models/misc.smithy

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,11 @@ structure InnermostShape {
9595
@required
9696
aDouble: Double,
9797

98-
// TODO(https://github.com/smithy-lang/smithy-rs/issues/312)
99-
// @required
100-
// aBigInteger: BigInteger,
98+
@required
99+
aBigInteger: BigInteger,
101100

102-
// @required
103-
// aBigDecimal: BigDecimal,
101+
@required
102+
aBigDecimal: BigDecimal,
104103

105104
@required
106105
aTimestamp: Timestamp,

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/RuntimeType.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,10 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)
425425

426426
fun dateTime(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("DateTime")
427427

428+
fun bigInteger(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("BigInteger")
429+
430+
fun bigDecimal(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("BigDecimal")
431+
428432
fun document(runtimeConfig: RuntimeConfig): RuntimeType = smithyTypes(runtimeConfig).resolve("Document")
429433

430434
fun format(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("date_time::Format")

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/SymbolVisitor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,11 @@ open class SymbolVisitor(
245245
}
246246

247247
override fun bigIntegerShape(shape: BigIntegerShape?): Symbol {
248-
TODO("Not yet implemented: https://github.com/smithy-lang/smithy-rs/issues/312")
248+
return RuntimeType.bigInteger(config.runtimeConfig).toSymbol()
249249
}
250250

251251
override fun bigDecimalShape(shape: BigDecimalShape?): Symbol {
252-
TODO("Not yet implemented: https://github.com/smithy-lang/smithy-rs/issues/312")
252+
return RuntimeType.bigDecimal(config.runtimeConfig).toSymbol()
253253
}
254254

255255
override fun operationShape(shape: OperationShape): Symbol {

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/generators/Instantiator.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import software.amazon.smithy.model.node.NullNode
1414
import software.amazon.smithy.model.node.NumberNode
1515
import software.amazon.smithy.model.node.ObjectNode
1616
import software.amazon.smithy.model.node.StringNode
17+
import software.amazon.smithy.model.shapes.BigDecimalShape
18+
import software.amazon.smithy.model.shapes.BigIntegerShape
1719
import software.amazon.smithy.model.shapes.BlobShape
1820
import software.amazon.smithy.model.shapes.BooleanShape
1921
import software.amazon.smithy.model.shapes.CollectionShape
@@ -544,6 +546,20 @@ class PrimitiveInstantiator(
544546
}
545547

546548
is StringShape -> renderString(shape, data as StringNode)(this)
549+
is BigIntegerShape -> {
550+
val value = data.toString()
551+
rustTemplate(
552+
"<#{BigInteger} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()",
553+
"BigInteger" to RuntimeType.bigInteger(runtimeConfig),
554+
)
555+
}
556+
is BigDecimalShape -> {
557+
val value = data.toString()
558+
rustTemplate(
559+
"<#{BigDecimal} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()",
560+
"BigDecimal" to RuntimeType.bigDecimal(runtimeConfig),
561+
)
562+
}
547563
is NumberShape ->
548564
when (data) {
549565
is StringNode -> {

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/CborParserGenerator.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
package software.amazon.smithy.rust.codegen.core.smithy.protocols.parse
77

8+
import software.amazon.smithy.codegen.core.CodegenException
89
import software.amazon.smithy.codegen.core.Symbol
10+
import software.amazon.smithy.model.shapes.BigDecimalShape
11+
import software.amazon.smithy.model.shapes.BigIntegerShape
912
import software.amazon.smithy.model.shapes.BlobShape
1013
import software.amazon.smithy.model.shapes.BooleanShape
1114
import software.amazon.smithy.model.shapes.ByteShape
@@ -579,6 +582,18 @@ class CborParserGenerator(
579582

580583
is TimestampShape -> rust("decoder.timestamp()")
581584

585+
// BigInteger/BigDecimal are not supported with CBOR.
586+
// The Smithy RPC v2 CBOR spec requires these to be encoded using CBOR tags 2/3/4
587+
// (binary bignum representation), but aws-smithy-cbor doesn't implement these tags yet.
588+
is BigIntegerShape ->
589+
throw CodegenException(
590+
"BigInteger is not supported with Concise Binary Object Representation (CBOR) protocol",
591+
)
592+
is BigDecimalShape ->
593+
throw CodegenException(
594+
"BigDecimal is not supported with Concise Binary Object Representation (CBOR) protocol",
595+
)
596+
582597
// Aggregate shapes: https://smithy.io/2.0/spec/aggregate-types.html
583598
is StructureShape -> deserializeStruct(target)
584599
is CollectionShape -> deserializeCollection(target)

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/parse/JsonParserGenerator.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package software.amazon.smithy.rust.codegen.core.smithy.protocols.parse
77

88
import software.amazon.smithy.codegen.core.Symbol
9+
import software.amazon.smithy.model.shapes.BigDecimalShape
10+
import software.amazon.smithy.model.shapes.BigIntegerShape
911
import software.amazon.smithy.model.shapes.BlobShape
1012
import software.amazon.smithy.model.shapes.BooleanShape
1113
import software.amazon.smithy.model.shapes.CollectionShape
@@ -296,6 +298,8 @@ class JsonParserGenerator(
296298
when (val target = model.expectShape(memberShape.target)) {
297299
is StringShape -> deserializeString(target)
298300
is BooleanShape -> rustTemplate("#{expect_bool_or_null}(tokens.next())?", *codegenScope)
301+
is BigIntegerShape -> deserializeBigInteger()
302+
is BigDecimalShape -> deserializeBigDecimal()
299303
is NumberShape -> deserializeNumber(target)
300304
is BlobShape -> deserializeBlob(memberShape)
301305
is TimestampShape -> deserializeTimestamp(memberShape)
@@ -374,6 +378,63 @@ class JsonParserGenerator(
374378
}
375379
}
376380

381+
private fun RustWriter.deserializeBigInteger() {
382+
// Match on Number enum to:
383+
// 1. Validate only integers are accepted (reject floats)
384+
// 2. Extract inner value and convert to string
385+
386+
rustTemplate(
387+
"""
388+
#{expect_number_or_null}(tokens.next())?
389+
.map(|v| {
390+
let s = match v {
391+
#{Number}::PosInt(n) => n.to_string(),
392+
#{Number}::NegInt(n) => n.to_string(),
393+
#{Number}::Float(_) => return Err(#{Error}::custom("expected integer, found float")),
394+
};
395+
Ok(<#{BigInteger} as ::std::str::FromStr>::from_str(&s).expect("infallible"))
396+
})
397+
.transpose()?
398+
""",
399+
"BigInteger" to RuntimeType.bigInteger(codegenContext.runtimeConfig),
400+
"Number" to RuntimeType.smithyTypes(codegenContext.runtimeConfig).resolve("Number"),
401+
*codegenScope,
402+
)
403+
}
404+
405+
private fun RustWriter.deserializeBigDecimal() {
406+
// Match on Number enum to extract inner value and convert to string
407+
// (Number doesn't implement Display, so we must match each variant)
408+
// For floats, preserve decimal notation that f64::to_string() drops for whole numbers
409+
410+
rustTemplate(
411+
"""
412+
#{expect_number_or_null}(tokens.next())?
413+
.map(|v| {
414+
let s = match v {
415+
#{Number}::PosInt(n) => n.to_string(),
416+
#{Number}::NegInt(n) => n.to_string(),
417+
#{Number}::Float(f) => {
418+
// Use format! to avoid scientific notation and preserve precision
419+
let s = format!("{f}");
420+
// f64 formatting drops ".0" for whole numbers (0.0 -> "0")
421+
// Restore it to preserve that the original JSON had decimal notation
422+
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
423+
format!("{s}.0")
424+
} else {
425+
s
426+
}
427+
},
428+
};
429+
<#{BigDecimal} as ::std::str::FromStr>::from_str(&s).expect("infallible")
430+
})
431+
""",
432+
"BigDecimal" to RuntimeType.bigDecimal(codegenContext.runtimeConfig),
433+
"Number" to RuntimeType.smithyTypes(codegenContext.runtimeConfig).resolve("Number"),
434+
*codegenScope,
435+
)
436+
}
437+
377438
private fun RustWriter.deserializeTimestamp(member: MemberShape) {
378439
val timestampFormat =
379440
httpBindingResolver.timestampFormat(

0 commit comments

Comments
 (0)