Skip to content

feat(core/schema): add schema classes #1595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/grumpy-cobras-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@smithy/core": minor
"@smithy/middleware-serde": patch
---

add schema classes
11 changes: 10 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"lint": "npx eslint -c ../../.eslintrc.js \"src/**/*.ts\" --fix && node ./scripts/lint",
"format": "prettier --config ../../prettier.config.js --ignore-path ../../.prettierignore --write \"**/*.{ts,md,json}\"",
"extract:docs": "api-extractor run --local",
"test": "yarn g:vitest run",
"test:cbor:perf": "node ./scripts/cbor-perf.mjs",
"test": "yarn g:vitest run",
"test:watch": "yarn g:vitest watch"
},
"main": "./dist-cjs/index.js",
Expand Down Expand Up @@ -53,6 +53,13 @@
"import": "./dist-es/submodules/serde/index.js",
"require": "./dist-cjs/submodules/serde/index.js",
"types": "./dist-types/submodules/serde/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 @@ -86,6 +93,8 @@
"./cbor.js",
"./protocols.d.ts",
"./protocols.js",
"./schema.d.ts",
"./schema.js",
"./serde.d.ts",
"./serde.js",
"dist-*/**"
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");
27 changes: 27 additions & 0 deletions packages/core/src/submodules/schema/TypeRegistry.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, test as it } from "vitest";

import { error } from "./schemas/ErrorSchema";
import { list } from "./schemas/ListSchema";
import { map } from "./schemas/MapSchema";
import { struct } from "./schemas/StructureSchema";
import { TypeRegistry } from "./TypeRegistry";

describe(TypeRegistry.name, () => {
const [List, Map, Struct] = [list("ack", "List", { sparse: 1 }, 0), map("ack", "Map", 0, 0, 1), () => schema];
const schema = struct("ack", "Structure", {}, ["list", "map", "struct"], [List, Map, Struct]);

const tr = TypeRegistry.for("ack");

it("stores and retrieves schema objects", () => {
expect(tr.getSchema("List")).toBe(List);
expect(tr.getSchema("Map")).toBe(Map);
expect(tr.getSchema("Structure")).toBe(schema);
});

it("has a helper method to retrieve a synthetic base exception", () => {
// the service namespace is appended to the synthetic prefix.
const err = error("smithyts.client.synthetic.ack", "UhOhServiceException", 0, [], [], Error);
const tr = TypeRegistry.for("smithyts.client.synthetic.ack");
expect(tr.getBaseException()).toEqual(err);
});
});
101 changes: 101 additions & 0 deletions packages/core/src/submodules/schema/TypeRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Schema as ISchema } from "@smithy/types";

import { ErrorSchema } from "./schemas/ErrorSchema";

