Skip to content

Commit 64e033f

Browse files
authored
chore(core/protocols): http binding and cbor serializer refactoring (#1659)
* chore(core/protocols): move http binding deser to HttpBindingProtocol * chore(core/protocols): internalize cbor serializer code
1 parent b15137d commit 64e033f

File tree

10 files changed

+299
-201
lines changed

10 files changed

+299
-201
lines changed

.changeset/ten-taxis-deliver.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/types": patch
3+
"@smithy/core": patch
4+
---
5+
6+
schema serde: http binding and cbor serializer refactoring

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

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NormalizedSchema } from "@smithy/core/schema";
2-
import { copyDocumentWithTransform, parseEpochTimestamp } from "@smithy/core/serde";
3-
import { Codec, Schema, SchemaRef, SerdeFunctions, ShapeDeserializer, ShapeSerializer } from "@smithy/types";
2+
import { parseEpochTimestamp } from "@smithy/core/serde";
3+
import { Codec, Schema, SerdeFunctions, ShapeDeserializer, ShapeSerializer } from "@smithy/types";
44

55
import { cbor } from "./cbor";
66
import { dateToTag } from "./parseCborBody";
@@ -40,38 +40,80 @@ export class CborShapeSerializer implements ShapeSerializer {
4040
}
4141

