Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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);
});
});
102 changes: 102 additions & 0 deletions packages/core/src/submodules/schema/TypeRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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 active: TypeRegistry | null = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this property for? i don't think it's used anywhere in the current revision

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