Skip to content

Commit 9ccb841

Browse files
authored
fix(core/protocols): support for $unknown union members in schema-serde (#1820)
1 parent 8d725cc commit 9ccb841

File tree

6 files changed

+148
-8
lines changed

6 files changed

+148
-8
lines changed

.changeset/chatty-dogs-rule.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/types": minor
3+
"@smithy/core": minor
4+
---
5+
6+
add static union schema as a new type

packages/core/src/submodules/cbor/CborCodec.spec.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { NormalizedSchema } from "@smithy/core/schema";
2-
import type { StaticSimpleSchema, StaticStructureSchema, StringSchema, TimestampDefaultSchema } from "@smithy/types";
2+
import type {
3+
StaticSimpleSchema,
4+
StaticStructureSchema,
5+
StaticUnionSchema,
6+
StringSchema,
7+
TimestampDefaultSchema,
8+
} from "@smithy/types";
39
import { describe, expect, it } from "vitest";
410

511
import { cbor } from "./cbor";
@@ -94,6 +100,32 @@ describe(CborShapeSerializer.name, () => {
94100
},
95101
});
96102
});
103+
104+
it("can serialize the $unknown union convention", async () => {
105+
const schema = [
106+
3,
107+
"ns",
108+
"Struct",
109+
0,
110+
["union"],
111+
[[4, "ns", "Union", 0, ["a", "b", "c"], [0, 0, 0]] satisfies StaticUnionSchema],
112+
] satisfies StaticStructureSchema;
113+
114+
const ns = NormalizedSchema.of(schema);
115+
const input = {
116+
union: {
117+
$unknown: ["d", {}],
118+
},
119+
};
120+
serializer.write(ns, input);
121+
const serialization = serializer.flush();
122+
const objectEquivalent = cbor.deserialize(serialization);
123+
expect(objectEquivalent).toEqual({
124+
union: {
125+
d: {},
126+
},
127+
});
128+
});
97129
});
98130

99131
describe("deserialization", () => {
@@ -128,5 +160,30 @@ describe(CborShapeSerializer.name, () => {
128160
timestamp: new Date(1),
129161
});
130162
});
163+
164+
it("deserializes unknown union members to the $unknown conventional property", async () => {
165+
const schema = [
166+
3,
167+
"ns",
168+
"Struct",
169+
0,
170+
["union"],
171+
[[4, "ns", "Union", 0, ["a", "b", "c"], [0, 0, 0]] satisfies StaticUnionSchema],
172+
] satisfies StaticStructureSchema;
173+
const ns = NormalizedSchema.of(schema);
174+
const receivedData = {
175+
union: {
176+
__type: "ns.Union",
177+
d: {},
178+
},
179+
};
180+
const serialization = cbor.serialize(receivedData);
181+
const deserialized = await deserializer.read(ns, serialization);
182+
expect(deserialized).toEqual({
183+
union: {
184+
$unknown: ["d", {}],
185+
},
186+
} satisfies Record<string, unknown>);
187+
});
131188
});
132189
});

