-
Notifications
You must be signed in to change notification settings - Fork 76
fixup json snaps and deser #692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "id": "2e107c2c-aba9-4fc7-a7ea-87a9e259466d", | ||
| "type": "bugfix", | ||
| "description": "Don't serialize unset JSON documents as `nil` in structure members.", | ||
| "modules": [ | ||
| "." | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "id": "95eb0908-2109-4264-bc1c-5a718b3dc64a", | ||
| "type": "bugfix", | ||
| "description": "Fix a deserialization panic around collection members in recursive shape configs.", | ||
| "modules": [ | ||
| "." | ||
| ] | ||
| } |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,9 @@ | |
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Set; | ||
| import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait; | ||
| import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait; | ||
| import software.amazon.smithy.aws.traits.protocols.RestJson1Trait; | ||
| import software.amazon.smithy.codegen.core.SymbolProvider; | ||
| import software.amazon.smithy.go.codegen.ChainWritable; | ||
| import software.amazon.smithy.go.codegen.GoDelegator; | ||
|
|
@@ -32,32 +35,37 @@ public void writeAdditionalFiles( | |
| goDelegator.useFileWriter("request_snapshot_test.go", settings.getModuleName(), writer -> { | ||
| writer.addBuildTag("request_snapshot"); | ||
| writer.write(commonSource()); | ||
| writer.write(bodyEqual(isCbor(settings))); | ||
| writer.write(bodyEqual(settings)); | ||
|
|
||
| var service = settings.getService(model); | ||
| var generator = new SnapshotInputGenerator(model, symbolProvider); | ||
| var generator = new SnapshotInputGenerator(model, symbolProvider, settings, false); | ||
| writer.write(snapshotTests(model, service, symbolProvider, generator)); | ||
| writer.write(snapshotUpdaters(model, service, symbolProvider, generator)); | ||
| }); | ||
| } | ||
|
|
||
| private static boolean isCbor(GoSettings settings) { | ||
| return Rpcv2CborTrait.ID.equals(settings.getProtocol()); | ||
| // Emits serdeBodyEqual, the request-body comparator, per protocol. Every variant falls back to a byte compare | ||
| // when either side won't decode, so a malformed golden fails loudly rather than silently passing. | ||
| private Writable bodyEqual(GoSettings settings) { | ||
| var protocol = settings.getProtocol(); | ||
| if (Rpcv2CborTrait.ID.equals(protocol)) { | ||
| return cborBodyEqual(); | ||
| } else if (AwsJson1_0Trait.ID.equals(protocol) || AwsJson1_1Trait.ID.equals(protocol) | ||
| || RestJson1Trait.ID.equals(protocol)) { | ||
| return jsonBodyEqual(); | ||
| } | ||
| // restXml has the same member-ordering divergence in XML element form, and awsQuery/ec2Query bodies are | ||
| // form-urlencoded. Those are out of scope for now (JSON snapshots only), so they stay on a byte compare. | ||
| return goTemplate(""" | ||
| func serdeBodyEqual(got, expected []byte) bool { | ||
| return bytes.Equal(got, expected) | ||
| } | ||
| """); | ||
| } | ||
|
|
||
| // Emits serdeBodyEqual, the request-body comparator. Most protocols serialize | ||
| // deterministically, so a raw byte compare is correct. rpcv2Cbor encodes struct | ||
| // fields as a CBOR map and the encoder emits map entries in Go map iteration | ||
| // order, so the same input produces different byte orderings across runs. For | ||
| // that protocol we compare decoded CBOR values, which is order-independent. | ||
| private Writable bodyEqual(boolean isCbor) { | ||
| if (!isCbor) { | ||
| return goTemplate(""" | ||
| func serdeBodyEqual(got, expected []byte) bool { | ||
| return bytes.Equal(got, expected) | ||
| } | ||
| """); | ||
| } | ||
| // rpcv2Cbor encodes struct fields as a CBOR map and the encoder emits map entries in Go map iteration order, so | ||
| // the same input produces different byte orderings across runs. Comparing decoded values is order-independent. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't have to resolve it here, but this has also been a point of friction on JSON encoders. Since we are now taking more control of the encoders, would it be worth it to sort map entries?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by default, a million percent no, it's bad for performance,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, as an option it sounds good.
Can't argue with this, but we've had multiple issues with serialization not being deterministic being an issue even if it's not a guarantee for JSON, so it's definitely a tredoff. Interestingly, JSON v2 switched this behavior and now map keys are not deterministic by default and you have to pass a deterministic flag https://pkg.go.dev/encoding/json/v2#Deterministic |
||
| private Writable cborBodyEqual() { | ||
| return writer -> { | ||
| writer.addUseImports(SmithyGoDependency.REFLECT); | ||
| writer.addUseImports(SmithyGoDependency.SMITHY_CBOR); | ||
|
|
@@ -77,6 +85,43 @@ func serdeBodyEqual(got, expected []byte) bool { | |
| }; | ||
| } | ||
|
|
||
| // JSON object member order isn't semantically meaningful, and legacy serde orders members by ShapeId | ||
| // (case-insensitive, via TreeSet) while schema-serde orders them by member name (byte order). A byte compare | ||
| // therefore reports thousands of semantically identical bodies as mismatches for the whole legacy -> | ||
| // schema-serde transition. | ||
| private Writable jsonBodyEqual() { | ||
| return writer -> { | ||
| writer.addUseImports(SmithyGoDependency.REFLECT); | ||
| writer.addUseImports(SmithyGoDependency.JSON); | ||
| writer.write(""" | ||
| func serdeBodyEqual(got, expected []byte) bool { | ||
| if len(got) == 0 || len(expected) == 0 { | ||
| return bytes.Equal(got, expected) | ||
| } | ||
| gv, gok := serdeDecodeJSON(got) | ||
| ev, eok := serdeDecodeJSON(expected) | ||
| if !gok || !eok { | ||
| return bytes.Equal(got, expected) | ||
| } | ||
| return reflect.DeepEqual(gv, ev) | ||
| } | ||
|
|
||
| // serdeDecodeJSON decodes a body for structural comparison. Numbers are kept as | ||
| // json.Number rather than float64 so a large int64 doesn't lose precision (which would | ||
| // mask a real difference) and so numeric formatting differences still show up. | ||
| func serdeDecodeJSON(b []byte) (any, bool) { | ||
| d := json.NewDecoder(bytes.NewReader(b)) | ||
| d.UseNumber() | ||
| var v any | ||
| if err := d.Decode(&v); err != nil { | ||
| return nil, false | ||
| } | ||
| return v, true | ||
| } | ||
| """); | ||
| }; | ||
| } | ||
|
|
||
| private Writable commonSource() { | ||
| return writer -> { | ||
| writer.addUseImports(SmithyGoDependency.OS); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what's a malformed golden?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
golden = snapshot file