Skip to content

Commit

Permalink
feat: schema
Browse files Browse the repository at this point in the history
schema generator wip

checkpoint: schema codegen compiles in typescript

feat: scaffold schema types

wip: codec/protocol/transport

operation schema
  • Loading branch information
kuhe committed Feb 1, 2025
1 parent f0562fe commit e1ac61f
Show file tree
Hide file tree
Showing 37 changed files with 2,042 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .changeset/nice-deers-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@smithy/types": minor
"@smithy/core": minor
---

implement schema framework
9 changes: 9 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@
"import": "./dist-es/submodules/protocols/index.js",
"require": "./dist-cjs/submodules/protocols/index.js",
"types": "./dist-types/submodules/protocols/index.d.ts"
},
"./schema": {
"module": "./dist-es/submodules/schema/index.js",
"node": "./dist-cjs/submodules/schema/index.js",
"import": "./dist-es/submodules/schema/index.js",
"require": "./dist-cjs/submodules/schema/index.js",
"types": "./dist-types/submodules/schema/index.d.ts"
}
},
"author": {
Expand Down Expand Up @@ -78,6 +85,8 @@
"./cbor.js",
"./protocols.d.ts",
"./protocols.js",
"./schema.d.ts",
"./schema.js",
"dist-*/**"
],
"homepage": "https://github.com/awslabs/smithy-typescript/tree/main/packages/core",
Expand Down
7 changes: 7 additions & 0 deletions packages/core/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Do not edit:
* This is a compatibility redirect for contexts that do not understand package.json exports field.
*/
declare module "@smithy/core/schema" {
export * from "@smithy/core/dist-types/submodules/schema/index.d";
}
6 changes: 6 additions & 0 deletions packages/core/schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

/**
* Do not edit:
* This is a compatibility redirect for contexts that do not understand package.json exports field.
*/
module.exports = require("./dist-cjs/submodules/schema/index.js");
100 changes: 100 additions & 0 deletions packages/core/src/submodules/cbor/CborCodec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { deref, ListSchema, StructureSchema } from "@smithy/core/schema";
import { copyDocumentWithTransform, parseEpochTimestamp } from "@smithy/smithy-client";
import { Codec, Schema, ShapeDeserializer, ShapeSerializer } from "@smithy/types";

import { cbor } from "./cbor";
import { dateToTag } from "./parseCborBody";

/* eslint @typescript-eslint/no-unused-vars: 0 */

export class CborCodec implements Codec {
public createSerializer(): CborShapeSerializer {
return new CborShapeSerializer();
}
public createDeserializer(): CborShapeDeserializer {
return new CborShapeDeserializer();
}
}

export class CborShapeSerializer implements ShapeSerializer {
private value: unknown;

public write(schema: Schema, value: unknown): void {
// Uint8Array (blob) is already supported by cbor serializer.

// As for timestamps, we don't actually need to refer to the schema since
// all Date objects have a uniform serialization target.
this.value = copyDocumentWithTransform(value, (_: any) => {
if (_ instanceof Date) {
return dateToTag(_);
}
return _;
});
}

public async flush(): Promise<Uint8Array> {
const buffer = cbor.serialize(this.value);
this.value = undefined;
return buffer as Uint8Array;
}
}

