From b5cac9722c0321dec9547faacc3f1016e797d8e9 Mon Sep 17 00:00:00 2001 From: Chris Barbour Date: Sun, 8 Dec 2024 16:40:57 +0000 Subject: [PATCH] adding support for apigateway and cognito --- package-lock.json | 57 ++++--- package.json | 8 +- src/index.ts | 2 +- src/oas.ts | 10 +- src/open-api-builder.ts | 286 +++++++++++++++++++++++++++++++ src/path-builder.ts | 68 ++++---- src/schema-type.ts | 364 ---------------------------------------- test/schema-example.ts | 163 ++++++++++-------- test/sdk.spec.ts | 3 +- test/validator.spec.ts | 131 --------------- 10 files changed, 469 insertions(+), 623 deletions(-) create mode 100644 src/open-api-builder.ts delete mode 100644 src/schema-type.ts delete mode 100644 test/validator.spec.ts diff --git a/package-lock.json b/package-lock.json index 355e347..48fdae7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@types/aws-lambda": "^8.10.40", "@types/ejs": "^3.1.5", "@types/jest": "^24.9.1", - "@types/node": "^18.18.5", + "@types/node": "^20.17.9", "@typescript-eslint/eslint-plugin": "^4.24.0", "@typescript-eslint/parser": "^4.24.0", "eslint": "^6.8.0", @@ -37,16 +37,18 @@ "jest-junit": "^10.0.0", "ts-jest": "^29.1.1", "tslib": "^2.6.2", - "typescript": "^4.7.4", + "typescript": "^5.7.2", "zod-to-json-schema": "^3.23.5" }, "peerDependencies": { "@aws-lambda-powertools/logger": "^2", - "@aws-lambda-powertools/parser": "^", + "@aws-lambda-powertools/parser": "^2", "@hexlabs/http-api-ts": "^2", "@hexlabs/lambda-api-ts": "^0", "@middy/core": "^5", - "@middy/http-router": "^5" + "@middy/http-router": "^5", + "zod": "^3", + "zod-to-json-schema": "^3" } }, "node_modules/@ampproject/remapping": { @@ -1694,9 +1696,13 @@ "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" }, "node_modules/@types/node": { - "version": "18.18.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.5.tgz", - "integrity": "sha512-4slmbtwV59ZxitY4ixUZdy1uRLf9eSIvBWPQxNjhHYWEtn0FryfKpyS2cvADYXTayWdKEIsJengncrVvkI4I6A==" + "version": "20.17.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", + "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/@types/normalize-package-data": { "version": "2.4.0", @@ -7864,17 +7870,24 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -8152,7 +8165,6 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "dev": true, "license": "MIT", "peer": true, "funding": { @@ -9438,9 +9450,12 @@ "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==" }, "@types/node": { - "version": "18.18.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.5.tgz", - "integrity": "sha512-4slmbtwV59ZxitY4ixUZdy1uRLf9eSIvBWPQxNjhHYWEtn0FryfKpyS2cvADYXTayWdKEIsJengncrVvkI4I6A==" + "version": "20.17.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", + "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", + "requires": { + "undici-types": "~6.19.2" + } }, "@types/normalize-package-data": { "version": "2.4.0", @@ -13956,9 +13971,14 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==" + }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "update-browserslist-db": { "version": "1.0.13", @@ -14165,7 +14185,6 @@ "version": "3.23.8", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", - "dev": true, "peer": true }, "zod-to-json-schema": { diff --git a/package.json b/package.json index 55ce39d..91fc3f1 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "build:esm": "tsc --project tsconfig.json && echo '{\"type\": \"module\"}' > dist/esm/package.json", "build:cjs": "tsc --project tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json", "build": "npm run build:esm && npm run build:cjs && chmod +x ./dist/esm/cli/index.js", - "generate:middy": "./dist/esm/cli/index.js generate --template middy --apiVersion 1.0.0 $(pwd)/test/schema-example.ts", - "generate:hexlabs": "./dist/esm/cli/index.js generate --template hexlabs --apiVersion 1.0.0 $(pwd)/test/schema-example.ts", + "generate:middy": "npm run build && ./dist/esm/cli/index.js generate --template middy --apiVersion 1.0.0 $(pwd)/test/schema-example.ts", + "generate:hexlabs": "npm run build && ./dist/esm/cli/index.js generate --template hexlabs --apiVersion 1.0.0 $(pwd)/test/schema-example.ts", "test": "npm run generate:middy && jest --ci --runInBand --coverage --reporters=default --reporters=jest-junit --passWithNoTests", "lint": "eslint **/*.ts" }, @@ -116,7 +116,7 @@ "@types/aws-lambda": "^8.10.40", "@types/ejs": "^3.1.5", "@types/jest": "^24.9.1", - "@types/node": "^18.18.5", + "@types/node": "^20.17.9", "@typescript-eslint/eslint-plugin": "^4.24.0", "@typescript-eslint/parser": "^4.24.0", "eslint": "^6.8.0", @@ -126,7 +126,7 @@ "jest-junit": "^10.0.0", "ts-jest": "^29.1.1", "tslib": "^2.6.2", - "typescript": "^4.7.4", + "typescript": "^5.7.2", "zod-to-json-schema": "^3.23.5" }, "dependencies": { diff --git a/src/index.ts b/src/index.ts index d864fac..6a8a592 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './hydra.js'; export * from './oas.js'; -export * from './schema-type.js'; +export * from './open-api-builder.js'; export * from './validator.js'; export * from './schema-builder'; diff --git a/src/oas.ts b/src/oas.ts index eadd74e..a77bf2d 100644 --- a/src/oas.ts +++ b/src/oas.ts @@ -1,4 +1,5 @@ import {JSONSchema} from 'json-schema-to-typescript' +import { JsonSchema7Type } from 'zod-to-json-schema'; export interface OAS { openapi: string; @@ -11,8 +12,8 @@ export interface OAS { externalDocs?: OASExternalDocs; } -export interface OASComponents { - schemas?: { [key: string]: JSONSchema }; +export type OASComponents = { + schemas?: { [key: string]: JSONSchema | JsonSchema7Type; }; responses?: { [key: string]: OASResponse | OASRef }; parameters?: { [key: string]: OASParameter | OASRef }; examples?: { [key: string]: any | OASRef }; @@ -21,13 +22,15 @@ export interface OASComponents { securitySchemes?: { [key: string]: OASSecurityScheme | OASRef }; links?: { [key: string]: any | OASRef }; callbacks?: { [key: string]: OASPath | OASRef }; + ['x-amazon-apigateway-integrations']?: any; } export type OASSecurityScheme = { description?: string; } & ({ type: "oauth2"; - flows: OASOAuthFlows; + flows?: OASOAuthFlows; + ['x-amazon-apigateway-authorizer']?: any; } | { type: "openIdConnect"; openIdConnectUrl: string; @@ -125,6 +128,7 @@ export interface OASOperation { deprecated?: boolean; security?: OASSecurity[]; servers?: OASServer[]; + ['x-amazon-apigateway-integration']?: OASRef | any; } export interface OASRequestBody { diff --git a/src/open-api-builder.ts b/src/open-api-builder.ts new file mode 100644 index 0000000..563921b --- /dev/null +++ b/src/open-api-builder.ts @@ -0,0 +1,286 @@ +import { JSONSchema } from 'json-schema-to-typescript'; +import { + OAS, OASComponents, OASEncoding, + OASInfo, OASMedia, OASOAuthFlows, OASOperation, OASParameter, OASPath, + OASRef, + OASResponse, + OASSecurity, + OASSecurityScheme +} from './oas'; +import { PathBuilder } from './path-builder'; + +export type OASParts = { + schemes: string[]; + apiIntegrations: string[]; +} + +export class OpenApiSpecificationBuilder< + S extends {components: {schemas?: any, responses?: Record}}, + T extends OASParts = { schemes: [], apiIntegrations: [] } +> { + + parts: OASParts = { + schemes: [], + apiIntegrations: [] + } + + private constructor(public oas: OAS) { + } + + private _defaultResponses: OASOperation["responses"] = {}; + + build(): OAS { + return this.oas; + } + + withAWSCognitoSecurityScheme(key: K, userPoolEndpoint: string, clientId: string, identitySource = '$request.header.Authorization') + : OpenApiSpecificationBuilder { + const scheme: OASSecurityScheme = { + type: "oauth2", + "x-amazon-apigateway-authorizer": { + type: "jwt", + jwtConfiguration: { + issuer: userPoolEndpoint, + audience: [clientId] + }, + identitySource: identitySource + } + }; + this.parts.schemes.push(key); + return this.addComponent('securitySchemes', () => ({ [key]: scheme })) as any; + } + + withAWSLambdaApiGatewayIntegration(key: K, functionUri: string, payloadFormatVersion: '1.0' | '2.0' = '1.0', passthroughBehavior: 'when_no_templates' | 'when_no_match' | 'never' = 'when_no_templates') + : OpenApiSpecificationBuilder { + const scheme = { + type: "aws_proxy", + httpMethod: "POST", + uri: functionUri, + passthroughBehavior: passthroughBehavior, + payloadFormatVersion: payloadFormatVersion + }; + this.parts.apiIntegrations.push(key); + return this.addComponent('x-amazon-apigateway-integrations',() => ({ [key]: scheme })) as any; + } + + get securitySchemes(): Record OASSecurity> { + const schemes = this.parts.schemes.reduce((prev, key) => { + return {...prev, [key]: ((scopes?: string[]) => { + return { [key]: scopes ?? [] } + })} + },{} as any) as any; + return schemes; + } + + get awsLambdaApiGatewayIntegration(): Record { + const schemes = this.parts.schemes.reduce((prev, key) => { + return {...prev, [key]: {"$ref": `#/components/x-amazon-apigateway-integrations/${key}`}} + },{} as any) as any; + return schemes; + } + + withPath(name: string, builder: (path: PathBuilder, oas: this) => PathBuilder): this { + const pathBuilder = builder(new PathBuilder('/' + name), this); + this.oas.paths = {...(this.oas.paths ?? {}), ...pathBuilder.path }; + return this; + } + + jsonContent( + key: K, + example?: any, + examples?: Record, + encoding?: { [key: string]: OASEncoding } + ): { 'application/json': OASMedia } { + return {'application/json': this.media(key, example, examples, encoding)}; + } + + textContent(example?: string, examples?: Record, encoding?: { [key: string]: OASEncoding }): { 'application/text': OASMedia } { + return {'application/text': {schema: { type: 'string' }, example, examples, encoding}}; + } + + response(content: OASResponse['content'], description = '', headers: string[] = []): OASResponse { + return {description, content, headers: headers.reduce((prev, name) => ({ ...prev, [name]: {required: true, schema: { type: 'string' }} }), {})} + } + + textResponse( + description = '', + headers: string[] = [], + example?: any, + examples?: Record, + encoding?: { [key: string]: OASEncoding } + ): OASResponse { + return this.response(this.textContent(example, examples, encoding), description, headers); + } + + jsonResponse( + key: K, + description = '', + headers: string[] = [], + example?: any, + examples?: Record, + encoding?: { [key: string]: OASEncoding } + ): OASResponse { + return this.response(this.jsonContent(key, example, examples, encoding), description, headers); + } + + responseReference(key: K): { '$ref': K extends string ? `#/components/responses/${K}` : string } { + return this.componentReference('responses', key); + } + + media( + key: K, + example?: any, + examples?: Record, + encoding?: { [key: string]: OASEncoding } + ): OASMedia { + return {schema: this.reference(key), example, examples, encoding} + } + + basicAuthScheme(description = ''): OASSecurityScheme { + return { + type: 'http', + scheme: 'basic', + description, + } + } + + apiKeyScheme(in_: "header" | "query" | "cookie", name: string, description = ''): OASSecurityScheme { + return { + type: 'apiKey', + in: in_, + name, + description, + } + } + + bearerScheme(description = ''): OASSecurityScheme { + return { + type: 'http', + scheme: 'bearer', + description + } + } + + oAuth2Scheme(flows: OASOAuthFlows, description = ''): OASSecurityScheme { + return { + type: 'oauth2', + flows, + description + } + } + + openIdConnectScheme(openIdConnectUrl: string, description = ''): OASSecurityScheme { + return { + type: 'openIdConnect', + openIdConnectUrl, + description + } + } + + query(name: string, required = true, multiple = false): OASParameter { + return this.parameter(name, "query", required, multiple ? {type: "array", items: {type: "string"}} : {type: "string"}) + } + + header(name: string, required = true): OASParameter { + return this.parameter(name, "header", required) + } + + + parameter(name: string, location: OASParameter['in'], required = true, schema: JSONSchema = {type: "string"}): OASParameter { + return { + name, + in: location, + required, + schema + } + } + + reference(key: K): { '$ref': K extends string ? `#/components/schemas/${K}` : string } { + return this.componentReference('schemas', key); + } + + componentReference(componentKey: K, key: T): { '$ref': K extends string ? T extends string ? `#/components/${K}/${T}` : string : string } { + return {'$ref': `#/components/${componentKey as string}/${key as string}`} as any; + } + + defaultResponses(itemBuilder: (builder: this) => Exclude): this { + this._defaultResponses = itemBuilder(this); + return this + } + + addComponent OASComponents[K]>(location: K, itemBuilder: B): B extends (builder: any) => infer R ? OpenApiSpecificationBuilder : never { + const item = itemBuilder(this); + const current = this.oas.components ?? {}; + if (typeof item === "object") { + if (Array.isArray(item)) { + this.oas = { + ...this.oas, + components: {...current, [location]: [...(current[location] as unknown as any[] ?? []), ...item]} + }; + } else { + this.oas = { + ...this.oas, + components: {...current, [location]: {...(current[location] as any ?? {}), ...(item as any)}} + }; + } + } else { + this.oas = {...this.oas, components: {...current, [location]: item}}; + } + return this as any; + } + + private modifyOperationWithDefaultResponses(operation?: OASOperation): OASOperation | undefined { + if(operation) { + const { responses, ...rest } = operation; + return {...rest, responses: { ...this._defaultResponses, ...responses } } + } + } + private addDefaultResponsesToPath(path: OASPath | OASRef): OASPath | OASRef { + if((path as OASRef).$ref) return path; + const { + get, + put, + post, + delete: deleteOp, + options, + head, + patch, + trace, + ...rest + } = path as OASPath; + return { + ...rest, + ...(get ? {get: this.modifyOperationWithDefaultResponses(get)} : {}), + ...(put ? {put: this.modifyOperationWithDefaultResponses(put)} : {}), + ...(post ? {post: this.modifyOperationWithDefaultResponses(post)} : {}), + ...(deleteOp ? {delete: this.modifyOperationWithDefaultResponses(deleteOp)} : {}), + ...(options ? {options: this.modifyOperationWithDefaultResponses(options)} : {}), + ...(head ? {head: this.modifyOperationWithDefaultResponses(head)} : {}), + ...(patch ? {patch: this.modifyOperationWithDefaultResponses(patch)} : {}), + ...(trace ? {trace: this.modifyOperationWithDefaultResponses(trace)} : {}), + ...rest + } + } + + private addDefaultResponses(paths: OAS["paths"]): OAS["paths"] { + return Object.keys(paths).reduce((previousValue, path) => ({ ...previousValue, [path]: this.addDefaultResponsesToPath(paths[path])}), {}) + } + + add(location: K, itemBuilder: (builder: this) => OAS[K]): this { + const item = itemBuilder(this); + if (typeof item === "object") { + if (Array.isArray(item)) { + this.oas = {...this.oas, [location]: [...(this.oas[location] as any[] ?? []), ...item]}; + } else { + this.oas = {...this.oas, [location]: {...(this.oas[location] as any ?? {}), ...(location === "paths" ? this.addDefaultResponses(item as any) : (item as any))}}; + } + } else { + this.oas = {...this.oas, [location]: item}; + } + return this; + } + + static create } }>(schemas: S, info: OASInfo): OpenApiSpecificationBuilder { + return new OpenApiSpecificationBuilder({openapi: '3.0.0', info, paths: {}, ...schemas as any}); + } +} diff --git a/src/path-builder.ts b/src/path-builder.ts index 2002273..c6245c3 100644 --- a/src/path-builder.ts +++ b/src/path-builder.ts @@ -1,57 +1,65 @@ import { OASOperation, OASParameter, OASPath, OASServer } from './oas'; -import { OpenApiSpecificationBuilder } from './schema-type'; -export type MethodBuilder = (o: SCHEMA) => OASOperation -export type PathBuilderFor = SCHEMA extends OpenApiSpecificationBuilder ? (path: PathBuilder) => PathBuilder : never; -export class PathBuilder}}> { - private path: Record = {}; - constructor(private readonly name: string, private readonly builder: OpenApiSpecificationBuilder) { - const matches = name.matchAll(/{([^/]+)}/); - this.set('parameters', [...matches].map(([, match]) => builder.path(match))); +export class PathBuilder { + public path: Record = {}; + + constructor(private readonly name: string) { + const matches = name.matchAll(/{([^/]+)}/g); + this.set('parameters', [...matches].map((match) => { + const parameter: OASParameter = { + name: match[1], + in: 'path', + required: true, + schema: {type: "string"} + }; + return parameter; + })); } - child(name: string, builder: (paths: PathBuilder) => PathBuilder): this { - const pathBuilder = new PathBuilder(this.name + '/' + name, this.builder); + childPath(name: string, builder: (paths: PathBuilder) => PathBuilder): this { + const pathBuilder = new PathBuilder(this.name + '/' + name); const childPaths = builder(pathBuilder); this.path = {...this.path, ...childPaths.path}; return this; } - resource(name: string, builder: (paths: PathBuilder) => PathBuilder): this { - return this.child(`{${name}}`, builder); + resource(name: string, builder: (paths: PathBuilder) => PathBuilder): this { + return this.childPath(`{${name}}`, builder); } private set(key: K, value: OASPath[K]): this { - this.path[this.name] = { ...this.path[this.name], [key]: value }; + this.path[this.name] = { ...(this.path[this.name] ?? {}), [key]: value }; return this; } summary(summary: string): this { return this.set('summary', summary); } description(description: string): this { return this.set('description', description); } - servers(...servers: OASServer[]): this { return this.set('servers', servers); } + servers(...servers: OASServer[]): this { + return this.set('servers', servers); + } parameters(...parameters: OASParameter[]): this { return this.set('parameters', parameters); } - get(operationId: string, builder: MethodBuilder>): this { - return this.set('get', { ...builder(this.builder), operationId }); + get(operationId: string, operation: OASOperation): this { + return this.set('get', { ...operation, operationId }); } - put(operationId: string, builder: MethodBuilder>): this { - return this.set('put', { ...builder(this.builder), operationId }); + put(operationId: string, operation: OASOperation): this { + return this.set('put', { ...operation, operationId }); } - post(operationId: string, builder: MethodBuilder>): this { - return this.set('post', { ...builder(this.builder), operationId }); + post(operationId: string, operation: OASOperation): this { + return this.set('post', { ...operation, operationId }); } - delete(operationId: string, builder: MethodBuilder>): this { - return this.set('delete', { ...builder(this.builder), operationId }); + delete(operationId: string, operation: OASOperation): this { + return this.set('delete', { ...operation, operationId }); } - options(operationId: string, builder: MethodBuilder>): this { - return this.set('options', { ...builder(this.builder), operationId }); + options(operationId: string, operation: OASOperation): this { + return this.set('options', { ...operation, operationId }); } - head(operationId: string, builder: MethodBuilder>): this { - return this.set('head', { ...builder(this.builder), operationId }); + head(operationId: string, operation: OASOperation): this { + return this.set('head', { ...operation, operationId }); } - patch(operationId: string, builder: MethodBuilder>): this { - return this.set('patch', { ...builder(this.builder), operationId }); + patch(operationId: string, operation: OASOperation): this { + return this.set('patch', { ...operation, operationId }); } - trace(operationId: string, builder: MethodBuilder>): this { - return this.set('trace', { ...builder(this.builder), operationId }); + trace(operationId: string, operation: OASOperation): this { + return this.set('trace', { ...operation, operationId }); } } diff --git a/src/schema-type.ts b/src/schema-type.ts deleted file mode 100644 index 9f5fcf0..0000000 --- a/src/schema-type.ts +++ /dev/null @@ -1,364 +0,0 @@ -import {JSONSchema} from "json-schema-to-typescript"; -import { - OASEncoding, - OASInfo, - OASMedia, - OASOAuthFlows, OASOperation, - OASParameter, OASPath, OASRef, - OASResponse, - OASSecurityScheme, -} from "./oas"; -import {OAS, OASComponents} from "./oas.js"; -import { PathBuilder } from './path-builder'; - -type UnionToTuple = ( - ( - ( - T extends any - ? (t: T) => T - : never - ) extends infer U - ? (U extends any - ? (u: U) => any - : never - ) extends (v: infer V) => any - ? V - : never - : never - ) extends (_: any) => infer W - ? [...UnionToTuple>, W] - : [] - ); - -type Ref = R extends `${infer Start}/${infer Rest}` ? (Start extends '#' ? Ref : (Start extends keyof S ? Ref : never)) : (R extends keyof S ? S[R] : never) -type FromProps = S extends {properties: any} ? { -readonly [K in keyof S['properties']]: Schema } : {}; -type FromArray = S extends {items: readonly any[]} ? FromTuple : S extends {items: any} ? Array> : never; -type FromTuple = T extends readonly [infer HEAD, ...infer TAIL] ? [Schema, ...FromTuple]: []; -type AllOf = T extends readonly [infer HEAD] ? Schema : T extends readonly [infer HEAD, ...infer TAIL] ? Schema & AllOf: never -type AnyOf = T extends readonly [infer HEAD] ? Schema : T extends readonly [infer HEAD, ...infer TAIL] ? Schema | AnyOf: never -type AdditionalProps = S extends { additionalProperties: false } ? T : (S extends { additionalProperties: true } ? T & {[key: string]: unknown} : (S extends {additionalProperties: any} ? T & {[key: string]: Schema} : T & {[key: string]: unknown})); -type Required = S extends { required: readonly [...infer R] } ? { [K in keyof T]-?: K extends KeysIn ? T[K] : T[K] | undefined}: Partial -type KeysIn = T extends [infer HEAD] ? HEAD : T extends [infer HEAD, ...infer TAIL] ? HEAD | KeysIn : never; -export type Schema = S extends {type: 'string'} ? (S extends {const: infer C} ? C : string) : - S extends { type: 'number'} ? number : - S extends {type: 'array'} ? FromArray : - S extends { '$ref': string } ? Schema, O> : - S extends { allOf: any } ? AllOf : - S extends { anyOf: any } ? AnyOf : AdditionalProps, S>, S, O> - - -export type SchemaType = Record; - -type FilteredKeys = { [P in keyof T]: T[P] extends never ? never : P }[keyof T]; - -type Stripped = { [Q in FilteredKeys]: T[Q] }; - -type Combine = A extends undefined ? ( B extends undefined ? never : B) : ( B extends undefined ? A : A & B) -type ObjectSchema| undefined = undefined, O extends Record | undefined= undefined> = Stripped<{type: 'object', title: string, additionalProperties: A extends undefined ? never : A, required: R extends undefined ? never: UnionToTuple, properties: Combine }>; -type ArraySchema = Stripped<{type: 'array', title: string, additionalItems: A extends undefined ? never : A, items: R }>; - -export class SchemaBuilder { - - private constructor(private readonly schemaParent: T) {} - - build(): T { - return this.schemaParent - } - - object | undefined = undefined, O extends Record | undefined = undefined, A extends boolean | JSONSchema | undefined = false, P extends JSONSchema | undefined = undefined> - (required: R = undefined as unknown as R, optional: O = undefined as unknown as O, additionalProperties: A = false as unknown as A, title = undefined, parts: P = undefined as unknown as P): ObjectSchema { - const requireds = Object.keys(required ?? {}); - return { - type: 'object', - title, - additionalProperties, - ...(requireds.length > 0 ? {required: requireds}: {}) , - properties: { - ...required, - ...optional - }, - ...parts - } as any - } - - array - (items: R, additionalItems: A = undefined as unknown as A, title = undefined, parts: P = undefined as unknown as P): ArraySchema { - return { - type: 'array', - title, - items, - additionalItems, - ...parts - } as any - } - - anyOf(...items: S): { anyOf: S } { return { anyOf: items } as any; } - allOf(...items: S): { allOf: S } { return { allOf: items } as any; } - oneOf(...items: S): { oneOf: S } { return { oneOf: items } as any; } - - list(...items: T): T { - return items; - } - - stringWith

(parts: P): Combine<{type: 'string'}, P> { - return {type: 'string', ...(parts ?? {})} as any; - } - numberWith

(parts: P): Combine<{type: 'number'}, P> { - return {type: 'number', ...(parts ?? {})} as any; - } - integerWith

(parts: P): Combine<{type: 'integer'}, P> { - return {type: 'integer', ...(parts ?? {})} as any; - } - booleanWith

(parts: P): Combine<{type: 'boolean'}, P> { - return {type: 'boolean', ...(parts ?? {})} as any; - } - nullWith

(parts: P): Combine<{type: 'null'}, P> { - return {type: 'null', ...(parts ?? {})} as any; - } - - string(): {type: 'string'} { return {type: 'string'} } - number(): {type: 'number'} { return {type: 'number'} } - integer(): {type: 'integer'} { return {type: 'integer'} } - boolean(): {type: 'boolean'} { return {type: 'boolean'} } - null(): {type: 'null'} { return {type: 'null'} } - - reference(key: K): { '$ref': K extends string ? `#/components/schemas/${K}` : string } { - return { '$ref': `#/components/schemas/${key as string}` } as any; - } - - hydraOperation(): ObjectSchema, method: {type: 'string'}}, {expects: {type: 'string'}, returns: {type: 'string'}}> { - return this.object({statusCodes: this.array(this.string()), method: this.string()}, {expects: this.string(), returns: this.string()}); - } - - hydraResource(reference: K): T['components']['schemas'][K] extends {type: 'object'} ? T['components']['schemas'][K] & {type: 'object', required: ['@id', '@operation'], properties: {'@id': {type: 'string'}, '@operation': ReturnType['hydraOperation']>}} : never{ - const other: JSONSchema = this.schemaParent.components.schemas[reference]; - if(!Object.keys(this.schemaParent.components.schemas).includes('HydraOperation')) throw new Error('HydraOperation must first be defined in the schema') - if(other.type === 'object') { - return { - ...other, - required: [...(other.required as string[] ?? []), '@id', '@operation'], - properties: { - ...other.properties, - '@id': this.string(), - '@operation': this.reference('HydraOperation') - } - } as any; - } - throw new Error('Must be an object to map to hydra resource') - } - - hydraCollection(reference: K): T['components']['schemas'][K] extends {type: 'object'} ? {type: 'object', required: ['@id', '@operation', 'member'], properties: {'@id': {type: 'string'}, '@operation': ReturnType['hydraOperation']>, 'member': ArraySchema}} : never{ - const other: JSONSchema = this.schemaParent.components.schemas[reference]; - if(other.type === 'object') { - if(!Object.keys(this.schemaParent.components.schemas).includes('HydraOperation')) throw new Error('HydraOperation must first be defined in the schema') - return this.object({'@id': this.string(), '@operation': this.reference('HydraOperation'), member: this.array(other)}, {next: this.string()}) as any - } - throw new Error('Must be an object to map to hydra resource') - } - - add) => any>(name: K, schema: B): B extends (s: any) => infer S ? SchemaBuilder: never { - const s = schema(new SchemaBuilder(this.schemaParent)); - return new SchemaBuilder({components: {schemas: {...this.schemaParent.components.schemas, [name]: {...s, title: name } }}}) as any; - } - - static create(): SchemaBuilder<{components:{schemas: {}}}> { - return new SchemaBuilder({components:{schemas: {}}}); - } -} - -export class OpenApiSpecificationBuilder}}> { - private constructor(public oas: OAS) { - } - - private _defaultResponses: OASOperation["responses"] = {}; - - build(): OAS { - return this.oas; - } - - jsonContent( - key: K, - example?: Schema, - examples?: Record>, - encoding?: { [key: string]: OASEncoding } - ): { 'application/json': OASMedia } { - return {'application/json': this.media(key, example, examples, encoding)}; - } - - textContent(example?: string, examples?: Record, encoding?: { [key: string]: OASEncoding }): { 'application/text': OASMedia } { - return {'application/text': {schema: SchemaBuilder.create().string(), example, examples, encoding}}; - } - - response(content: OASResponse['content'], description = '', headers: string[] = []): OASResponse { - return {description, content, headers: headers.reduce((prev, name) => ({ ...prev, [name]: {required: true, schema: { type: 'string' }} }), {})} - } - - responseReference(key: K): { '$ref': K extends string ? `#/components/responses/${K}` : string } { - return this.componentReference('responses', key); - } - - media( - key: K, - example?: Schema, - examples?: Record>, - encoding?: { [key: string]: OASEncoding } - ): OASMedia { - return {schema: this.reference(key), example, examples, encoding} - } - - basicAuthScheme(description = ''): OASSecurityScheme { - return { - type: 'http', - scheme: 'basic', - description, - } - } - - apiKeyScheme(in_: "header" | "query" | "cookie", name: string, description = ''): OASSecurityScheme { - return { - type: 'apiKey', - in: in_, - name, - description, - } - } - - bearerScheme(description = ''): OASSecurityScheme { - return { - type: 'http', - scheme: 'bearer', - description - } - } - - oAuth2Scheme(flows: OASOAuthFlows, description = ''): OASSecurityScheme { - return { - type: 'oauth2', - flows, - description - } - } - - openIdConnectScheme(openIdConnectUrl: string, description = ''): OASSecurityScheme { - return { - type: 'openIdConnect', - openIdConnectUrl, - description - } - } - - query(name: string, required = true, multiple = false): OASParameter { - return this.parameter(name, "query", required, multiple ? {type: "array", items: {type: "string"}} : {type: "string"}) - } - - header(name: string, required = true): OASParameter { - return this.parameter(name, "header", required) - } - - path(name: string): OASParameter { - return this.parameter(name, "path", true) - } - - parameter(name: string, location: OASParameter['in'], required = true, schema: JSONSchema = {type: "string"}): OASParameter { - return { - name, - in: location, - required, - schema - } - } - - reference(key: K): { '$ref': K extends string ? `#/components/schemas/${K}` : string } { - return this.componentReference('schemas', key); - } - - componentReference(componentKey: K, key: T): { '$ref': K extends string ? T extends string ? `#/components/${K}/${T}` : string : string } { - return {'$ref': `#/components/${componentKey as string}/${key as string}`} as any; - } - - defaultResponses(itemBuilder: (builder: this) => Exclude): this { - this._defaultResponses = itemBuilder(this); - return this - } - - addComponent OASComponents[K]>(location: K, itemBuilder: B): B extends (builder: any) => infer R ? OpenApiSpecificationBuilder : never { - const item = itemBuilder(this); - const current = this.oas.components ?? {}; - if (typeof item === "object") { - if (Array.isArray(item)) { - this.oas = { - ...this.oas, - components: {...current, [location]: [...(current[location] as unknown as any[] ?? []), ...item]} - }; - } else { - this.oas = { - ...this.oas, - components: {...current, [location]: {...(current[location] as any ?? {}), ...(item as any)}} - }; - } - } else { - this.oas = {...this.oas, components: {...current, [location]: item}}; - } - return this as any; - } - - private modifyOperationWithDefaultResponses(operation?: OASOperation): OASOperation | undefined { - if(operation) { - const { responses, ...rest } = operation; - return {...rest, responses: { ...this._defaultResponses, ...responses } } - } - } - private addDefaultResponsesToPath(path: OASPath | OASRef): OASPath | OASRef { - if((path as OASRef).$ref) return path; - const { - get, - put, - post, - delete: deleteOp, - options, - head, - patch, - trace, - ...rest - } = path as OASPath; - return { - ...rest, - ...(get ? {get: this.modifyOperationWithDefaultResponses(get)} : {}), - ...(put ? {put: this.modifyOperationWithDefaultResponses(put)} : {}), - ...(post ? {post: this.modifyOperationWithDefaultResponses(post)} : {}), - ...(deleteOp ? {delete: this.modifyOperationWithDefaultResponses(deleteOp)} : {}), - ...(options ? {options: this.modifyOperationWithDefaultResponses(options)} : {}), - ...(head ? {head: this.modifyOperationWithDefaultResponses(head)} : {}), - ...(patch ? {patch: this.modifyOperationWithDefaultResponses(patch)} : {}), - ...(trace ? {trace: this.modifyOperationWithDefaultResponses(trace)} : {}), - ...rest - } - } - - private addDefaultResponses(paths: OAS["paths"]): OAS["paths"] { - return Object.keys(paths).reduce((previousValue, path) => ({ ...previousValue, [path]: this.addDefaultResponsesToPath(paths[path])}), {}) - } - - route(name: string, builder: (paths: PathBuilder) => PathBuilder): this { - - return this; - } - - add(location: K, itemBuilder: (builder: this) => OAS[K]): this { - const item = itemBuilder(this); - if (typeof item === "object") { - if (Array.isArray(item)) { - this.oas = {...this.oas, [location]: [...(this.oas[location] as any[] ?? []), ...item]}; - } else { - this.oas = {...this.oas, [location]: {...(this.oas[location] as any ?? {}), ...(location === "paths" ? this.addDefaultResponses(item as any) : (item as any))}}; - } - } else { - this.oas = {...this.oas, [location]: item}; - } - return this; - } - - static create } }>(schemas: S, info: OASInfo): OpenApiSpecificationBuilder { - return new OpenApiSpecificationBuilder({openapi: '3.0.0', info, paths: {}, ...schemas as any}); - } -} diff --git a/test/schema-example.ts b/test/schema-example.ts index fe863ff..69882b8 100644 --- a/test/schema-example.ts +++ b/test/schema-example.ts @@ -1,5 +1,4 @@ -import {OpenApiSpecificationBuilder, OASServer} from "../src"; -import { ZodSchemaBuilder } from '../src/schema-builder'; +import { OASServer, OpenApiSpecificationBuilder, ZodSchemaBuilder } from "../src"; import * as model from './model'; const servers: OASServer[] = [ @@ -22,71 +21,97 @@ const schemas = ZodSchemaBuilder.create() .add("ChickenCreateRequest", model.ChickenCreateRequest) .build() -export default OpenApiSpecificationBuilder -.create(schemas, { title: 'Chicken Store API', version: '1.0.0'}) -.add('servers', () => servers) - .defaultResponses(o => ({ - 200: o.response(o.textContent()) - })) -.addComponent('securitySchemes', o => ({ Auth: o.openIdConnectScheme('.well-known/xyz')})) -.add('paths', o => ({ - '/chicken': { - get: { - operationId: 'getChickens', - security: [{Auth: ['read', 'write', 'admin']}], - responses: { - 200: o.response(o.jsonContent('ChickenCollection'), 'The Flock'), +export default OpenApiSpecificationBuilder.create(schemas, { title: 'Chicken Store API', version: '1.0.0'}) + .add('servers', oas => servers) + .withAWSCognitoSecurityScheme('Cognito', '${ENDPOINT}', '${CLIENT}') + .withAWSLambdaApiGatewayIntegration('apiHandler', '${API_HANDLER}') + .withPath('chicken', (path, oas) => + path.get('getChickens', { + 'x-amazon-apigateway-integration': oas.awsLambdaApiGatewayIntegration.apiHandler, + security: [oas.securitySchemes.Cognito()], + responses: { + 200: oas.jsonResponse('ChickenCollection') + } } - }, - post: { - operationId: 'createChicken', - requestBody: { - description: 'A Chicken', - content: o.jsonContent('ChickenCreateRequest') - }, - responses: { - 201: { - description: 'The Flock', - content: o.jsonContent('ChickenCollection') - }, - 400: {description: 'Bad Request', content: o.textContent()} - } - } - }, - '/chicken/{chickenId}': { - get: { - operationId: 'getChicken', - parameters: [ - o.path('chickenId'), - o.query('someQuery'), - o.query('someOtherQuery', false, true) - ], - responses: { - 200: {content: o.jsonContent('Chicken'), description: 'The Chicken'}, - } - }, - put: { - operationId: 'updateChicken', - parameters: [ - o.path('chickenId'), - o.query('someQuery', true, true) - ], - requestBody: { - description: 'A Chicken', - content: o.jsonContent('ChickenCreateRequest') - }, - responses: { - 200: o.response({...o.jsonContent('Chicken'), ...o.textContent()}, 'The Chicken'), - 404: o.response( o.textContent(), 'Not Found'), - } - }, - delete: { - operationId: 'deleteChicken', - parameters: [ - o.path('chickenId'), - o.header('X-Encryption-Key') - ] - }, - } -})) -.build(); + ).resource('chickenId', path => + path.put('upsertChicken', { + 'x-amazon-apigateway-integration': oas.awsLambdaApiGatewayIntegration.apiHandler, + security: [oas.securitySchemes.Cognito()], + requestBody: { content: oas.jsonContent('Chicken') }, + responses: { + 200: oas.jsonResponse('ChickenCollection') + } + }) + ) + ).build(); + + + +// OpenApiSpecificationBuilder +// .create(schemas, { title: 'Chicken Store API', version: '1.0.0'}) +// .add('servers', () => servers) +// .defaultResponses(o => ({ +// 200: o.response(o.textContent()) +// })) +// .addComponent('securitySchemes', o => ({ Auth: o.openIdConnectScheme('.well-known/xyz')})) +// .add('paths', o => ({ +// '/chicken': { +// get: { +// operationId: 'getChickens', +// security: [], +// responses: { +// 200: o.response(o.jsonContent('ChickenCollection'), 'The Flock'), +// } +// }, +// post: { +// operationId: 'createChicken', +// requestBody: { +// description: 'A Chicken', +// content: o.jsonContent('ChickenCreateRequest') +// }, +// responses: { +// 201: { +// description: 'The Flock', +// content: o.jsonContent('ChickenCollection') +// }, +// 400: {description: 'Bad Request', content: o.textContent()} +// } +// } +// }, +// '/chicken/{chickenId}': { +// get: { +// operationId: 'getChicken', +// parameters: [ +// o.path('chickenId'), +// o.query('someQuery'), +// o.query('someOtherQuery', false, true) +// ], +// responses: { +// 200: {content: o.jsonContent('Chicken'), description: 'The Chicken'}, +// } +// }, +// put: { +// operationId: 'updateChicken', +// parameters: [ +// o.path('chickenId'), +// o.query('someQuery', true, true) +// ], +// requestBody: { +// description: 'A Chicken', +// content: o.jsonContent('ChickenCreateRequest') +// }, +// responses: { +// 200: o.response({...o.jsonContent('Chicken'), ...o.textContent()}, 'The Chicken'), +// 404: o.response( o.textContent(), 'Not Found'), +// } +// }, +// delete: { +// operationId: 'deleteChicken', +// parameters: [ +// o.path('chickenId'), +// o.header('X-Encryption-Key') +// ] +// }, +// } +// })) +// .build(); diff --git a/test/sdk.spec.ts b/test/sdk.spec.ts index 09b0358..fb2901a 100644 --- a/test/sdk.spec.ts +++ b/test/sdk.spec.ts @@ -10,8 +10,7 @@ const caller: Caller = { describe('SDK', () => { it('should', async () => { - const result = await new ChickenStoreAPISdk(caller, {environment: 'prod'}) - .getChicken({chickenId: 'id'}, {}, {}); + const result = await new ChickenStoreAPISdk(caller, {environment: 'prod'}).getChickens() expect(result.headers['x-uri']).toEqual('https://api.xyz.io/views') }) }); diff --git a/test/validator.spec.ts b/test/validator.spec.ts deleted file mode 100644 index c4f0a38..0000000 --- a/test/validator.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import {JSONSchema} from "json-schema-to-typescript"; -import {SchemaBuilder, Validator} from "../src"; - -const s = SchemaBuilder.create(); -describe('Validation', () => { - - describe('Primitives', () => { - it('should validate null', () => { - const schema = s.null(); - expect(Validator.validate(null, schema)).toEqual([]); - expect(Validator.validate(false, schema)).toEqual([{value: false, location: '#', schema, message: 'Expected value to be null'}]); - expect(Validator.validate(undefined, schema)).toEqual([{value: undefined, location: '#', schema, message: 'Expected value to be null'}]); - expect(Validator.validate(0, schema)).toEqual([{value: 0, location: '#', schema, message: 'Expected value to be null'}]); - }); - - it('should validate boolean', () => { - const schema = s.boolean(); - expect(Validator.validate(true, schema)).toEqual([]); - expect(Validator.validate(false, schema)).toEqual([]); - expect(Validator.validate(undefined, schema)).toEqual([{value: undefined, location: '#', schema, message: 'Expected value to be a boolean'}]); - expect(Validator.validate(0, schema)).toEqual([{value: 0, location: '#', schema, message: 'Expected value to be a boolean'}]); - }); - - it('should validate string', () => { - const schema = s.string(); - expect(Validator.validate('a string', schema)).toEqual([]); - expect(Validator.validate(false, schema)).toEqual([{value: false, location: '#', schema, message: 'Expected value to be a string'}]); - expect(Validator.validate(0, schema)).toEqual([{value: 0, location: '#', schema, message: 'Expected value to be a string'}]); - expect(Validator.validate(undefined, schema)).toEqual([{value: undefined, location: '#', schema, message: 'Expected value to be a string'}]); - }); - - it('should validate number', () => { - const schema = s.number(); - expect(Validator.validate(0, schema)).toEqual([]); - expect(Validator.validate(-7600000, schema)).toEqual([]); - expect(Validator.validate(123.321, schema)).toEqual([]); - expect(Validator.validate('a string', schema)).toEqual([{value: 'a string', location: '#', schema, message: 'Expected value to be a number'}]); - expect(Validator.validate(false, schema)).toEqual([{value: false, location: '#', schema, message: 'Expected value to be a number'}]); - expect(Validator.validate(undefined, schema)).toEqual([{value: undefined, location: '#', schema, message: 'Expected value to be a number'}]); - }); - - it('should validate integer', () => { - const schema = s.integer(); - expect(Validator.validate(0, schema)).toEqual([]); - expect(Validator.validate(-7600000, schema)).toEqual([]); - expect(Validator.validate(123.321, schema)).toEqual([{value: 123.321, location: '#', schema, message: 'Expected value to be an integer'}]); - expect(Validator.validate('a string', schema)).toEqual([{value: 'a string', location: '#', schema, message: 'Expected value to be a number'}]); - expect(Validator.validate(false, schema)).toEqual([{value: false, location: '#', schema, message: 'Expected value to be a number'}]); - expect(Validator.validate(undefined, schema)).toEqual([{value: undefined, location: '#', schema, message: 'Expected value to be a number'}]); - }); - }); - - describe('Object Validation', () => { - it('should validate object type by default', () => { - const schema = {}; - expect(Validator.validate({}, schema)).toEqual([]); - expect(Validator.validate(12, schema)).toEqual([{value: 12, location: '#', schema, message: 'Expected value to be an object'}]); - }); - - it('should validate object type', () => { - const schema = s.object(undefined, undefined, true) as JSONSchema; - expect(Validator.validate({a: 'b'}, schema)).toEqual([]); - expect(Validator.validate('a string', schema)).toEqual([{value: 'a string', location: '#', schema, message: 'Expected value to be an object'}]); - expect(Validator.validate(['a'], schema)).toEqual([{value: ['a'], location: '#', schema, message: 'Expected value to be an object'}]); - expect(Validator.validate(false, schema)).toEqual([{value: false, location: '#', schema, message: 'Expected value to be an object'}]); - expect(Validator.validate(0, schema)).toEqual([{value: 0, location: '#', schema, message: 'Expected value to be an object'}]); - expect(Validator.validate(undefined, schema)).toEqual([{value: undefined, location: '#', schema, message: 'Expected value to be an object'}]); - }); - - it('should validate object has required properties', () => { - const schema = s.object({a: s.string(), b: s.number()}, undefined, false) as JSONSchema; - expect(Validator.validate({a: 'b', b: 5}, schema)).toEqual([]); - expect(Validator.validate({a: 'b'}, schema)).toEqual([{value: {a: 'b'}, location: '#', schema, message: 'Expected the following keys that were not present: [b]'}]); - expect(Validator.validate({a: 'b', b : null}, schema)).toEqual([{value: null, location: '#/b', schema: { type: 'number' }, message: 'Expected value to be a number'}]); - }); - - it('should validate child property', () => { - const schema = s.object({a: s.string(), b: s.number()}, undefined, false) as JSONSchema; - expect(Validator.validate({a: 'b', b: 4}, schema)).toEqual([]); - expect(Validator.validate({a: 'b', b: 'o'}, schema)).toEqual([{value: 'o', schema: s.number(), location: '#/b', message: 'Expected value to be a number'}]); - }); - - it('should validate child of child property', () => { - const schema = s.object({a: s.string(), b: s.object({x: s.string() })}, undefined, false) as JSONSchema; - expect(Validator.validate({a: 'b', b: { x: 'x' }}, schema)).toEqual([]); - expect(Validator.validate({a: 'b', b: { x: 4 }}, schema)).toEqual([{value: 4, schema: s.string(), location: '#/b/x', message: 'Expected value to be a string'}]); - }); - - it('should validate reference', () => { - const schema = SchemaBuilder.create() - .add('Abc', s => s.object({a: s.string()})) - .add('Def', s => s.reference('Abc')) - .build(); - expect(Validator.validate({a: 'b'}, schema.components.schemas.Def, schema)).toEqual([]); - expect(Validator.validate({a: 5}, schema.components.schemas.Def, schema)).toEqual([{value: 5, schema: s.string(), location: '#/a', message: 'Expected value to be a string'}]); - }); - - it('should validate additional properties', () => { - const schema = s.object({a: s.string()}, undefined, s.boolean()); - expect(Validator.validate({a: 'b', b: true}, schema)).toEqual([]); - expect(Validator.validate({a: 'b', b: 78}, schema)).toEqual([{value: 78, schema: s.boolean(), location: '#/b', message: 'Expected value to be a boolean'}]); - }); - - it('should validate optional properties', () => { - const schema = s.object({a: s.string()}, {b: s.boolean()}); - expect(Validator.validate({a: 'b', b: true}, schema)).toEqual([]); - expect(Validator.validate({a: 'b'}, schema)).toEqual([]); - expect(Validator.validate({a: 'b', b: 78}, schema)).toEqual([{value: 78, schema: s.boolean(), location: '#/b', message: 'Expected value to be a boolean'}]); - }); - - it('should validate anyof', () => { - const schema: JSONSchema = { anyOf: [{type: 'string', const: 'X'}, {type: 'string', const: 'Y'}]} - expect(Validator.validate('X', schema)).toEqual([]); - expect(Validator.validate('Y', schema)).toEqual([]); - expect(Validator.validate('T', schema).length).toEqual(1); - }); - - it('should validate minLength', () => { - const schema: JSONSchema = {type: 'string', minLength: 1}; - expect(Validator.validate('', schema)).toEqual([{value: '', schema: {type: 'string', minLength: 1}, location: '#', message: 'Expected value to have length greater than or equal to 1'}]); - expect(Validator.validate('Y', schema)).toEqual([]); - }); - - it('should validate minLength in object', () => { - const schema: JSONSchema = s.object({ abc: {type: 'string', minLength: 1} }); - expect(Validator.validate({ abc: '' }, schema)).toEqual([{value: '', schema: {type: 'string', minLength: 1}, location: '#/abc', message: 'Expected value to have length greater than or equal to 1'}]); - expect(Validator.validate({ abc: 'Y' }, schema)).toEqual([]); - }); - }); - -});