Skip to content

Commit ae11e3a

Browse files
authored
feat(core/schema): add schema classes (#1595)
* feat(core/schema): add schema classes * feat(core/schema): add docblocks * set schema APIs to alpha * remove static active typeregistry
1 parent aae226d commit ae11e3a

32 files changed

+1750
-9
lines changed

.changeset/grumpy-cobras-melt.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/core": minor
3+
"@smithy/middleware-serde": patch
4+
---
5+
6+
add schema classes

packages/core/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"lint": "npx eslint -c ../../.eslintrc.js \"src/**/*.ts\" --fix && node ./scripts/lint",
1313
"format": "prettier --config ../../prettier.config.js --ignore-path ../../.prettierignore --write \"**/*.{ts,md,json}\"",
1414
"extract:docs": "api-extractor run --local",
15-
"test": "yarn g:vitest run",
1615
"test:cbor:perf": "node ./scripts/cbor-perf.mjs",
16+
"test": "yarn g:vitest run",
1717
"test:watch": "yarn g:vitest watch"
1818
},
1919
"main": "./dist-cjs/index.js",
@@ -53,6 +53,13 @@
5353
"import": "./dist-es/submodules/serde/index.js",
5454
"require": "./dist-cjs/submodules/serde/index.js",
5555
"types": "./dist-types/submodules/serde/index.d.ts"
56+
},
57+
"./schema": {
58+
"module": "./dist-es/submodules/schema/index.js",
59+
"node": "./dist-cjs/submodules/schema/index.js",
60+
"import": "./dist-es/submodules/schema/index.js",
61+
"require": "./dist-cjs/submodules/schema/index.js",
62+
"types": "./dist-types/submodules/schema/index.d.ts"
5663
}
5764
},
5865
"author": {
@@ -86,6 +93,8 @@
8693
"./cbor.js",
8794
"./protocols.d.ts",
8895
"./protocols.js",
96+
"./schema.d.ts",
97+
"./schema.js",
8998
"./serde.d.ts",
9099
"./serde.js",
91100
"dist-*/**"

packages/core/schema.d.ts

Lines changed: 7 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
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");
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, test as it } from "vitest";
2+
3+
import { error } from "./schemas/ErrorSchema";
4+
import { list } from "./schemas/ListSchema";
5+
import { map } from "./schemas/MapSchema";
6+
import { struct } from "./schemas/StructureSchema";
7+
import { TypeRegistry } from "./TypeRegistry";
8+
9+
describe(TypeRegistry.name, () => {
10+
const [List, Map, Struct] = [list("ack", "List", { sparse: 1 }, 0), map("ack", "Map", 0, 0, 1), () => schema];
11+
const schema = struct("ack", "Structure", {}, ["list", "map", "struct"], [List, Map, Struct]);
12+
13+
const tr = TypeRegistry.for("ack");
14+
15+
it("stores and retrieves schema objects", () => {
16+
expect(tr.getSchema("List")).toBe(List);
17+
expect(tr.getSchema("Map")).toBe(Map);
18+
expect(tr.getSchema("Structure")).toBe(schema);
19+
});
20+
21+
it("has a helper method to retrieve a synthetic base exception", () => {
22+
// the service namespace is appended to the synthetic prefix.
23+
const err = error("smithyts.client.synthetic.ack", "UhOhServiceException", 0, [], [], Error);
24+
const tr = TypeRegistry.for("smithyts.client.synthetic.ack");
25+
expect(tr.getBaseException()).toEqual(err);
26+
});
27+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { Schema as ISchema } from "@smithy/types";
2+
3+
import { ErrorSchema } from "./schemas/ErrorSchema";
4+
5+
/**
6+
* A way to look up schema by their ShapeId values.
7+
*
8+
* @alpha
9+
*/
10+
export class TypeRegistry {
11+
public static readonly registries = new Map<string, TypeRegistry>();
12+
13+
private constructor(
14+
public readonly namespace: string,
15+
private schemas: Map<string, ISchema> = new Map()
16+
) {}
17+
18+
/**
19+
* @param namespace - specifier.
20+
* @returns the schema for that namespace, creating it if necessary.
21+
*/
22+
public static for(namespace: string): TypeRegistry {
23+
if (!TypeRegistry.registries.has(namespace)) {
24+
TypeRegistry.registries.set(namespace, new TypeRegistry(namespace));
25+
}
26+
return TypeRegistry.registries.get(namespace)!;
27+
}
28+
29+
/**
30+
* Adds the given schema to a type registry with the same namespace.
31+
*
32+
* @param shapeId - to be registered.
33+
* @param schema - to be registered.
34+
*/
35+
public register(shapeId: string, schema: ISchema) {
36+
const qualifiedName = this.normalizeShapeId(shapeId);
37+
const registry = TypeRegistry.for(this.getNamespace(shapeId));
38+
registry.schemas.set(qualifiedName, schema);
39+
}
40+
41+
/**
42+
* @param shapeId - query.
43+
* @returns the schema.
44+
*/
45+
public getSchema(shapeId: string): ISchema {
46+
const id = this.normalizeShapeId(shapeId);
47+
if (!this.schemas.has(id)) {
48+
throw new Error(`@smithy/core/schema - schema not found for ${id}`);
49+
}
50+
return this.schemas.get(id)!;
51+
}
52+
53+
/**
54+
* The smithy-typescript code generator generates a synthetic (i.e. unmodeled) base exception,
55+
* because generated SDKs before the introduction of schemas have the notion of a ServiceBaseException, which
56+
* is unique per service/model.
57+
*
58+
* This is generated under a unique prefix that is combined with the service namespace, and this
59+
* method is used to retrieve it.
60+
*
61+
* The base exception synthetic schema is used when an error is returned by a service, but we cannot
62+
* determine what existing schema to use to deserialize it.
63+
*
64+
* @returns the synthetic base exception of the service namespace associated with this registry instance.
65+
*/
66+
public getBaseException(): ErrorSchema | undefined {
67+
for (const [id, schema] of this.schemas.entries()) {
68+
if (id.startsWith("smithyts.client.synthetic.") && id.endsWith("ServiceException")) {
69+
return schema as ErrorSchema;
70+
}
71+
}
72+
return undefined;
73+
}
74+
75+
/**
76+
* @param predicate - criterion.
77+
* @returns a schema in this registry matching the predicate.
78+
*/
79+
public find(predicate: (schema: ISchema) => boolean) {
80+
return [...this.schemas.values()].find(predicate);
81+
}
82+
83+
/**
84+
* Unloads the current TypeRegistry.
85+
*/
86+
public destroy() {
87+
TypeRegistry.registries.delete(this.namespace);
88+
this.schemas.clear();
89+
}
90+
91+
private normalizeShapeId(shapeId: string) {
92+
if (shapeId.includes("#")) {
93+
return shapeId;
94+
}
95+
return this.namespace + "#" + shapeId;
96+
}
97+
98+
private getNamespace(shapeId: string) {
99+
return this.normalizeShapeId(shapeId).split("#")[0];
100+
}
101+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Schema, SchemaRef } from "@smithy/types";
2+
3+
/**
4+
* Dereferences a SchemaRef if needed.
5+
* @internal
6+
*/
7+
export const deref = (schemaRef: SchemaRef): Schema => {
8+
if (typeof schemaRef === "function") {
9+
return schemaRef();
10+
}
11+
return schemaRef;
12+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export * from "./deref";
2+
export * from "./middleware/getSchemaSerdePlugin";
3+
export * from "./schemas/ListSchema";
4+
export * from "./schemas/MapSchema";
5+
export * from "./schemas/OperationSchema";
6+
export * from "./schemas/ErrorSchema";
7+
export * from "./schemas/NormalizedSchema";
8+
export * from "./schemas/Schema";
9+
export * from "./schemas/SimpleSchema";
10+
export * from "./schemas/StructureSchema";
11+
export * from "./schemas/sentinels";
12+
export * from "./TypeRegistry";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
DeserializeHandlerOptions,
3+
MetadataBearer,
4+
MiddlewareStack,
5+
Pluggable,
6+
SerdeFunctions,
7+
SerializeHandlerOptions,
8+
} from "@smithy/types";
9+
10+
import { PreviouslyResolved } from "./schema-middleware-types";
11+
import { schemaDeserializationMiddleware } from "./schemaDeserializationMiddleware";
12+
import { schemaSerializationMiddleware } from "./schemaSerializationMiddleware";
13+
14+
/**
15+
* @internal
16+
*/
17+
export const deserializerMiddlewareOption: DeserializeHandlerOptions = {
18+
name: "deserializerMiddleware",
19+
step: "deserialize",
20+
tags: ["DESERIALIZER"],
21+
override: true,
22+
};
23+
24+
/**
25+
* @internal
26+
*/
27+
export const serializerMiddlewareOption: SerializeHandlerOptions = {
28+
name: "serializerMiddleware",
29+
step: "serialize",
30+
tags: ["SERIALIZER"],
31+
override: true,
32+
};
33+
34+
/**
35+
* @internal
36+
*/
37+
export function getSchemaSerdePlugin<InputType extends object = any, OutputType extends MetadataBearer = any>(
38+
config: PreviouslyResolved
39+
): Pluggable<InputType, OutputType> {
40+
return {
41+
applyToStack: (commandStack: MiddlewareStack<InputType, OutputType>) => {
42+
commandStack.add(schemaSerializationMiddleware(config), serializerMiddlewareOption);
43+
commandStack.add(schemaDeserializationMiddleware(config), deserializerMiddlewareOption);
44+
// `config` is fully resolved at the point of applying plugins.
45+
// As such, config qualifies as SerdeContext.
46+
config.protocol.setSerdeContext(config as SerdeFunctions);
47+
},
48+
};
49+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { ClientProtocol, SerdeContext, UrlParser } from "@smithy/types";
2+
3+
/**
4+
* @internal
5+
*/
6+
export type PreviouslyResolved = Omit<
7+
SerdeContext & {
8+
urlParser: UrlParser;
9+
protocol: ClientProtocol<any, any>;
10+
},
11+
"endpoint"
12+
>;

0 commit comments

Comments
 (0)