export class CborShapeDeserializer implements ShapeDeserializer {
public read(schema: Schema, bytes: Uint8Array): any {
const data: any = cbor.deserialize(bytes);
return this.readValue(schema, data);
}

private readValue(schema: Schema, value: any): any {
if (typeof schema === "string") {
if (schema === "time" || schema === "epoch-seconds") {
return parseEpochTimestamp(value);
}
if (schema === "blob") {
return value;
}
}
switch (typeof value) {
case "undefined":
case "boolean":
case "number":
case "string":
case "bigint":
case "symbol":
return value;
case "function":
case "object":
if (value === null) {
return null;
}
if (Array.isArray(value)) {
const newArray = new Array(value.length);
let i = 0;
for (const item of value) {
newArray[i++] = this.readValue(schema instanceof ListSchema ? deref(schema.valueSchema) : void 0, item);
}
return newArray;
}
if ("byteLength" in (value as Uint8Array)) {
return value;
}
if (value instanceof Date) {
return value;
}
const newObject = {} as any;
for (const key of Object.keys(value)) {
newObject[key] = this.readValue(
schema instanceof StructureSchema ? deref(schema.members[key]?.[0]) : void 0,
value[key]
);
}
return newObject;
default:
return value;
}
}

public getContainerSize(): number {
throw new Error("Method not implemented.");
}
}
156 changes: 156 additions & 0 deletions packages/core/src/submodules/cbor/SmithyRpcV2CborProtocol.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { struct } from "@smithy/core/schema";
import { HttpRequest } from "@smithy/protocol-http";
import { toBase64 } from "@smithy/util-base64";
import { describe, expect, test as it } from "vitest";

import { cbor } from "./cbor";
import { dateToTag } from "./parseCborBody";
import { SmithyRpcV2CborProtocol } from "./SmithyRpcV2CborProtocol";

describe(SmithyRpcV2CborProtocol.name, () => {
const bytes = (arr: number[]) => Buffer.from(arr);

const testCases = [
{
name: "document with timestamp and blob",
schema: struct(
"MyExtendedDocument",
{},
{
timestamp: [() => "time", {}],
blob: [() => "blob", {}],
}
),
input: {
bool: true,
int: 5,
float: -3.001,
timestamp: new Date(1_000_000),
blob: bytes([97, 98, 99, 100]),
},
expected: {
request: {},
body: {
bool: true,
int: 5,
float: -3.001,
timestamp: dateToTag(new Date(1_000_000)),
blob: bytes([97, 98, 99, 100]),
},
},
},
{
name: "write to header and query",
schema: struct(
"MyExtendedDocument",
{},
{
bool: [, { httpQuery: "bool" }],
timestamp: [
() => "time",
{
httpHeader: "timestamp",
},
],
blob: [
() => "blob",
{
httpHeader: "blob",
},
],
prefixHeaders: [, { httpPrefixHeaders: "anti-" }],
searchParams: [, { httpQueryParams: {} }],
}
),
input: {
bool: true,
timestamp: new Date(1_000_000),
blob: bytes([97, 98, 99, 100]),
prefixHeaders: {
pasto: "cheese dodecahedron",
clockwise: "left",
},
searchParams: {
a: 1,
b: 2,
},
},
expected: {
request: {
headers: {
timestamp: new Date(1_000_000).toISOString(),
blob: toBase64(bytes([97, 98, 99, 100])),
"anti-clockwise": "left",
"anti-pasto": "cheese dodecahedron",
},
query: { bool: "true", a: "1", b: "2" },
},
body: {},
},
},
{
name: "timestamp with header",
schema: struct(
"MyShape",
{},
{
myHeader: [
,
{
httpHeader: "my-header",
},
],
myTimestamp: [() => "time", {}],
}
),
input: {
myHeader: "header!",
myTimestamp: new Date(0),
},
expected: {
request: {
headers: {
["my-header"]: "header!",
},
},
body: {
myTimestamp: dateToTag(new Date(0)),
},
},
},
];

for (const testCase of testCases) {
it(`should serialize HTTP Requests: ${testCase.name}`, async () => {
const protocol = new SmithyRpcV2CborProtocol();
expect(protocol).toBeDefined();

const httpRequest = await protocol.serializeRequest(
{
input: testCase.schema,
output: void 0,
traits: {},
},
testCase.input,
{
endpointV2: {
url: new URL("https://example.com/"),
},
}
);

const body = httpRequest.body;
httpRequest.body = void 0;

expect(httpRequest).toEqual(
new HttpRequest({
protocol: "https:",
hostname: "example.com",
...testCase.expected.request,
})
);

expect(cbor.deserialize(body)).toEqual(testCase.expected.body);
});
}
});
Loading

0 comments on commit e1ac61f

Please sign in to comment.