packages/core/src/submodules/cbor/CborCodec.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ export class CborShapeSerializer extends SerdeContext implements ShapeSerializer
9696
newObject[key] = value;
9797
}
9898
}
99+
const isUnion = ns.isUnionSchema();
100+
if (isUnion && Array.isArray(sourceObject.$unknown)) {
101+
const [k, v] = sourceObject.$unknown;
102+
newObject[k] = v;
103+
}
99104
} else if (ns.isDocumentSchema()) {
100105
for (const key of Object.keys(sourceObject)) {
101106
newObject[key] = this.serialize(ns.getValueSchema(), sourceObject[key]);
@@ -199,11 +204,22 @@ export class CborShapeDeserializer extends SerdeContext implements ShapeDeserial
199204
}
200205
}
201206
} else if (ns.isStructSchema()) {
207+
const isUnion = ns.isUnionSchema();
208+
let keys: Set<string> | undefined;
209+
if (isUnion) {
210+
keys = new Set(Object.keys(value).filter((k) => k !== "__type"));
211+
}
202212
for (const [key, memberSchema] of ns.structIterator()) {
203-
const v = this.readValue(memberSchema, value[key]);
204-
if (v != null) {
205-
newObject[key] = v;
213+
if (isUnion) {
214+
keys!.delete(key);
206215
}
216+
if (value[key] != null) {
217+
newObject[key] = this.readValue(memberSchema, value[key]);
218+
}
219+
}
220+
if (isUnion && keys?.size === 1 && Object.keys(newObject).length === 0) {
221+
const k = keys!.values().next().value as string;
222+
newObject.$unknown = [k, value[k]];
207223
}
208224
}
209225
return newObject;

packages/core/src/submodules/schema/schemas/NormalizedSchema.spec.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
StaticMapSchema,
1313
StaticSimpleSchema,
1414
StaticStructureSchema,
15+
StaticUnionSchema,
1516
StreamingBlobSchema,
1617
StringSchema,
1718
TimestampDefaultSchema,
@@ -22,12 +23,25 @@ import { NormalizedSchema } from "./NormalizedSchema";
2223
import { translateTraits } from "./translateTraits";
2324

2425
describe(NormalizedSchema.name, () => {
25-
const [List, Map, Struct]: [StaticListSchema, StaticMapSchema, () => StaticStructureSchema] = [
26+
const [List, Map, Struct, Union]: [
27+
StaticListSchema,
28+
StaticMapSchema,
29+
() => StaticStructureSchema,
30+
StaticUnionSchema,
31+
] = [
2632
[1, "ack", "List", { sparse: 1 }, 0] satisfies StaticListSchema,
2733
[2, "ack", "Map", 0, 0, 1] satisfies StaticMapSchema,
2834
() => schema,
35+
[4, "ack", "Union", 0, ["a", "b", "c"], ["unit", 0, 128]],
36+
];
37+
const schema: StaticStructureSchema = [
38+
3,
39+
"ack",
40+
"Structure",
41+
{},
42+
["list", "map", "struct", "union"],
43+
[List, Map, Struct, Union],
2944
];
30-
const schema: StaticStructureSchema = [3, "ack", "Structure", {}, ["list", "map", "struct"], [List, Map, Struct]];
3145

3246
const ns = NormalizedSchema.of(schema);
3347
const nsFromIndirect = NormalizedSchema.of(() => ns);
@@ -200,6 +214,22 @@ describe(NormalizedSchema.name, () => {
200214
);
201215
});
202216
});
217+
describe("union member", () => {
218+
it("is a union and a struct", () => {
219+
const member = ns.getMemberSchema("union");
220+
expect(member.getName(true)).toBe("ack#Union");
221+
expect(member.isMemberSchema()).toBe(true);
222+
expect(member.isListSchema()).toBe(false);
223+
expect(member.isMapSchema()).toBe(false);
224+
expect(member.isStructSchema()).toBe(true);
225+
expect(member.isUnionSchema()).toBe(true);
226+
expect(member.getMemberName()).toBe("union");
227+
228+
expect(member.getMemberSchema("a").isUnitSchema()).toBe(true);
229+
expect(member.getMemberSchema("b").isStringSchema()).toBe(true);
230+
expect(member.getMemberSchema("c").isMapSchema()).toBe(true);
231+
});
232+
});
203233
});
204234

205235
describe("iteration", () => {

packages/core/src/submodules/schema/schemas/NormalizedSchema.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
StaticSchemaIdList,
2323
StaticSchemaIdMap,
2424
StaticSchemaIdStruct,
25+
StaticSchemaIdUnion,
2526
StaticSimpleSchema,
2627
StaticStructureSchema,
2728
StreamingBlobSchema,
@@ -194,14 +195,26 @@ export class NormalizedSchema implements INormalizedSchema {
194195
: (sc as StaticSchema)[0] === (2 satisfies StaticSchemaIdMap);
195196
}
196197

198+
/**
199+
* To simplify serialization logic, static union schemas are considered a specialization
200+
* of structs in the TypeScript typings and JS runtime, as well as static error schemas
201+
* which have a different identifier.
202+
*/
197203
public isStructSchema(): boolean {
198204
const sc = this.getSchema();
205+
const id = (sc as StaticSchema)[0];
199206
return (
200-
(sc as StaticSchema)[0] === (3 satisfies StaticSchemaIdStruct) ||
201-
(sc as StaticSchema)[0] === (-3 satisfies StaticSchemaIdError)
207+
id === (3 satisfies StaticSchemaIdStruct) ||
208+
id === (-3 satisfies StaticSchemaIdError) ||
209+
id === (4 satisfies StaticSchemaIdUnion)
202210
);
203211
}
204212

213+
public isUnionSchema(): boolean {
214+
const sc = this.getSchema();
215+
return (sc as StaticSchema)[0] === (4 satisfies StaticSchemaIdUnion);
216+
}
217+
205218
public isBlobSchema(): boolean {
206219
const sc = this.getSchema();
207220
return sc === (21 satisfies BlobSchema) || sc === (42 satisfies StreamingBlobSchema);

packages/types/src/schema/static-schemas.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ export type StaticSchemaIdMap = 2;
2525
*/
2626
export type StaticSchemaIdStruct = 3;
2727

28+
/**
29+
* @public
30+
*/
31+
export type StaticSchemaIdUnion = 4;
32+
2833
/**
2934
* @public
3035
*/
@@ -43,6 +48,7 @@ export type StaticSchema =
4348
| StaticListSchema
4449
| StaticMapSchema
4550
| StaticStructureSchema
51+
| StaticUnionSchema
4652
| StaticErrorSchema
4753
| StaticOperationSchema;
4854

@@ -83,6 +89,18 @@ export type StaticStructureSchema = [
8389
$SchemaRef[], // member schema list
8490
];
8591

92+
/**
93+
* @public
94+
*/
95+
export type StaticUnionSchema = [
96+
StaticSchemaIdUnion,
97+
ShapeNamespace,
98+
ShapeName,
99+
SchemaTraits,
100+
string[], // member name list
101+
$SchemaRef[], // member schema list
102+
];
103+
86104
/**
87105
* @public
88106
*/

0 commit comments

Comments
 (0)