/**
* A way to look up schema by their ShapeId values.
*
* @alpha
*/
export class TypeRegistry {
public static readonly registries = new Map<string, TypeRegistry>();

private constructor(
public readonly namespace: string,
private schemas: Map<string, ISchema> = new Map()
) {}

/**
* @param namespace - specifier.
* @returns the schema for that namespace, creating it if necessary.
*/
public static for(namespace: string): TypeRegistry {
if (!TypeRegistry.registries.has(namespace)) {
TypeRegistry.registries.set(namespace, new TypeRegistry(namespace));
}
return TypeRegistry.registries.get(namespace)!;
}

/**
* Adds the given schema to a type registry with the same namespace.
*
* @param shapeId - to be registered.
* @param schema - to be registered.
*/
public register(shapeId: string, schema: ISchema) {
const qualifiedName = this.normalizeShapeId(shapeId);
const registry = TypeRegistry.for(this.getNamespace(shapeId));
registry.schemas.set(qualifiedName, schema);
}

/**
* @param shapeId - query.
* @returns the schema.
*/
public getSchema(shapeId: string): ISchema {
const id = this.normalizeShapeId(shapeId);
if (!this.schemas.has(id)) {
throw new Error(`@smithy/core/schema - schema not found for ${id}`);
}
return this.schemas.get(id)!;
}

/**
* The smithy-typescript code generator generates a synthetic (i.e. unmodeled) base exception,
* because generated SDKs before the introduction of schemas have the notion of a ServiceBaseException, which
* is unique per service/model.
*
* This is generated under a unique prefix that is combined with the service namespace, and this
* method is used to retrieve it.
*
* The base exception synthetic schema is used when an error is returned by a service, but we cannot
* determine what existing schema to use to deserialize it.
*
* @returns the synthetic base exception of the service namespace associated with this registry instance.
*/
public getBaseException(): ErrorSchema | undefined {
for (const [id, schema] of this.schemas.entries()) {
if (id.startsWith("smithyts.client.synthetic.") && id.endsWith("ServiceException")) {
return schema as ErrorSchema;
}
}
return undefined;
}

/**
* @param predicate - criterion.
* @returns a schema in this registry matching the predicate.
*/
public find(predicate: (schema: ISchema) => boolean) {
return [...this.schemas.values()].find(predicate);
}

/**
* Unloads the current TypeRegistry.
*/
public destroy() {
TypeRegistry.registries.delete(this.namespace);
this.schemas.clear();
}

private normalizeShapeId(shapeId: string) {
if (shapeId.includes("#")) {
return shapeId;
}
return this.namespace + "#" + shapeId;
}

private getNamespace(shapeId: string) {
return this.normalizeShapeId(shapeId).split("#")[0];
}
}
12 changes: 12 additions & 0 deletions packages/core/src/submodules/schema/deref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Schema, SchemaRef } from "@smithy/types";

/**
* Dereferences a SchemaRef if needed.
* @internal
*/
export const deref = (schemaRef: SchemaRef): Schema => {
if (typeof schemaRef === "function") {
return schemaRef();
}
return schemaRef;
};
12 changes: 12 additions & 0 deletions packages/core/src/submodules/schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export * from "./deref";
export * from "./middleware/getSchemaSerdePlugin";
export * from "./schemas/ListSchema";
export * from "./schemas/MapSchema";
export * from "./schemas/OperationSchema";
export * from "./schemas/ErrorSchema";
export * from "./schemas/NormalizedSchema";
export * from "./schemas/Schema";
export * from "./schemas/SimpleSchema";
export * from "./schemas/StructureSchema";
export * from "./schemas/sentinels";
export * from "./TypeRegistry";
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
DeserializeHandlerOptions,
MetadataBearer,
MiddlewareStack,
Pluggable,
SerdeFunctions,
SerializeHandlerOptions,
} from "@smithy/types";

import { PreviouslyResolved } from "./schema-middleware-types";
import { schemaDeserializationMiddleware } from "./schemaDeserializationMiddleware";
import { schemaSerializationMiddleware } from "./schemaSerializationMiddleware";

/**
* @internal
*/
export const deserializerMiddlewareOption: DeserializeHandlerOptions = {
name: "deserializerMiddleware",
step: "deserialize",
tags: ["DESERIALIZER"],
override: true,
};

/**
* @internal
*/
export const serializerMiddlewareOption: SerializeHandlerOptions = {
name: "serializerMiddleware",
step: "serialize",
tags: ["SERIALIZER"],
override: true,
};

/**
* @internal
*/
export function getSchemaSerdePlugin<InputType extends object = any, OutputType extends MetadataBearer = any>(
config: PreviouslyResolved
): Pluggable<InputType, OutputType> {
return {
applyToStack: (commandStack: MiddlewareStack<InputType, OutputType>) => {
commandStack.add(schemaSerializationMiddleware(config), serializerMiddlewareOption);
commandStack.add(schemaDeserializationMiddleware(config), deserializerMiddlewareOption);
// `config` is fully resolved at the point of applying plugins.
// As such, config qualifies as SerdeContext.
config.protocol.setSerdeContext(config as SerdeFunctions);
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ClientProtocol, SerdeContext, UrlParser } from "@smithy/types";

/**
* @internal
*/
export type PreviouslyResolved = Omit<
SerdeContext & {
urlParser: UrlParser;
protocol: ClientProtocol<any, any>;
},
"endpoint"
>;
Loading
Loading