4242
public write(schema: Schema, value: unknown): void {
43-
this.value = copyDocumentWithTransform(value, schema, (_: any, schemaRef: SchemaRef) => {
44-
if (_ instanceof Date) {
45-
return dateToTag(_);
46-
}
47-
if (_ instanceof Uint8Array) {
48-
return _;
49-
}
43+
this.value = this.serialize(schema, value);
44+
}
5045

51-
const ns = NormalizedSchema.of(schemaRef);
52-
const sparse = !!ns.getMergedTraits().sparse;
46+
/**
47+
* Recursive serializer transform that copies and prepares the user input object
48+
* for CBOR serialization.
49+
*/
50+
public serialize(schema: Schema, source: unknown): any {
51+
const ns = NormalizedSchema.of(schema);
5352

54-
if (ns.isListSchema() && Array.isArray(_)) {
55-
if (!sparse) {
56-
return _.filter((item) => item != null);
53+
switch (typeof source) {
54+
case "undefined":
55+
return null;
56+
case "boolean":
57+
case "number":
58+
case "string":
59+
case "bigint":
60+
case "symbol":
61+
return source;
62+
case "function":
63+
case "object":
64+
if (source === null) {
65+
return null;
5766
}
58-
} else if (_ && typeof _ === "object") {
59-
const members = ns.getMemberSchemas();
60-
const isStruct = ns.isStructSchema();
61-
if (!sparse || isStruct) {
62-
for (const [k, v] of Object.entries(_)) {
63-
const filteredOutByNonSparse = !sparse && v == null;
64-
const filteredOutByUnrecognizedMember = isStruct && !(k in members);
65-
if (filteredOutByNonSparse || filteredOutByUnrecognizedMember) {
66-
delete _[k];
67+
68+
const sourceObject = source as Record<string, unknown>;
69+
const sparse = !!ns.getMergedTraits().sparse;
70+
71+
if (ns.isListSchema() && Array.isArray(sourceObject)) {
72+
const newArray = [];
73+
let i = 0;
74+
for (const item of sourceObject) {
75+
const value = this.serialize(ns.getValueSchema(), item);
76+
if (value != null || sparse) {
77+
newArray[i++] = value;
6778
}
6879
}
69-
return _;
80+
return newArray;
7081
}
71-
}
72-
73-
return _;
74-
});
82+
if (sourceObject instanceof Uint8Array) {
83+
const newBytes = new Uint8Array(sourceObject.byteLength);
84+
newBytes.set(sourceObject, 0);
85+
return newBytes;
86+
}
87+
if (sourceObject instanceof Date) {
88+
return dateToTag(sourceObject);
89+
}
90+
const newObject = {} as any;
91+
if (ns.isMapSchema()) {
92+
for (const key of Object.keys(sourceObject)) {
93+
const value = this.serialize(ns.getValueSchema(), sourceObject[key]);
94+
if (value != null || sparse) {
95+
newObject[key] = value;
96+
}
97+
}
98+
} else if (ns.isStructSchema()) {
99+
for (const [key, memberSchema] of ns.structIterator()) {
100+
const value = this.serialize(memberSchema, sourceObject[key]);
101+
if (value != null) {
102+
newObject[key] = value;
103+
}
104+
}
105+
} else if (ns.isDocumentSchema()) {
106+
for (const key of Object.keys(sourceObject)) {
107+
const value = this.serialize(ns.getValueSchema(), sourceObject[key]);
108+
if (value != null) {
109+
newObject[key] = value;
110+
}
111+
}
112+
}
113+
return newObject;
114+
default:
115+
return source;
116+
}
75117
}
76118

77119
public flush(): Uint8Array {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ describe(SmithyRpcV2CborProtocol.name, () => {
103103
0,
104104
["mySparseList", "myRegularList", "mySparseMap", "myRegularMap"],
105105
[
106-
[() => list("", "MyList", { sparse: 1 }, SCHEMA.NUMERIC), {}],
106+
[() => list("", "MySparseList", { sparse: 1 }, SCHEMA.NUMERIC), {}],
107107
[() => list("", "MyList", {}, SCHEMA.NUMERIC), {}],
108-
[() => map("", "MyMap", { sparse: 1 }, SCHEMA.STRING, SCHEMA.NUMERIC), {}],
108+
[() => map("", "MySparseMap", { sparse: 1 }, SCHEMA.STRING, SCHEMA.NUMERIC), {}],
109109
[() => map("", "MyMap", {}, SCHEMA.STRING, SCHEMA.NUMERIC), {}],
110110
]
111111
),

packages/core/src/submodules/protocols/HttpBindingProtocol.spec.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { op, SCHEMA, struct } from "@smithy/core/schema";
1+
import { map, op, SCHEMA, struct } from "@smithy/core/schema";
22
import { HttpResponse } from "@smithy/protocol-http";
33
import {
44
Codec,
@@ -8,6 +8,8 @@ import {
88
MetadataBearer,
99
OperationSchema,
1010
ResponseMetadata,
11+
Schema,
12+
SerdeFunctions,
1113
ShapeDeserializer,
1214
ShapeSerializer,
1315
} from "@smithy/types";
@@ -41,9 +43,11 @@ describe(HttpBindingProtocol.name, () => {
4143
public getShapeId(): string {
4244
throw new Error("Method not implemented.");
4345
}
46+
4447
public getPayloadCodec(): Codec<any, any> {
4548
throw new Error("Method not implemented.");
4649
}
50+
4751
protected handleError(
4852
operationSchema: OperationSchema,
4953
context: HandlerExecutionContext,
@@ -157,4 +161,53 @@ describe(HttpBindingProtocol.name, () => {
157161
);
158162
expect(request.path).toEqual("/custom/Operation");
159163
});
164+
165+
it("can deserialize a prefix header binding and header binding from the same header", async () => {
166+
type TestSignature = (
167+
schema: Schema,
168+
context: HandlerExecutionContext & SerdeFunctions,
169+
response: IHttpResponse,
170+
dataObject: any
171+
) => Promise<string[]>;
172+
const deserializeHttpMessage = ((StringRestProtocol.prototype as any).deserializeHttpMessage as TestSignature).bind(
173+
{
174+
deserializer: new FromStringShapeDeserializer({
175+
httpBindings: true,
176+
timestampFormat: {
177+
useTrait: true,
178+
default: SCHEMA.TIMESTAMP_EPOCH_SECONDS,
179+
},
180+
}),
181+
}
182+
);
183+
const httpResponse: IHttpResponse = {
184+
statusCode: 200,
185+
headers: {
186+
"my-header": "header-value",
187+
},
188+
};
189+
190+
const dataObject = {};
191+
await deserializeHttpMessage(
192+
struct(
193+
"",
194+
"Struct",
195+
0,
196+
["prefixHeaders", "header"],
197+
[
198+
[map("", "Map", 0, 0, 0), { httpPrefixHeaders: "my-" }],
199+
[0, { httpHeader: "my-header" }],
200+
]
201+
),
202+
{} as any,
203+
httpResponse,
204+
dataObject
205+
);
206+
expect(dataObject).toEqual({
207+
prefixHeaders: {
208+
header: "header-value",
209+
},
210+
header: "header-value",
211+
});
212+
});
160213
});

packages/core/src/submodules/protocols/HttpBindingProtocol.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import { NormalizedSchema, SCHEMA } from "@smithy/core/schema";
2+
import { splitEvery, splitHeader } from "@smithy/core/serde";
23
import { HttpRequest } from "@smithy/protocol-http";
34
import {
45
Endpoint,
56
EndpointBearer,
7+
EventStreamSerdeContext,
68
HandlerExecutionContext,
79
HttpRequest as IHttpRequest,
810
HttpResponse as IHttpResponse,
911
MetadataBearer,
1012
OperationSchema,
13+
Schema,
1114
SerdeFunctions,
1215
} from "@smithy/types";
16+
import { sdkStreamMixin } from "@smithy/util-stream";
1317

1418
import { collectBody } from "./collect-stream-body";
1519
import { extendedEncodeURIComponent } from "./extended-encode-uri-component";
@@ -226,4 +230,127 @@ export abstract class HttpBindingProtocol extends HttpProtocol {
226230

227231
return output;
228232
}
233+
234+
/**
235+
* The base method ignores HTTP bindings.
236+
*
237+
* @deprecated (only this signature) use signature without headerBindings.
238+
* @override
239+
*/
240+
protected async deserializeHttpMessage(
241+
schema: Schema,
242+
context: HandlerExecutionContext & SerdeFunctions,
243+
response: IHttpResponse,
244+
headerBindings: Set<string>,
245+
dataObject: any
246+
): Promise<string[]>;
247+
protected async deserializeHttpMessage(
248+
schema: Schema,
249+
context: HandlerExecutionContext & SerdeFunctions,
250+
response: IHttpResponse,
251+
dataObject: any
252+
): Promise<string[]>;
253+
protected async deserializeHttpMessage(
254+
schema: Schema,
255+
context: HandlerExecutionContext & SerdeFunctions,
256+
response: IHttpResponse,
257+
arg4: unknown,
258+
arg5?: unknown
259+
): Promise<string[]> {
260+
let dataObject: any;
261+
if (arg4 instanceof Set) {
262+
dataObject = arg5;
263+
} else {
264+
dataObject = arg4;
265+
}
266+
267+
const deserializer = this.deserializer;
268+
const ns = NormalizedSchema.of(schema);
269+
const nonHttpBindingMembers = [] as string[];
270+
271+
for (const [memberName, memberSchema] of ns.structIterator()) {
272+
const memberTraits = memberSchema.getMemberTraits();
273+
274+
if (memberTraits.httpPayload) {
275+
const isStreaming = memberSchema.isStreaming();
276+
if (isStreaming) {
277+
const isEventStream = memberSchema.isStructSchema();
278+
if (isEventStream) {
279+
// streaming event stream (union)
280+
const context = this.serdeContext as unknown as EventStreamSerdeContext;
281+
if (!context.eventStreamMarshaller) {
282+
throw new Error("@smithy/core - HttpProtocol: eventStreamMarshaller missing in serdeContext.");
283+
}
284+
const memberSchemas = memberSchema.getMemberSchemas();
285+
dataObject[memberName] = context.eventStreamMarshaller.deserialize(response.body, async (event) => {
286+
const unionMember =
287+
Object.keys(event).find((key) => {
288+
return key !== "__type";
289+
}) ?? "";
290+
if (unionMember in memberSchemas) {
291+
const eventStreamSchema = memberSchemas[unionMember];
292+
return {
293+
[unionMember]: await deserializer.read(eventStreamSchema, event[unionMember].body),
294+
};
295+
} else {
296+
// todo(schema): This union convention is ignored by the event stream marshaller.
297+
// todo(schema): This should be returned to the user instead.
298+
// see "if (deserialized.$unknown) return;" in getUnmarshalledStream.ts
299+
return {
300+
$unknown: event,
301+
};
302+
}
303+
});
304+
} else {
305+
// streaming blob body
306+
dataObject[memberName] = sdkStreamMixin(response.body);
307+
}
308+
} else if (response.body) {
309+
const bytes: Uint8Array = await collectBody(response.body, context as SerdeFunctions);
310+
if (bytes.byteLength > 0) {
311+
dataObject[memberName] = await deserializer.read(memberSchema, bytes);
312+
}
313+
}
314+
} else if (memberTraits.httpHeader) {
315+
const key = String(memberTraits.httpHeader).toLowerCase();
316+
const value = response.headers[key];
317+
if (null != value) {
318+
if (memberSchema.isListSchema()) {
319+
const headerListValueSchema = memberSchema.getValueSchema();
320+
let sections: string[];
321+
if (
322+
headerListValueSchema.isTimestampSchema() &&
323+
headerListValueSchema.getSchema() === SCHEMA.TIMESTAMP_DEFAULT
324+
) {
325+
sections = splitEvery(value, ",", 2);
326+
} else {
327+
sections = splitHeader(value);
328+
}
329+
const list = [];
330+
for (const section of sections) {
331+
list.push(await deserializer.read([headerListValueSchema, { httpHeader: key }], section.trim()));
332+
}
333+
dataObject[memberName] = list;
334+
} else {
335+
dataObject[memberName] = await deserializer.read(memberSchema, value);
336+
}
337+
}
338+
} else if (memberTraits.httpPrefixHeaders !== undefined) {
339+
dataObject[memberName] = {};
340+
for (const [header, value] of Object.entries(response.headers)) {
341+
if (header.startsWith(memberTraits.httpPrefixHeaders)) {
342+
dataObject[memberName][header.slice(memberTraits.httpPrefixHeaders.length)] = await deserializer.read(
343+
[memberSchema.getValueSchema(), { httpHeader: header }],
344+
value
345+
);
346+
}
347+
}
348+
} else if (memberTraits.httpResponseCode) {
349+
dataObject[memberName] = response.statusCode;
350+
} else {
351+
nonHttpBindingMembers.push(memberName);
352+
}
353+
}
354+
return nonHttpBindingMembers;
355+
}
229356
}

packages/core/src/submodules/protocols/HttpProtocol.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { HttpProtocol } from "./HttpProtocol";
66
import { FromStringShapeDeserializer } from "./serde/FromStringShapeDeserializer";
77

88
describe(HttpProtocol.name, () => {
9-
it("can deserialize a prefix header binding and header binding from the same header", async () => {
9+
it("ignores http bindings (only HttpBindingProtocol uses them)", async () => {
1010
type TestSignature = (
1111
schema: Schema,
1212
context: HandlerExecutionContext & SerdeFunctions,
@@ -46,10 +46,7 @@ describe(HttpProtocol.name, () => {
4646
dataObject
4747
);
4848
expect(dataObject).toEqual({
49-
prefixHeaders: {
50-
header: "header-value",
51-
},
52-
header: "header-value",
49+
// headers were ignored
5350
});
5451
});
5552
});

0 commit comments

Comments
 (0)