Skip to content

Commit 0ced415

Browse files
committed
feat: schema
schema generator wip checkpoint: schema codegen compiles in typescript feat: scaffold schema types wip: codec/protocol/transport operation schema
1 parent f0562fe commit 0ced415

File tree

61 files changed

+2408
-112
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2408
-112
lines changed

.changeset/nice-deers-shake.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@smithy/smithy-client": minor
3+
"@smithy/types": minor
4+
"@smithy/core": minor
5+
---
6+
7+
implement schema framework

packages/core/package.json

+9
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@
4545
"import": "./dist-es/submodules/protocols/index.js",
4646
"require": "./dist-cjs/submodules/protocols/index.js",
4747
"types": "./dist-types/submodules/protocols/index.d.ts"
48+
},
49+
"./schema": {
50+
"module": "./dist-es/submodules/schema/index.js",
51+
"node": "./dist-cjs/submodules/schema/index.js",
52+
"import": "./dist-es/submodules/schema/index.js",
53+
"require": "./dist-cjs/submodules/schema/index.js",
54+
"types": "./dist-types/submodules/schema/index.d.ts"
4855
}
4956
},
5057
"author": {
@@ -78,6 +85,8 @@
7885
"./cbor.js",
7986
"./protocols.d.ts",
8087
"./protocols.js",
88+
"./schema.d.ts",
89+
"./schema.js",
8190
"dist-*/**"
8291
],
8392
"homepage": "https://github.com/awslabs/smithy-typescript/tree/main/packages/core",

packages/core/schema.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Do not edit:
3+
* This is a compatibility redirect for contexts that do not understand package.json exports field.
4+
*/
5+
declare module "@smithy/core/schema" {
6+
export * from "@smithy/core/dist-types/submodules/schema/index.d";
7+
}

