Skip to content

Commit df2ceab

Browse files
committed
feat(core/protocols): runtime protocol classes
1 parent 6542d25 commit df2ceab

30 files changed

+2343
-5
lines changed

packages/core/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,18 @@
8282
"license": "Apache-2.0",
8383
"dependencies": {
8484
"@aws-sdk/types": "*",
85+
"@aws-sdk/xml-builder": "*",
8586
"@smithy/core": "^3.5.1",
8687
"@smithy/node-config-provider": "^4.1.3",
8788
"@smithy/property-provider": "^4.0.4",
8889
"@smithy/protocol-http": "^5.1.2",
8990
"@smithy/signature-v4": "^5.1.2",
9091
"@smithy/smithy-client": "^4.4.1",
9192
"@smithy/types": "^4.3.1",
93+
"@smithy/util-base64": "^4.0.0",
94+
"@smithy/util-body-length-browser": "^4.0.0",
9295
"@smithy/util-middleware": "^4.0.4",
96+
"@smithy/util-utf8": "^4.0.0",
9397
"fast-xml-parser": "4.4.1",
9498
"tslib": "^2.6.2"
9599
},
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ConfigurableSerdeContext, SerdeFunctions } from "@smithy/types";
2+
3+
/**
4+
* @internal
5+
*/
6+
export class SerdeContextConfig implements ConfigurableSerdeContext {
7+
protected serdeContext?: SerdeFunctions;
8+
9+
public setSerdeContext(serdeContext: SerdeFunctions): void {
10+
this.serdeContext = serdeContext;
11+
}
12+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { collectBody } from "@smithy/smithy-client";
2-
import type { HttpResponse, SerdeContext } from "@smithy/types";
2+
import type { SerdeFunctions } from "@smithy/types";
33

4-
export const collectBodyString = (streamBody: any, context: SerdeContext): Promise<string> =>
4+
export const collectBodyString = (streamBody: any, context: SerdeFunctions): Promise<string> =>
55
collectBody(streamBody, context).then((body) => context.utf8Encoder(body));
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
export * from "./coercing-serializers";
2+
export * from "./json/AwsJson1_0Protocol";
3+
export * from "./json/AwsJson1_1Protocol";
4+
export * from "./json/AwsJsonRpcProtocol";
5+
export * from "./json/AwsRestJsonProtocol";
6+
export * from "./json/JsonCodec";
7+
export * from "./json/JsonShapeDeserializer";
8+
export * from "./json/JsonShapeSerializer";
29
export * from "./json/awsExpectUnion";
310
export * from "./json/parseJsonBody";
11+
export * from "./query/AwsEc2QueryProtocol";
12+
export * from "./query/AwsQueryProtocol";
13+
export * from "./xml/AwsRestXmlProtocol";
14+
export * from "./xml/XmlCodec";
15+
export * from "./xml/XmlShapeDeserializer";
16+
export * from "./xml/XmlShapeSerializer";
417
export * from "./xml/parseXmlBody";
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { map, op, SCHEMA, sim, struct } from "@smithy/core/schema";
2+
import { toBase64 } from "@smithy/util-base64";
3+
import { toUtf8 } from "@smithy/util-utf8";
4+
import { describe, expect, test as it } from "vitest";
5+
6+
import { AwsJson1_0Protocol } from "./AwsJson1_0Protocol";
7+
8+
describe(AwsJson1_0Protocol.name, () => {
9+
const json = {
10+
string: "string",
11+
number: 1234,
12+
boolean: false,
13+
blob: "AAAAAAAAAAA=",
14+
timestamp: 0,
15+
};
16+
const schema = struct(
17+
"ns",
18+
"MyStruct",
19+
0,
20+
[...Object.keys(json)],
21+
[SCHEMA.STRING, SCHEMA.NUMERIC, SCHEMA.BOOLEAN, SCHEMA.BLOB, SCHEMA.TIMESTAMP_DEFAULT]
22+
);
23+
const serdeContext = {
24+
base64Encoder: toBase64,
25+
utf8Encoder: toUtf8,
26+
} as any;
27+
28+
describe("codec", () => {
29+
it("serializes blobs and timestamps", () => {
30+
const protocol = new AwsJson1_0Protocol({
31+
defaultNamespace: "namespace",
32+
});
33+
protocol.setSerdeContext(serdeContext);
34+
const codec = protocol.getPayloadCodec();
35+
const serializer = codec.createSerializer();
36+
const data = {
37+
string: "string",
38+
number: 1234,
39+
boolean: false,
40+
blob: new Uint8Array(8),
41+
timestamp: new Date(0),
42+
};
43+
serializer.write(schema, data);
44+
const serialized = serializer.flush();
45+
expect(JSON.parse(serialized)).toEqual({
46+
string: "string",
47+
number: 1234,
48+
boolean: false,
49+
blob: "AAAAAAAAAAA=",
50+
timestamp: 0,
51+
});
52+
});
53+
54+
it("deserializes blobs and timestamps", async () => {
55+
const protocol = new AwsJson1_0Protocol({
56+
defaultNamespace: "namespace",
57+
});
58+
protocol.setSerdeContext(serdeContext);
59+
const codec = protocol.getPayloadCodec();
60+
const deserializer = codec.createDeserializer();
61+
62+
const parsed = await deserializer.read(schema, JSON.stringify(json));
63+
expect(parsed).toEqual({
64+
string: "string",
65+
number: 1234,
66+
boolean: false,
67+
blob: new Uint8Array(8),
68+
timestamp: new Date(0),
69+
});
70+
});
71+
72+
it("ignores JSON name and HTTP bindings", async () => {
73+
const protocol = new AwsJson1_0Protocol({
74+
defaultNamespace: "namespace",
75+
});
76+
protocol.setSerdeContext(serdeContext);
77+
78+
const schema = struct(
79+
"ns",
80+
"MyHttpBindingStructure",
81+
{},
82+
["header", "query", "headerMap", "payload"],
83+
[
84+
sim("ns", "MyHeader", SCHEMA.STRING, { httpHeader: "header", jsonName: "MyHeader" }),
85+
sim("ns", "MyQuery", SCHEMA.STRING, { httpQuery: "query" }),
86+
map(
87+
"ns",
88+
"HeaderMap",
89+
{
90+
httpPrefixHeaders: "",
91+
},
92+
SCHEMA.STRING,
93+
SCHEMA.NUMERIC
94+
),
95+
sim("ns", "MyPayload", SCHEMA.DOCUMENT, { httpPayload: 1 }),
96+
]
97+
);
98+
const operationSchema = op("ns", "MyOperation", {}, schema, "unit");
99+
100+
const request = await protocol.serializeRequest(
101+
operationSchema,
102+
{
103+
header: "hello",
104+
query: "world",
105+
headerMap: {
106+
a: 1,
107+
b: 2,
108+
},
109+
},
110+
{
111+
async endpoint() {
112+
return {
113+
protocol: "https:",
114+
hostname: "amazonaws.com",
115+
path: "/",
116+
};
117+
},
118+
} as any
119+
);
120+
121+
expect(request.headers).toEqual({
122+
"content-length": "60",
123+
"content-type": "application/x-amz-json-1.0",
124+
"x-amz-target": "JsonRpc10.MyOperation",
125+
});
126+
expect(request.query).toEqual({});
127+
expect(request.body).toEqual(`{"header":"hello","query":"world","headerMap":{"a":1,"b":2}}`);
128+
});
129+
});
130+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { AwsJsonRpcProtocol } from "./AwsJsonRpcProtocol";
2+
3+
/**
4+
* @alpha
5+
* @see https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html#differences-between-awsjson1-0-and-awsjson1-1
6+
*/
7+
export class AwsJson1_0Protocol extends AwsJsonRpcProtocol {
8+
public constructor({ defaultNamespace }: { defaultNamespace: string }) {
9+
super({
10+
defaultNamespace,
11+
});
12+
}
13+
14+
public getShapeId(): string {
15+
return "aws.protocols#awsJson1_0";
16+
}
17+
18+
protected getJsonRpcVersion() {
19+
return "1.0" as const;
20+
}
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { describe, expect, test as it } from "vitest";
2+
3+
describe("", () => {
4+
it("placeholder", async () => {
5+
void expect;
6+
throw new Error("NYI");
7+
});
8+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { AwsJsonRpcProtocol } from "./AwsJsonRpcProtocol";
2+
3+
/**
4+
* @alpha
5+
* @see https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html#differences-between-awsjson1-0-and-awsjson1-1
6+
*/
7+
export class AwsJson1_1Protocol extends AwsJsonRpcProtocol {
8+
public constructor({ defaultNamespace }: { defaultNamespace: string }) {
9+
super({
10+
defaultNamespace,
11+
});
12+
}
13+
14+
public getShapeId(): string {
15+
return "aws.protocols#awsJson1_1";
16+
}
17+
18+
protected getJsonRpcVersion() {
19+
return "1.1" as const;
20+
}
21+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { describe, expect, test as it } from "vitest";
2+
3+
describe("", () => {
4+
it("placeholder", async () => {
5+
void expect;
6+
throw new Error("NYI");
7+
});
8+
});
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { RpcProtocol } from "@smithy/core/protocols";
2+
import { deref, ErrorSchema, NormalizedSchema, SCHEMA, TypeRegistry } from "@smithy/core/schema";
3+
import {
4+
EndpointBearer,
5+
HandlerExecutionContext,
6+
HttpRequest,
7+
HttpResponse,
8+
OperationSchema,
9+
ResponseMetadata,
10+
SerdeFunctions,
11+
ShapeDeserializer,
12+
ShapeSerializer,
13+
} from "@smithy/types";
14+
import { calculateBodyLength } from "@smithy/util-body-length-browser";
15+
16+
import { JsonCodec } from "./JsonCodec";
17+
import { loadRestJsonErrorCode } from "./parseJsonBody";
18+
19+
/**
20+
* @alpha
21+
*/
22+
export abstract class AwsJsonRpcProtocol extends RpcProtocol {
23+
protected serializer: ShapeSerializer<string | Uint8Array>;
24+
protected deserializer: ShapeDeserializer<string | Uint8Array>;
25+
private codec: JsonCodec;
26+
27+
protected constructor({ defaultNamespace }: { defaultNamespace: string }) {
28+
super({
29+
defaultNamespace,
30+
});
31+
this.codec = new JsonCodec({
32+
timestampFormat: {
33+
useTrait: true,
34+
default: SCHEMA.TIMESTAMP_EPOCH_SECONDS,
35+
},
36+
jsonName: false,
37+
});
38+
this.serializer = this.codec.createSerializer();
39+
this.deserializer = this.codec.createDeserializer();
40+
}
41+
42+
public async serializeRequest<Input extends object>(
43+
operationSchema: OperationSchema,
44+
input: Input,
45+
context: HandlerExecutionContext & SerdeFunctions & EndpointBearer
46+
): Promise<HttpRequest> {
47+
const request = await super.serializeRequest(operationSchema, input, context);
48+
if (!request.path.endsWith("/")) {
49+
request.path += "/";
50+
}
51+
Object.assign(request.headers, {
52+
"content-type": `application/x-amz-json-${this.getJsonRpcVersion()}`,
53+
"x-amz-target":
54+
(this.getJsonRpcVersion() === "1.0" ? `JsonRpc10.` : `JsonProtocol.`) +
55+
NormalizedSchema.of(operationSchema).getName(),
56+
});
57+
if (deref(operationSchema.input) === "unit" || !request.body) {
58+
request.body = "{}";
59+
}
60+
try {
61+
request.headers["content-length"] = String(calculateBodyLength(request.body));
62+
} catch (e) {}
63+
return request;
64+
}
65+
66+
public getPayloadCodec(): JsonCodec {
67+
return this.codec;
68+
}
69+
70+
protected abstract getJsonRpcVersion(): "1.1" | "1.0";
71+
72+
protected async handleError(
73+
operationSchema: OperationSchema,
74+
context: HandlerExecutionContext & SerdeFunctions,
75+
response: HttpResponse,
76+
dataObject: any,
77+
metadata: ResponseMetadata
78+
): Promise<never> {
79+
// loadRestJsonErrorCode is still used in JSON RPC.
80+
const errorIdentifier = loadRestJsonErrorCode(response, dataObject) ?? "Unknown";
81+
82+
let namespace = this.options.defaultNamespace;
83+
let errorName = errorIdentifier;
84+
if (errorIdentifier.includes("#")) {
85+
[namespace, errorName] = errorIdentifier.split("#");
86+
}
87+
88+
const registry = TypeRegistry.for(namespace);
89+
let errorSchema: ErrorSchema;
90+
try {
91+
errorSchema = registry.getSchema(errorIdentifier) as ErrorSchema;
92+
} catch (e) {
93+
const baseExceptionSchema = TypeRegistry.for("awssdkjs.synthetic." + namespace).getBaseException();
94+
if (baseExceptionSchema) {
95+
const ErrorCtor = baseExceptionSchema.ctor;
96+
throw Object.assign(new ErrorCtor(errorName), dataObject);
97+
}
98+
throw new Error(errorName);
99+
}
100+
101+
const ns = NormalizedSchema.of(errorSchema);
102+
const message = dataObject.message ?? dataObject.Message ?? "Unknown";
103+
const exception = new errorSchema.ctor(message);
104+
105+
const headerBindings = new Set<string>(
106+
Object.values(NormalizedSchema.of(errorSchema).getMemberSchemas())
107+
.map((schema) => {
108+
return schema.getMergedTraits().httpHeader;
109+
})
110+
.filter(Boolean) as string[]
111+
);
112+
await this.deserializeHttpMessage(errorSchema, context, response, headerBindings, dataObject);
113+
const output = {} as any;
114+
for (const [name, member] of Object.entries(ns.getMemberSchemas())) {
115+
const target = member.getMergedTraits().jsonName ?? name;
116+
output[name] = this.codec.createDeserializer().readObject(member, dataObject[target]);
117+
}
118+
119+
Object.assign(exception, {
120+
$metadata: metadata,
121+
$response: response,
122+
$fault: ns.getMergedTraits().error,
123+
message,
124+
...output,
125+
});
126+
127+
throw exception;
128+
}
129+
}

0 commit comments

Comments
 (0)