packages/core/schema.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
/**
3+
* Do not edit:
4+
* This is a compatibility redirect for contexts that do not understand package.json exports field.
5+
*/
6+
module.exports = require("./dist-cjs/submodules/schema/index.js");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { deref, ListSchema, StructureSchema } from "@smithy/core/schema";
2+
import { copyDocumentWithTransform, parseEpochTimestamp } from "@smithy/smithy-client";
3+
import { Codec, Schema, ShapeDeserializer, ShapeSerializer } from "@smithy/types";
4+
5+
import { cbor } from "./cbor";
6+
import { dateToTag } from "./parseCborBody";
7+
8+
/* eslint @typescript-eslint/no-unused-vars: 0 */
9+
10+
export class CborCodec implements Codec {
11+
public createSerializer(): CborShapeSerializer {
12+
return new CborShapeSerializer();
13+
}
14+
public createDeserializer(): CborShapeDeserializer {
15+
return new CborShapeDeserializer();
16+
}
17+
}
18+
19+
export class CborShapeSerializer implements ShapeSerializer {
20+
private value: unknown;
21+
22+
public write(schema: Schema, value: unknown): void {
23+
// Uint8Array (blob) is already supported by cbor serializer.
24+
25+
// As for timestamps, we don't actually need to refer to the schema since
26+
// all Date objects have a uniform serialization target.
27+
this.value = copyDocumentWithTransform(value, (_: any) => {
28+
if (_ instanceof Date) {
29+
return dateToTag(_);
30+
}
31+
return _;
32+
});
33+
}
34+
35+
public async flush(): Promise<Uint8Array> {
36+
const buffer = cbor.serialize(this.value);
37+
this.value = undefined;
38+
return buffer as Uint8Array;
39+
}
40+
}
41+
42+
export class CborShapeDeserializer implements ShapeDeserializer {
43+
public read(schema: Schema, bytes: Uint8Array): any {
44+
const data: any = cbor.deserialize(bytes);
45+
return this.readValue(schema, data);
46+
}
47+
48+
private readValue(schema: Schema, value: any): any {
49+
if (typeof schema === "string") {
50+
if (schema === "time" || schema === "epoch-seconds") {
51+
return parseEpochTimestamp(value);
52+
}
53+
if (schema === "blob") {
54+
return value;
55+
}
56+
}
57+
switch (typeof value) {
58+
case "undefined":
59+
case "boolean":
60+
case "number":
61+
case "string":
62+
case "bigint":
63+
case "symbol":
64+
return value;
65+
case "function":
66+
case "object":
67+
if (value === null) {
68+
return null;
69+
}
70+
if (Array.isArray(value)) {
71+
const newArray = new Array(value.length);
72+
let i = 0;
73+
for (const item of value) {
74+
newArray[i++] = this.readValue(schema instanceof ListSchema ? deref(schema.valueSchema) : void 0, item);
75+
}
76+
return newArray;
77+
}
78+
if ("byteLength" in (value as Uint8Array)) {
79+
return value;
80+
}
81+
if (value instanceof Date) {
82+
return value;
83+
}
84+
const newObject = {} as any;
85+
for (const key of Object.keys(value)) {
86+
newObject[key] = this.readValue(
87+
schema instanceof StructureSchema ? deref(schema.members[key]?.[0]) : void 0,
88+
value[key]
89+
);
90+
}
91+
return newObject;
92+
default:
93+
return value;
94+
}
95+
}
96+
97+
public getContainerSize(): number {
98+
throw new Error("Method not implemented.");
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { struct } from "@smithy/core/schema";
2+
import { HttpRequest } from "@smithy/protocol-http";
3+
import { toBase64 } from "@smithy/util-base64";
4+
import { describe, expect, test as it } from "vitest";
5+
6+
import { cbor } from "./cbor";
7+
import { dateToTag } from "./parseCborBody";
8+
import { SmithyRpcV2CborProtocol } from "./SmithyRpcV2CborProtocol";
9+
10+
describe(SmithyRpcV2CborProtocol.name, () => {
11+
const bytes = (arr: number[]) => Buffer.from(arr);
12+
13+
const testCases = [
14+
{
15+
name: "document with timestamp and blob",
16+
schema: struct(
17+
"MyExtendedDocument",
18+
{},
19+
{
20+
timestamp: [() => "time", {}],
21+
blob: [() => "blob", {}],
22+
}
23+
),
24+
input: {
25+
bool: true,
26+
int: 5,
27+
float: -3.001,
28+
timestamp: new Date(1_000_000),
29+
blob: bytes([97, 98, 99, 100]),
30+
},
31+
expected: {
32+
request: {},
33+
body: {
34+
bool: true,
35+
int: 5,
36+
float: -3.001,
37+
timestamp: dateToTag(new Date(1_000_000)),
38+
blob: bytes([97, 98, 99, 100]),
39+
},
40+
},
41+
},
42+
{
43+
name: "write to header and query",
44+
schema: struct(
45+
"MyExtendedDocument",
46+
{},
47+
{
48+
bool: [, { httpQuery: "bool" }],
49+
timestamp: [
50+
() => "time",
51+
{
52+
httpHeader: "timestamp",
53+
},
54+
],
55+
blob: [
56+
() => "blob",
57+
{
58+
httpHeader: "blob",
59+
},
60+
],
61+
prefixHeaders: [, { httpPrefixHeaders: "anti-" }],
62+
searchParams: [, { httpQueryParams: {} }],
63+
}
64+
),
65+
input: {
66+
bool: true,
67+
timestamp: new Date(1_000_000),
68+
blob: bytes([97, 98, 99, 100]),
69+
prefixHeaders: {
70+
pasto: "cheese dodecahedron",
71+
clockwise: "left",
72+
},
73+
searchParams: {
74+
a: 1,
75+
b: 2,
76+
},
77+
},
78+
expected: {
79+
request: {
80+
headers: {
81+
timestamp: new Date(1_000_000).toISOString(),
82+
blob: toBase64(bytes([97, 98, 99, 100])),
83+
"anti-clockwise": "left",
84+
"anti-pasto": "cheese dodecahedron",
85+
},
86+
query: { bool: "true", a: "1", b: "2" },
87+
},
88+
body: {},
89+
},
90+
},
91+
{
92+
name: "timestamp with header",
93+
schema: struct(
94+
"MyShape",
95+
{},
96+
{
97+
myHeader: [
98+
,
99+
{
100+
httpHeader: "my-header",
101+
},
102+
],
103+
myTimestamp: [() => "time", {}],
104+
}
105+
),
106+
input: {
107+
myHeader: "header!",
108+
myTimestamp: new Date(0),
109+
},
110+
expected: {
111+
request: {
112+
headers: {
113+
["my-header"]: "header!",
114+
},
115+
},
116+
body: {
117+
myTimestamp: dateToTag(new Date(0)),
118+
},
119+
},
120+
},
121+
];
122+
123+
for (const testCase of testCases) {
124+
it(`should serialize HTTP Requests: ${testCase.name}`, async () => {
125+
const protocol = new SmithyRpcV2CborProtocol();
126+
expect(protocol).toBeDefined();
127+
128+
const httpRequest = await protocol.serializeRequest(
129+
{
130+
input: testCase.schema,
131+
output: void 0,
132+
traits: {},
133+
},
134+
testCase.input,
135+
{
136+
endpointV2: {
137+
url: new URL("https://example.com/"),
138+
},
139+
}
140+
);
141+
142+
const body = httpRequest.body;
143+
httpRequest.body = void 0;
144+
145+
expect(httpRequest).toEqual(
146+
new HttpRequest({
147+
protocol: "https:",
148+
hostname: "example.com",
149+
...testCase.expected.request,
150+
})
151+
);
152+
153+
expect(cbor.deserialize(body)).toEqual(testCase.expected.body);
154+
});
155+
}
156+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { HttpProtocol } from "@smithy/core/protocols";
2+
import { HttpInterceptingShapeSerializer, OperationSchema } from "@smithy/core/schema";
3+
import type {
4+
HandlerExecutionContext,
5+
HttpRequest as IHttpRequest,
6+
HttpResponse as IHttpResponse,
7+
MetadataBearer,
8+
} from "@smithy/types";
9+
import { getSmithyContext } from "@smithy/util-middleware";
10+
11+
import { CborCodec, CborShapeSerializer } from "./CborCodec";
12+
13+
export class SmithyRpcV2CborProtocol extends HttpProtocol {
14+
private codec = new CborCodec();
15+
protected serializer = new HttpInterceptingShapeSerializer<CborShapeSerializer>(this.codec.createSerializer());
16+
protected deserializer = this.codec.createDeserializer();
17+
18+
public getShapeId(): string {
19+
return "smithy.protocols#rpcv2Cbor";
20+
}
21+
22+
public async serializeRequest<Input extends object>(
23+
operationSchema: OperationSchema,
24+
input: Input,
25+
context: HandlerExecutionContext
26+
): Promise<IHttpRequest> {
27+
const request = await super.serializeRequest(operationSchema, input, context);
28+
Object.assign(request.headers, {
29+
"content-type": "application/cbor",
30+
"smithy-protocol": "rpc-v2-cbor",
31+
accept: "application/cbor",
32+
});
33+
if (!request.body || (request.body as Uint8Array).byteLength === 0) {
34+
delete request.headers["content-type"];
35+
} else {
36+
try {
37+
request.headers["content-length"] = String((request.body as Uint8Array).byteLength);
38+
} catch (e) {}
39+
}
40+
const { service, operation } = getSmithyContext(context) as {
41+
service: string;
42+
operation: string;
43+
};
44+
const path = `/service/${service}/operation/${operation}`;
45+
if (request.path.endsWith("/")) {
46+
request.path = request.path + path.slice(1);
47+
} else {
48+
request.path = request.path + path;
49+
}
50+
return request;
51+
}
52+
53+
public async deserializeResponse<Output extends MetadataBearer>(
54+
operationSchema: OperationSchema,
55+
context: HandlerExecutionContext,
56+
response: IHttpResponse
57+
): Promise<Output> {
58+
return super.deserializeResponse(operationSchema, context, response);
59+
}
60+
}
+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export { cbor } from "./cbor";
2+
export { tag, tagSymbol } from "./cbor-types";
23
export * from "./parseCborBody";
3-
export { tagSymbol, tag } from "./cbor-types";
4+
export * from "./SmithyRpcV2CborProtocol";
5+
export * from "./CborCodec";

0 commit comments

Comments
 (0)