diff --git a/src/framework/types.ts b/src/framework/types.ts index b75adc31..e0ef13e3 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -7,7 +7,7 @@ import AjvDraft4 from 'ajv-draft-04'; import Ajv2020 from 'ajv/dist/2020'; export { OpenAPIFrameworkArgs }; -export type AjvInstance = AjvDraft4 | Ajv2020 +export type AjvInstance = AjvDraft4 | Ajv2020; export type BodySchema = | OpenAPIV3.ReferenceObject @@ -48,7 +48,7 @@ export interface Options extends ajv.Options { ajvFormats?: FormatsPluginOptions; } -export interface RequestValidatorOptions extends Options, ValidateRequestOpts { } +export interface RequestValidatorOptions extends Options, ValidateRequestOpts {} export type ValidateRequestOpts = { /** @@ -125,32 +125,42 @@ export class SerDesSingleton implements SerDes { serialize: param.serialize, }; } -}; +} export type SerDesMap = { - [format: string]: SerDes + [format: string]: SerDes; }; -type Primitive = undefined | null | boolean | string | number | Function +type Primitive = undefined | null | boolean | string | number | Function; -type Immutable = - T extends Primitive ? T : - T extends Array ? ReadonlyArray : - T extends Map ? ReadonlyMap : Readonly +type Immutable = T extends Primitive + ? T + : T extends Array + ? ReadonlyArray + : T extends Map + ? ReadonlyMap + : Readonly; -type DeepImmutable = - T extends Primitive ? T : - T extends Array ? DeepImmutableArray : - T extends Map ? DeepImmutableMap : DeepImmutableObject +type DeepImmutable = T extends Primitive + ? T + : T extends Array + ? DeepImmutableArray + : T extends Map + ? DeepImmutableMap + : DeepImmutableObject; interface DeepImmutableArray extends ReadonlyArray> {} -interface DeepImmutableMap extends ReadonlyMap, DeepImmutable> {} +interface DeepImmutableMap + extends ReadonlyMap, DeepImmutable> {} type DeepImmutableObject = { - readonly [K in keyof T]: DeepImmutable -} + readonly [K in keyof T]: DeepImmutable; +}; export interface OpenApiValidatorOpts { - apiSpec: DeepImmutable | DeepImmutable | string; + apiSpec: + | DeepImmutable + | DeepImmutable + | string; validateApiSpec?: boolean; validateResponses?: boolean | ValidateResponseOpts; validateRequests?: boolean | ValidateRequestOpts; @@ -204,18 +214,19 @@ export namespace OpenAPIV3 { externalDocs?: ExternalDocumentationObject; } - interface ComponentsV3_1 extends ComponentsObject { - pathItems?: { [path: string]: PathItemObject | ReferenceObject } + export interface ComponentsV3_1 extends ComponentsObject { + pathItems?: { [path: string]: PathItemObject | ReferenceObject }; } - export interface DocumentV3_1 extends Omit { + export interface DocumentV3_1 + extends Omit { openapi: `3.1.${string}`; paths?: DocumentV3['paths']; info: InfoObjectV3_1; components: ComponentsV3_1; webhooks: { - [name: string]: PathItemObject | ReferenceObject - } + [name: string]: PathItemObject | ReferenceObject; + }; } export interface InfoObject { @@ -299,7 +310,7 @@ export namespace OpenAPIV3 { in: string; } - export interface HeaderObject extends ParameterBaseObject { } + export interface HeaderObject extends ParameterBaseObject {} interface ParameterBaseObject { description?: string; @@ -323,14 +334,18 @@ export namespace OpenAPIV3 { | 'integer'; export type ArraySchemaObjectType = 'array'; - export type SchemaObject = ArraySchemaObject | NonArraySchemaObject | CompositionSchemaObject; + export type SchemaObject = + | ArraySchemaObject + | NonArraySchemaObject + | CompositionSchemaObject; - export interface ArraySchemaObject extends BaseSchemaObject { + export interface ArraySchemaObject + extends BaseSchemaObject { items: ReferenceObject | SchemaObject; } - export interface NonArraySchemaObject extends BaseSchemaObject { - } + export interface NonArraySchemaObject + extends BaseSchemaObject {} export interface CompositionSchemaObject extends BaseSchemaObject { // JSON schema allowed properties, adjusted for OpenAPI diff --git a/src/middlewares/parsers/schema.preprocessor.ts b/src/middlewares/parsers/schema.preprocessor.ts index 36b0e315..93b087af 100644 --- a/src/middlewares/parsers/schema.preprocessor.ts +++ b/src/middlewares/parsers/schema.preprocessor.ts @@ -1,93 +1,15 @@ -import Ajv from 'ajv'; -import ajv = require('ajv'); -import * as cloneDeep from 'lodash.clonedeep'; -import * as _get from 'lodash.get'; -import { createRequestAjv } from '../../framework/ajv'; import { OpenAPIV3, - SerDesMap, Options, + SerDesMap, ValidateResponseOpts, } from '../../framework/types'; +import { createRequestAjv } from '../../framework/ajv'; +import Ajv from 'ajv'; +import * as _get from 'lodash.get'; +import * as cloneDeep from 'lodash.clonedeep'; -interface TraversalStates { - req: TraversalState; - res: TraversalState; -} - -interface TraversalState { - discriminator: object; - kind: 'req' | 'res'; - path: string[]; -} - -interface TopLevelPathNodes { - requestBodies: Root[]; - requestParameters: Root[]; - responses: Root[]; -} -interface TopLevelSchemaNodes extends TopLevelPathNodes { - schemas: Root[]; - requestBodies: Root[]; - responses: Root[]; -} - -class Node { - public readonly path: string[]; - public readonly parent: P; - public readonly schema: T; - constructor(parent: P, schema: T, path: string[]) { - this.path = path; - this.parent = parent; - this.schema = schema; - } -} -type SchemaObjectNode = Node; - -function isParameterObject( - node: ParameterObject | ReferenceObject, -): node is ParameterObject { - return !(node as ReferenceObject).$ref; -} -function isReferenceObject( - node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject, -): node is ReferenceObject { - return !!(node as ReferenceObject).$ref; -} -function isArraySchemaObject( - node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject, -): node is ArraySchemaObject { - return !!(node as ArraySchemaObject).items; -} -function isNonArraySchemaObject( - node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject, -): node is NonArraySchemaObject { - return !isArraySchemaObject(node) && !isReferenceObject(node); -} - -class Root extends Node { - constructor(schema: T, path: string[]) { - super(null, schema, path); - } -} - -type ArraySchemaObject = OpenAPIV3.ArraySchemaObject; -type NonArraySchemaObject = OpenAPIV3.NonArraySchemaObject; -type OperationObject = OpenAPIV3.OperationObject; -type ParameterObject = OpenAPIV3.ParameterObject; -type ReferenceObject = OpenAPIV3.ReferenceObject; -type SchemaObject = OpenAPIV3.SchemaObject; -type Schema = ReferenceObject | SchemaObject; - -if (!Array.prototype['flatMap']) { - // polyfill flatMap - // TODO remove me when dropping node 10 support - Array.prototype['flatMap'] = function (lambda) { - return Array.prototype.concat.apply([], this.map(lambda)); - }; - Object.defineProperty(Array.prototype, 'flatMap', { enumerable: false }); -} -export const httpMethods = new Set([ +const HttpMethods = [ 'get', 'put', 'post', @@ -96,284 +18,618 @@ export const httpMethods = new Set([ 'head', 'patch', 'trace', -]); -export class SchemaPreprocessor { - private ajv: Ajv; - private apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1; - private apiDocRes: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1; - private serDesMap: SerDesMap; - private responseOpts: ValidateResponseOpts; - private resolvedSchemaCache = new Map(); +] as const; +const xOfObjects = ['allOf', 'oneOf', 'anyOf'] as const; + +// compatibility with other code +export const httpMethods = new Set(HttpMethods); + +type Document = OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1; + +type VisitorObjects = { + document: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1; + components: OpenAPIV3.ComponentsObject; + componentsV3_1: OpenAPIV3.ComponentsV3_1; + pathItem: OpenAPIV3.PathItemObject | OpenAPIV3.ReferenceObject; + schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; + operation: OpenAPIV3.OperationObject; + requestBody: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject; + response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject; + encoding: OpenAPIV3.EncodingObject; + header: OpenAPIV3.HeaderObject | OpenAPIV3.ReferenceObject; + mediaType: OpenAPIV3.MediaTypeObject; + parameter: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject; + callback: OpenAPIV3.CallbackObject | OpenAPIV3.ReferenceObject; +}; + +type VisitorTypes = keyof VisitorObjects; +type OpenAPIObject = VisitorObjects[VisitorTypes]; + +/** A union of all property keys of `Object` that have type `DesiredType`. */ +type KeysWithMatchingValue = { + [Key in keyof Object]: Object[Key] extends DesiredKey + ? DesiredKey extends Object[Key] + ? Key + : never + : never; +}[keyof Object]; + +/** A wrapper type for `KeysWithMatchingValue` that fixes usage when `Object` is an optional type union. */ +type KeysWithExactType = Object extends unknown + ? KeysWithMatchingValue + : never; + +/** All `VisitorTypes` that also support `OpenAPIV3.ReferenceObject`s. */ +type VisitorTypesWithReference = { + [Key in keyof VisitorObjects]: VisitorObjects[Key] extends OpenAPIV3.ReferenceObject + ? OpenAPIV3.ReferenceObject extends VisitorObjects[Key] + ? Key + : never + : VisitorObjects[Key] extends OpenAPIV3.ReferenceObject | infer _Other + ? OpenAPIV3.ReferenceObject extends VisitorObjects[Key] + ? Key + : never + : never; +}[keyof VisitorObjects]; + +type DiscriminatorState = { + discriminator?: string; + options?: { option: any; ref: any }[]; + properties?: { + [p: string]: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject; + }; + required?: string[]; +}; + +class VisitorNode { + public originalRef?: string; + public traversedObjects: Set; // track circular references + public discriminator: DiscriminatorState = {}; constructor( - apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1, - ajvOptions: Options, - validateResponsesOpts: ValidateResponseOpts, + public type: NodeType, + public object: VisitorObjects[NodeType], + public responseObject: VisitorObjects[NodeType] | undefined, + traversedObjects: Set = new Set(), + public path: string[] = [], + public pathFromParent?: string[], ) { - this.ajv = createRequestAjv(apiDoc, ajvOptions); - this.apiDoc = apiDoc; - this.serDesMap = ajvOptions.serDesMap; - this.responseOpts = validateResponsesOpts; + // copy traversed object to not affect other children of the same parent + this.traversedObjects = new Set(traversedObjects); + this.traversedObjects.add(object); } - public preProcess() { - const componentSchemas = this.gatherComponentSchemaNodes(); - let r; + public resolve( + parent: VisitorNode, + resolver: ObjectResolver, + responseResolver: ObjectResolver | undefined, + ): boolean { + if ( + !isReferenceNode(this) || + !isReferenceObject(this.object) || + (this.responseObject !== undefined && + !isReferenceObject(this.responseObject)) + ) { + throw new Error('Cannot resolve node that is not referenced.'); + } - if (this.apiDoc.paths) { - r = this.gatherSchemaNodesFromPaths(); + if (this.responseObject !== undefined && responseResolver === undefined) { + throw new Error( + 'Response resolver is required when response object exists.', + ); } - // Now that we've processed paths, clone a response spec if we are validating responses - this.apiDocRes = !!this.responseOpts ? cloneDeep(this.apiDoc) : null; + const resolvedObject = resolver.resolveObject( + this.object, + ); + const resolvedResponseObject = responseResolver?.resolveObject< + typeof this.type + >(this.responseObject as OpenAPIV3.ReferenceObject | undefined); - if (this.apiDoc.components) { - this.removeExamples(this.apiDoc.components); + // stop when detecting circular references + if ( + resolvedObject === undefined || + this.traversedObjects.has(resolvedObject) + ) { + return false; } - const schemaNodes = { - schemas: componentSchemas, - requestBodies: r?.requestBodies, - responses: r?.responses, - requestParameters: r?.requestParameters, - }; + this.object = resolvedObject; + this.responseObject = resolvedResponseObject; + + // cast valid as we just resolved the object of this node + parent.resolveChild(this as VisitorNode); + + return true; + } - // Traverse the schemas - if (r) { - this.traverseSchemas(schemaNodes, (parent, schema, opts) => - this.schemaVisitor(parent, schema, opts), + public resolveChild( + child: VisitorNode, + ): void { + function resolveParentWithPath( + path: string[], + object: VisitorObjects[NodeType] | undefined, + childObject: VisitorObjects[ChildType] | undefined, + ): void { + if (object === undefined) return; + + const pathLength = path.length; + for (let i = 0; i < pathLength - 1; i++) { + object = object[path[i]]; + } + + object[path[pathLength - 1]] = childObject; + } + + // Resolve reference object in parent, then process again with resolved schema + // As every object (aka schema) is 'pass-by-reference', this will update the actual apiDoc. + if (child.pathFromParent && child.pathFromParent.length > 0) { + resolveParentWithPath(child.pathFromParent, this.object, child.object); + resolveParentWithPath( + child.pathFromParent, + this.responseObject, + child.responseObject, ); + } else { + const lastPathComponent = child.path[child.path.length - 1]; + + this.object[lastPathComponent] = child.object; + if (this.responseObject !== undefined) { + this.responseObject[lastPathComponent] = child.responseObject; + } } + } - return { - apiDoc: this.apiDoc, - apiDocRes: this.apiDocRes, + static fromParent< + ParentType extends VisitorTypes, + NodeType extends VisitorTypes, + PropertyKey extends KeysWithExactType< + VisitorObjects[ParentType], + VisitorObjects[NodeType] + >, + >( + parent: VisitorNode, + type: NodeType, + propertyPath?: PropertyKey, + ): VisitorNode[] { + propertyPath = propertyPath ?? (type as unknown as PropertyKey); + + const object = parent.object[ + propertyPath + ] as unknown as VisitorObjects[NodeType]; + + if (object === undefined) return []; + + return [ + new VisitorNode( + type, + object, + parent.responseObject?.[ + propertyPath + ] as unknown as VisitorObjects[NodeType], + parent.traversedObjects, + [...parent.path, propertyPath], + ), + ]; + } + + static fromParentDict< + ParentType extends VisitorTypes, + NodeType extends VisitorTypes, + DictKey extends KeysWithExactType< + VisitorObjects[ParentType], + { [key: string]: VisitorObjects[NodeType] } + >, + >( + parent: VisitorNode, + type: NodeType, + dictPath: DictKey, + ): VisitorNode[] { + const nodes: VisitorNode[] = []; + const dict = parent.object[dictPath] as unknown as { + [key: string]: VisitorObjects[NodeType]; }; + + if (dict === undefined) return []; + + forEachValue(dict, (object, key) => { + nodes.push( + new VisitorNode( + type, + object, + parent.responseObject?.[dictPath]?.[key], + parent.traversedObjects, + [...parent.path, dictPath, key], + [dictPath, key], + ), + ); + }); + + return nodes; } - private gatherComponentSchemaNodes(): Root[] { - const nodes = []; - const componentSchemaMap = this.apiDoc?.components?.schemas ?? []; - for (const [id, s] of Object.entries(componentSchemaMap)) { - const schema = this.resolveSchema(s); - this.apiDoc.components.schemas[id] = schema; - const path = ['components', 'schemas', id]; - const node = new Root(schema, path); - nodes.push(node); - } + static fromParentArray< + ParentType extends VisitorTypes, + NodeType extends VisitorTypes, + ArrayKey extends KeysWithExactType< + VisitorObjects[ParentType], + Array + >, + >( + parent: VisitorNode, + type: NodeType, + arrayPath: ArrayKey, + ): VisitorNode[] { + const nodes: VisitorNode[] = []; + const array = parent.object[arrayPath] as unknown as Array< + VisitorObjects[NodeType] + >; + + if (array === undefined) return []; + + array.forEach((object, index) => { + nodes.push( + new VisitorNode( + type, + object, + parent.responseObject?.[arrayPath]?.[index], + parent.traversedObjects, + [...parent.path, arrayPath, `${index}`], + [arrayPath, `${index}`], + ), + ); + }); + return nodes; } +} - private gatherSchemaNodesFromPaths(): TopLevelPathNodes { - const requestBodySchemas = []; - const requestParameterSchemas = []; - const responseSchemas = []; - - for (const [p, pi] of Object.entries(this.apiDoc.paths)) { - const pathItem = this.resolveSchema(pi); - - // Since OpenAPI 3.1, paths can be a #ref to reusable path items - // The following line mutates the paths item to dereference the reference, so that we can process as a POJO, as we would if it wasn't a reference - this.apiDoc.paths[p] = pathItem; - - for (const method of Object.keys(pathItem)) { - if (httpMethods.has(method)) { - const operation = pathItem[method]; - // Adds path declared parameters to the schema's parameters list - this.preprocessPathLevelParameters(method, pathItem); - const path = ['paths', p, method]; - const node = new Root(operation, path); - const requestBodies = this.extractRequestBodySchemaNodes(node); - const responseBodies = this.extractResponseSchemaNodes(node); - const requestParameters = - this.extractRequestParameterSchemaNodes(node); - - requestBodySchemas.push(...requestBodies); - responseSchemas.push(...responseBodies); - requestParameterSchemas.push(...requestParameters); - } - } +class ObjectResolver { + public ajv: Ajv; + private resolvedObjectCache = new Map(); + + constructor(private apiDoc: OpenAPISchema, ajvOptions: Options) { + this.ajv = createRequestAjv(this.apiDoc, ajvOptions); + } + + public resolveObject( + object: OpenAPIV3.ReferenceObject | undefined, + ): VisitorObjects[ObjectType] | undefined { + if (!object) return undefined; + + const ref = object.$ref; + + if (ref && this.resolvedObjectCache.has(ref)) { + return this.resolvedObjectCache.get(ref) as Exclude< + VisitorObjects[ObjectType], + OpenAPIV3.ReferenceObject + >; } - return { - requestBodies: requestBodySchemas, - requestParameters: requestParameterSchemas, - responses: responseSchemas, - }; + let res = (ref ? this.ajv.getSchema(ref)?.schema : object) as Exclude< + VisitorObjects[ObjectType], + OpenAPIV3.ReferenceObject + >; + + if (ref && !res) { + const path = ref.split('/').join('.'); + const p = path.substring(path.indexOf('.') + 1); + res = _get(this.apiDoc, p); + } + + if (ref) { + this.resolvedObjectCache.set(ref, res); + } + + return res; } +} - /** - * Traverse the schema starting at each node in nodes - * @param nodes the nodes to traverse - * @param visit a function to invoke per node - */ - private traverseSchemas(nodes: TopLevelSchemaNodes, visit) { - const seen = new Set(); - const recurse = (parent, node, opts: TraversalStates) => { - const schema = node.schema; +export class SchemaPreprocessor { + private readonly resolver: ObjectResolver; - if (!schema || seen.has(schema)) return; + private readonly apiDocRes: OpenAPISchema | undefined; + private readonly responseResolver: ObjectResolver | undefined; - seen.add(schema); + private readonly serDesMap: SerDesMap; - if (schema.$ref) { - const resolvedSchema = this.resolveSchema(schema); - const path = schema.$ref.split('/').slice(1); + constructor( + private apiDoc: OpenAPISchema, + ajvOptions: Options, + private responseOptions: ValidateResponseOpts | undefined, + ) { + this.resolver = new ObjectResolver(this.apiDoc, ajvOptions); - (opts).req.originalSchema = schema; - (opts).res.originalSchema = schema; + this.apiDocRes = !!this.responseOptions ? cloneDeep(this.apiDoc) : null; + if (this.apiDocRes) { + this.responseResolver = new ObjectResolver(this.apiDocRes, ajvOptions); + } + + this.serDesMap = ajvOptions.serDesMap; + } + + public preProcess(): { apiDoc: OpenAPISchema; apiDocRes: OpenAPISchema } { + const root = new VisitorNode('document', this.apiDoc, this.apiDocRes); + + this.traverseSchema(root); + + return { + apiDoc: this.apiDoc, + apiDocRes: this.apiDocRes, + }; + } - visit(parent, node, opts); - recurse(node, new Node(schema, resolvedSchema, path), opts); + private traverseSchema(root: VisitorNode<'document'>): void { + const seenObjects = new Set(); + + const traverse = < + ParentType extends VisitorTypes, + NodeType extends VisitorTypes, + >( + parent: VisitorNode, + node: VisitorNode, + ) => { + // should technically not be required + if (node.object === undefined) { return; } - // Save the original schema so we can check if it was a $ref - (opts).req.originalSchema = schema; - (opts).res.originalSchema = schema; + // resolve references + if (isReferenceNode(node) && isReferenceObject(node.object)) { + node.originalRef = node.object.$ref; + + // To handle discriminators correctly, we cannot resolve xOF schemas. + // Instead, just process discriminator. + if ( + node.pathFromParent && + (xOfObjects as readonly string[]).includes( + node.pathFromParent[0], // this works, as these nodes are always constructed from arrays + ) && + hasNodeType(node, 'schema') + ) { + this.processDiscriminator( + hasNodeType(parent, 'schema') ? parent : undefined, + node, + ); + return; + } - visit(parent, node, opts); + const resolved = node.resolve( + parent, + this.resolver, + this.responseResolver, + ); - if (schema.allOf) { - schema.allOf.forEach((s, i) => { - const child = new Node(node, s, [...node.path, 'allOf', i + '']); - recurse(node, child, opts); - }); - } else if (schema.oneOf) { - schema.oneOf.forEach((s, i) => { - const child = new Node(node, s, [...node.path, 'oneOf', i + '']); - recurse(node, child, opts); - }); - } else if (schema.anyOf) { - schema.anyOf.forEach((s, i) => { - const child = new Node(node, s, [...node.path, 'anyOf', i + '']); - recurse(node, child, opts); - }); - } else if (schema.type === 'array' && schema.items) { - const child = new Node(node, schema.items, [...node.path, 'items']); - recurse(node, child, opts); - } else if (schema.properties) { - Object.entries(schema.properties).forEach(([id, cschema]) => { - const path = [...node.path, 'properties', id]; - const child = new Node(node, cschema, path); - recurse(node, child, opts); - }); - } else if (schema.additionalProperties) { - const child = new Node(node, schema.additionalProperties, [ - ...node.path, - 'additionalProperties', - ]); - recurse(node, child, opts); + if (!resolved) { + return; + } + } + + if (seenObjects.has(node.object)) return; + seenObjects.add(node.object); + + this.visitNode(parent, node); + + let children: VisitorNode[]; + + if (hasNodeType(node, 'document')) { + children = this.getChildrenForDocument(node); + } else if (hasNodeType(node, 'components')) { + children = this.getChildrenForComponents(node); + } else if (hasNodeType(node, 'componentsV3_1')) { + children = this.getChildrenForComponentsV3_1(node); + } else if (hasNodeType(node, 'pathItem')) { + children = this.getChildrenForPathItem(node); + } else if (hasNodeType(node, 'schema')) { + children = this.getChildrenForSchema(node); + } else if (hasNodeType(node, 'operation')) { + children = this.getChildrenForOperation(node); + } else if (hasNodeType(node, 'requestBody')) { + children = this.getChildrenForRequestBody(node); + } else if (hasNodeType(node, 'response')) { + children = this.getChildrenForResponse(node); + } else if (hasNodeType(node, 'encoding')) { + children = this.getChildrenForEncoding(node); + } else if (hasNodeType(node, 'header')) { + children = this.getChildrenForHeader(node); + } else if (hasNodeType(node, 'mediaType')) { + children = this.getChildrenForMediaType(node); + } else if (hasNodeType(node, 'parameter')) { + children = this.getChildrenForParameter(node); + } else if (hasNodeType(node, 'callback')) { + children = this.getChildrenForCallback(node); + } else { + throw new Error(`No strategy to traverse node with type ${node.type}.`); } + + children.forEach((child) => { + traverse(node as VisitorNode, child); + }); }; - const initOpts = (): TraversalStates => ({ - req: { discriminator: {}, kind: 'req', path: [] }, - res: { discriminator: {}, kind: 'res', path: [] }, - }); + traverse(undefined, root); + } - for (const node of nodes.schemas) { - recurse(null, node, initOpts()); + private visitNode< + ParentType extends VisitorTypes, + NodeType extends VisitorTypes, + >( + parent: VisitorNode | undefined, + node: VisitorNode, + ): void { + this.removeExamples(node); + + if (hasNodeType(node, 'pathItem')) { + this.preProcessPathParameters(node.object); + } else if (hasNodeType(node, 'schema')) { + this.preProcessSchema( + hasNodeType(parent, 'schema') ? parent : undefined, + node, + ); } + } - for (const node of nodes.requestBodies) { - recurse(null, node, initOpts()); + private removeExamples( + node: VisitorNode, + ): void { + if ( + isReferenceObject(node.object) || + isReferenceObject(node.responseObject) + ) { + throw new Error('Object should have been unwrapped.'); } - for (const node of nodes.responses) { - recurse(null, node, initOpts()); - } + if (hasNodeType(node, 'components')) { + delete node.object.examples; - for (const node of nodes.requestParameters) { - recurse(null, node, initOpts()); + if (node.responseObject) { + delete node.responseObject.examples; + } + } else if ( + hasNodeType(node, 'mediaType') || + hasNodeType(node, 'header') || + hasNodeType(node, 'parameter') || + hasNodeType(node, 'schema') + ) { + delete node.object.example; + delete node.object.examples; + + if (node.responseObject) { + delete node.responseObject.example; + delete node.responseObject.examples; + } } } - private schemaVisitor( - parent: SchemaObjectNode, - node: SchemaObjectNode, - opts: TraversalStates, - ) { - const pschemas = [parent?.schema]; - const nschemas = [node.schema]; - - if (this.apiDocRes) { - const p = _get(this.apiDocRes, parent?.path); - const n = _get(this.apiDocRes, node?.path); - pschemas.push(p); - nschemas.push(n); + /** + * add path level parameters to the schema's parameters list + * @param pathItem + */ + private preProcessPathParameters(pathItem: VisitorObjects['pathItem']): void { + if (isReferenceObject(pathItem)) { + throw new Error('Object should have been unwrapped.'); } - // visit the node in both the request and response schema - for (let i = 0; i < nschemas.length; i++) { - const kind = i === 0 ? 'req' : 'res'; - const pschema = pschemas[i]; - const nschema = nschemas[i]; - const options = opts[kind]; - options.path = node.path; - - if (nschema) { - // This null check should no longer be necessary - this.handleSerDes(pschema, nschema, options); - this.handleReadonly(pschema, nschema, options); - this.handleWriteonly(pschema, nschema, options); - this.processDiscriminator(pschema, nschema, options); - this.removeSchemaExamples(pschema, nschema, options); + const parameters = pathItem.parameters ?? []; + if (parameters.length === 0) return; + + HttpMethods.forEach((method) => { + const operation = pathItem[method]; + + if (operation === undefined || operation === parameters) return; + + operation.parameters = operation.parameters ?? []; + + const match = ( + pathParam: OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject, + opParam: OpenAPIV3.ReferenceObject | OpenAPIV3.OperationObject, + ) => + // if name or ref exists and are equal + (opParam['name'] && opParam['name'] === pathParam['name']) || + (opParam['$ref'] && opParam['$ref'] === pathParam['$ref']); + + // Add Path level query param to list ONLY if there is not already an operation-level query param by the same name. + for (const param of parameters) { + if ( + !operation.parameters.some((operationParam) => + match(param, operationParam), + ) + ) { + operation.parameters.push(param); + } } - } + }); } - private processDiscriminator(parent: Schema, schema: Schema, opts: any = {}) { - const o = opts.discriminator; - const schemaObj = schema; - const xOf = schemaObj.oneOf ? 'oneOf' : schemaObj.anyOf ? 'anyOf' : null; + private preProcessSchema( + parent: VisitorNode<'schema'> | undefined, + node: VisitorNode<'schema'>, + ): void { + if ( + isReferenceObject(parent?.object) || + isReferenceObject(parent?.responseObject) || + isReferenceObject(node.object) || + isReferenceObject(node.responseObject) + ) { + throw new Error('Object should have been unwrapped.'); + } + + this.handleSerDes(node.object); + this.handleSerDes(node.responseObject); - if (xOf && schemaObj.discriminator?.propertyName && !o.discriminator) { - const options = schemaObj[xOf].flatMap((refObject) => { + // NOTE: only using request schemas, not response schemas! + this.handleReadonly(parent?.object, node.object, node.path); + // NOTE: only using response schemas, not request schemas! + this.handleWriteonly( + parent?.responseObject, + node.responseObject, + node.path, + ); + + this.processDiscriminator(parent, node); + } + + private processDiscriminator( + parent: VisitorNode<'schema'> | undefined, + node: VisitorNode<'schema'>, + ): void { + const nodeState = node.discriminator; + const schemaObj = node.object; + + const xOf = + schemaObj.oneOf !== undefined + ? 'oneOf' + : schemaObj.anyOf + ? 'anyOf' + : undefined; + + if (xOf && schemaObj.discriminator?.propertyName !== undefined) { + nodeState.options = schemaObj[xOf].flatMap((refObject) => { if (refObject['$ref'] === undefined) { return []; } - const keys = this.findKeys( + const keys = findKeys( schemaObj.discriminator.mapping, (value) => value === refObject['$ref'], ); - const ref = this.getKeyFromRef(refObject['$ref']); + const ref = getKeyFromRef(refObject['$ref']); return keys.length > 0 ? keys.map((option) => ({ option, ref })) : [{ option: ref, ref }]; }); - o.options = options; - o.discriminator = schemaObj.discriminator?.propertyName; - o.properties = { - ...(o.properties ?? {}), + nodeState.discriminator = schemaObj.discriminator?.propertyName; + nodeState.properties = { + ...(nodeState.properties ?? {}), ...(schemaObj.properties ?? {}), }; - o.required = Array.from( - new Set((o.required ?? []).concat(schemaObj.required ?? [])), + nodeState.required = Array.from( + new Set((nodeState.required ?? []).concat(schemaObj.required ?? [])), ); } if (xOf) return; - if (o.discriminator) { - o.properties = { - ...(o.properties ?? {}), + const parentState = parent?.discriminator; + + if (parent && parentState && parentState.discriminator) { + parentState.properties = { + ...(parentState.properties ?? {}), ...(schemaObj.properties ?? {}), }; - o.required = Array.from( - new Set((o.required ?? []).concat(schemaObj.required ?? [])), + parentState.required = Array.from( + new Set((parentState.required ?? []).concat(schemaObj.required ?? [])), ); - const ancestor: any = parent; - const ref = opts.originalSchema.$ref; + const ancestor: any = parent.object; + const ref = node.originalRef; if (!ref) return; - const options = this.findKeys( + const options = findKeys( ancestor.discriminator?.mapping, (value) => value === ref, ); - const refName = this.getKeyFromRef(ref); + const refName = getKeyFromRef(ref); if (options.length === 0 && ref) { options.push(refName); } @@ -382,14 +638,14 @@ export class SchemaPreprocessor { const newSchema = JSON.parse(JSON.stringify(schemaObj)); const newProperties = { - ...(o.properties ?? {}), + ...(parentState.properties ?? {}), ...(newSchema.properties ?? {}), }; if (Object.keys(newProperties).length > 0) { newSchema.properties = newProperties; } - newSchema.required = o.required; + newSchema.required = parentState.required; if (newSchema.required.length === 0) { delete newSchema.required; } @@ -399,19 +655,16 @@ export class SchemaPreprocessor { enumerable: false, value: ancestor._discriminator ?? { validators: {}, - options: o.options, - property: o.discriminator, + options: parentState.options, + property: parentState.discriminator, }, }); for (const option of options) { ancestor._discriminator.validators[option] = - this.ajv.compile(newSchema); + this.resolver.ajv.compile(newSchema); } } - //reset data - o.properties = {}; - delete o.required; } } @@ -443,22 +696,20 @@ export class SchemaPreprocessor { * * See [`createAjv`](../../framework/ajv/index.ts) for custom keyword definitions. * - * @param {object} parent - parent schema * @param {object} schema - schema - * @param {object} state - traversal state */ - private handleSerDes( - parent: SchemaObject, - schema: SchemaObject, - state: TraversalState, - ) { + private handleSerDes(schema: OpenAPIV3.SchemaObject | undefined): void { + if (schema === undefined) return; + if ( schema.type === 'string' && !!schema.format && this.serDesMap[schema.format] ) { const serDes = this.serDesMap[schema.format]; + (schema)['x-eov-type'] = schema.type; + if ('nullable' in schema) { // Ajv requires `type` keyword with `nullable` (regardless of value). (schema).type = ['string', 'number', 'boolean', 'object', 'array']; @@ -474,234 +725,373 @@ export class SchemaPreprocessor { } } - private removeSchemaExamples( - parent: OpenAPIV3.SchemaObject, - schema: OpenAPIV3.SchemaObject, - opts, - ) { - this.removeExamples(parent); - this.removeExamples(schema); - } - - private removeExamples( - object: OpenAPIV3.SchemaObject | OpenAPIV3.MediaTypeObject, - ): void { - delete object?.example; - delete object?.examples; - } - private handleReadonly( - parent: OpenAPIV3.SchemaObject, - schema: OpenAPIV3.SchemaObject, - opts, + parentSchema: OpenAPIV3.SchemaObject | undefined, + nodeSchema: OpenAPIV3.SchemaObject, + path: string[], ) { - if (opts.kind === 'res') return; - - const required = parent?.required ?? []; - const prop = opts?.path?.[opts?.path?.length - 1]; + const required = parentSchema?.required ?? []; + const prop = path[path.length - 1]; const index = required.indexOf(prop); - if (schema.readOnly && index > -1) { + if (nodeSchema.readOnly && index > -1) { // remove required if readOnly - parent.required = required + parentSchema.required = required .slice(0, index) .concat(required.slice(index + 1)); - if (parent.required.length === 0) { - delete parent.required; + if (parentSchema.required.length === 0) { + delete parentSchema.required; } } } private handleWriteonly( - parent: OpenAPIV3.SchemaObject, - schema: OpenAPIV3.SchemaObject, - opts, + parentSchema: OpenAPIV3.SchemaObject | undefined, + nodeSchema: OpenAPIV3.SchemaObject, + path: string[], ) { - if (opts.kind === 'req') return; - - const required = parent?.required ?? []; - const prop = opts?.path?.[opts?.path?.length - 1]; + const required = parentSchema?.required ?? []; + const prop = path[path.length - 1]; const index = required.indexOf(prop); - if (schema.writeOnly && index > -1) { + if (nodeSchema?.writeOnly && index > -1) { // remove required if writeOnly - parent.required = required + parentSchema.required = required .slice(0, index) .concat(required.slice(index + 1)); - if (parent.required.length === 0) { - delete parent.required; + if (parentSchema.required.length === 0) { + delete parentSchema.required; } } } - /** - * extract all requestBodies' schemas from an operation - * @param op - */ - private extractRequestBodySchemaNodes( - node: Root, - ): Root[] { - const op = node.schema; - const bodySchema = this.resolveSchema( - op.requestBody, - ); - op.requestBody = bodySchema; - - if (!bodySchema?.content) return []; + private getChildrenForDocument( + parent: VisitorNode<'document'>, + ): VisitorNode[] { + const children = []; - const result: Root[] = []; - const contentEntries = Object.entries(bodySchema.content); - for (const [type, mediaTypeObject] of contentEntries) { - const mediaTypeSchema = this.resolveSchema( - mediaTypeObject.schema, + if (isDocumentV3_1(parent.object)) { + children.push( + ...VisitorNode.fromParent(parent, 'componentsV3_1', 'components'), ); - op.requestBody.content[type].schema = mediaTypeSchema; + children.push( + ...VisitorNode.fromParentDict(parent, 'pathItem', 'webhooks'), + ); + } else { + children.push(...VisitorNode.fromParent(parent, 'components')); + } - // TODO replace with visitor - this.removeExamples(op.requestBody.content[type]); + children.push(...VisitorNode.fromParentDict(parent, 'pathItem', 'paths')); - const path = [...node.path, 'requestBody', 'content', type, 'schema']; - result.push(new Root(mediaTypeSchema, path)); - } - return result; - } - - private extractResponseSchemaNodes( - node: Root, - ): Root[] { - const op = node.schema; - const responses = op.responses; - - if (!responses) return []; - - const schemas: Root[] = []; - for (const [statusCode, response] of Object.entries(responses)) { - const rschema = this.resolveSchema(response); - if (!rschema) { - // issue #553 - // TODO the schema failed to resolve. - // This can occur with multi-file specs - // improve resolution, so that rschema resolves (use json ref parser?) - continue; - } - responses[statusCode] = rschema; - - if (rschema.content) { - for (const [type, mediaType] of Object.entries(rschema.content)) { - const schema = this.resolveSchema(mediaType?.schema); - if (schema) { - rschema.content[type].schema = schema; - const path = [ - ...node.path, - 'responses', - statusCode, - 'content', - type, - 'schema', - ]; - - // TODO replace with visitor - this.removeExamples(rschema.content[type]); - - schemas.push(new Root(schema, path)); - } - } - } - } - return schemas; + return children; + } + + private getChildrenForComponents( + parent: VisitorNode<'components'>, + ): VisitorNode[] { + const children = []; + + children.push(...VisitorNode.fromParentDict(parent, 'schema', 'schemas')); + children.push( + ...VisitorNode.fromParentDict(parent, 'response', 'responses'), + ); + children.push(...VisitorNode.fromParentDict(parent, 'header', 'headers')); + children.push( + ...VisitorNode.fromParentDict(parent, 'callback', 'callbacks'), + ); + children.push( + ...VisitorNode.fromParentDict(parent, 'requestBody', 'requestBodies'), + ); + children.push( + ...VisitorNode.fromParentDict(parent, 'parameter', 'parameters'), + ); + + return children; } - private extractRequestParameterSchemaNodes( - operationNode: Root, - ): Root[] { - return (operationNode.schema.parameters ?? []).flatMap((node) => { - const parameterObject = isParameterObject(node) ? node : undefined; + private getChildrenForComponentsV3_1( + parent: VisitorNode<'componentsV3_1'>, + ): VisitorNode[] { + const children = []; - // TODO replace with visitor - // TODO This does not handle JSON query parameters - this.removeExamples(parameterObject); + children.push( + ...VisitorNode.fromParentDict(parent, 'pathItem', 'pathItems'), + ); + // process components V3.1 also like normal components + children.push( + new VisitorNode( + 'components', + parent.object, + parent.responseObject, + parent.traversedObjects, + parent.path, + ), + ); - if (!parameterObject?.schema) return []; + return children; + } - const schema = isNonArraySchemaObject(parameterObject.schema) - ? parameterObject.schema - : undefined; - if (!schema) return []; + private getChildrenForPathItem( + parent: VisitorNode<'pathItem'>, + ): VisitorNode[] { + if (isReferenceObject(parent.object)) { + throw new Error('Object should have been unwrapped.'); + } + + const children = []; - return new Root(schema, [ - ...operationNode.path, - 'parameters', - parameterObject.name, - parameterObject.in, - ]); + HttpMethods.forEach((method) => { + if (method in parent.object) + children.push(...VisitorNode.fromParent(parent, 'operation', method)); }); + + children.push( + ...VisitorNode.fromParentArray(parent, 'parameter', 'parameters'), + ); + + return children; } - private resolveSchema(schema): T { - if (!schema) return null; - const ref = schema?.['$ref']; - if (ref && this.resolvedSchemaCache.has(ref)) { - return this.resolvedSchemaCache.get(ref) as T; + private getChildrenForSchema( + parent: VisitorNode<'schema'>, + ): VisitorNode[] { + if ( + isReferenceObject(parent.object) || + isReferenceObject(parent.responseObject) + ) { + throw new Error('Object should have been unwrapped.'); } - let res = (ref ? this.ajv.getSchema(ref)?.schema : schema) as T; - if (ref && !res) { - const path = ref.split('/').join('.'); - const p = path.substring(path.indexOf('.') + 1); - res = _get(this.apiDoc, p); + + const children = []; + + if ( + typeof parent.object.additionalProperties !== 'boolean' && + typeof parent.responseObject?.additionalProperties !== 'boolean' + ) { + // constructing this manually, as the type of additional properties includes boolean + children.push( + new VisitorNode( + 'schema', + parent.object.additionalProperties, + parent.responseObject?.additionalProperties, + parent.traversedObjects, + [...parent.path, 'additionalProperties'], + ), + ); } - if (ref) { - this.resolvedSchemaCache.set(ref, res); + children.push( + ...VisitorNode.fromParentDict(parent, 'schema', 'properties'), + ); + + if (parent.object.type === 'array' && 'items' in parent.object) { + children.push(...VisitorNode.fromParent(parent, 'schema', 'items')); + } else { + if ('not' in parent.object) { + children.push(...VisitorNode.fromParent(parent, 'schema', 'not')); + } + + xOfObjects.forEach((property) => { + children.push( + ...VisitorNode.fromParentArray(parent, 'schema', property), + ); + }); } - return res; + + return children; } - /** - * add path level parameters to the schema's parameters list - * @param pathItemKey - * @param pathItem - */ - private preprocessPathLevelParameters( - pathItemKey: string, - pathItem: OpenAPIV3.PathItemObject, - ) { - const parameters = pathItem.parameters ?? []; - if (parameters.length === 0) return; + private getChildrenForOperation( + parent: VisitorNode<'operation'>, + ): VisitorNode[] { + const children = []; - const v = this.resolveSchema( - pathItem[pathItemKey], + children.push( + ...VisitorNode.fromParentArray(parent, 'parameter', 'parameters'), ); - if (v === parameters) return; - v.parameters = v.parameters || []; - - const match = ( - pathParam: OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject, - opParam: OpenAPIV3.ReferenceObject | OpenAPIV3.OperationObject, - ) => - // if name or ref exists and are equal - (opParam['name'] && opParam['name'] === pathParam['name']) || - (opParam['$ref'] && opParam['$ref'] === pathParam['$ref']); - - // Add Path level query param to list ONLY if there is not already an operation-level query param by the same name. - for (const param of parameters) { - if (!v.parameters.some((vparam) => match(param, vparam))) { - v.parameters.push(param); - } - } + + children.push(...VisitorNode.fromParent(parent, 'requestBody')); + children.push( + ...VisitorNode.fromParentDict(parent, 'response', 'responses'), + ); + children.push( + ...VisitorNode.fromParentDict(parent, 'callback', 'callbacks'), + ); + + return children; + } + + private getChildrenForRequestBody( + parent: VisitorNode<'requestBody'>, + ): VisitorNode[] { + const children = []; + + children.push( + ...VisitorNode.fromParentDict(parent, 'mediaType', 'content'), + ); + + return children; + } + + private getChildrenForResponse( + parent: VisitorNode<'response'>, + ): VisitorNode[] { + const children = []; + + children.push(...VisitorNode.fromParentDict(parent, 'header', 'headers')); + children.push( + ...VisitorNode.fromParentDict(parent, 'mediaType', 'content'), + ); + + return children; + } + + private getChildrenForEncoding( + parent: VisitorNode<'encoding'>, + ): VisitorNode[] { + const children = []; + + children.push(...VisitorNode.fromParentDict(parent, 'header', 'headers')); + + return children; + } + + private getChildrenForHeader( + parent: VisitorNode<'header'>, + ): VisitorNode[] { + const children = []; + + children.push(...VisitorNode.fromParent(parent, 'schema')); + children.push( + ...VisitorNode.fromParentDict(parent, 'mediaType', 'content'), + ); + + return children; } - private findKeys(object, searchFunc): string[] { - const matches = []; - if (!object) { - return matches; + private getChildrenForMediaType( + parent: VisitorNode<'mediaType'>, + ): VisitorNode[] { + const children = []; + + children.push(...VisitorNode.fromParent(parent, 'schema')); + children.push( + ...VisitorNode.fromParentDict(parent, 'encoding', 'encoding'), + ); + + return children; + } + + private getChildrenForParameter( + parent: VisitorNode<'parameter'>, + ): VisitorNode[] { + if (isReferenceObject(parent.object)) { + throw new Error('Object should have been unwrapped.'); } - const keys = Object.keys(object); - for (let i = 0; i < keys.length; i++) { - if (searchFunc(object[keys[i]])) { - matches.push(keys[i]); - } + + const children = []; + + /* + // TODO: Probably not correctly resolved in case of schema ref!!! + // path calculation is taken from the old code + children.push( + new VisitorNode('schema', parent.object.schema, [ + ...parent.path.slice(0, parent.path.length - 1), + parent.object.name, + parent.object.in, + ]), + ); + */ + children.push(...VisitorNode.fromParent(parent, 'schema')); + children.push( + ...VisitorNode.fromParentDict(parent, 'mediaType', 'content'), + ); + + return children; + } + + private getChildrenForCallback( + parent: VisitorNode<'callback'>, + ): VisitorNode[] { + if (isReferenceObject(parent.object)) { + throw new Error('Object should have been unwrapped.'); } + + const children = []; + + forEachValue(parent.object, (pathItem, key) => { + children.push( + new VisitorNode( + 'pathItem', + pathItem, + parent.responseObject[key], + parent.traversedObjects, + [...parent.path, key], + ), + ); + }); + + return children; + } +} + +function isDocumentV3_1( + document: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1, +): document is OpenAPIV3.DocumentV3_1 { + return document.openapi.startsWith('3.1.'); +} + +function isReferenceNode( + node: VisitorNode, +): node is VisitorNode { + return [ + 'pathItem', + 'schema', + 'requestBody', + 'response', + 'header', + 'parameter', + 'callback', + ].includes(node.type); +} + +function isReferenceObject( + object: VisitorObjects[SchemaType] | undefined, +): object is OpenAPIV3.ReferenceObject { + return !!object && '$ref' in object && !!object.$ref; +} + +function hasNodeType( + node: VisitorNode | undefined, + type: ObjectType, +): node is VisitorNode { + return node?.type === type; +} + +function forEachValue( + object: { [key: string]: Value }, + perform: (value: Value, key: string) => void, +): void { + Object.entries(object).forEach(([key, value]) => perform(value, key)); +} + +function findKeys( + object: { [value: string]: string }, + searchFunc: (key: string) => boolean, +): string[] { + const matches: string[] = []; + + if (!object) { return matches; } - getKeyFromRef(ref) { - return ref.split('/components/schemas/')[1]; + const keys = Object.keys(object); + for (let i = 0; i < keys.length; i++) { + if (searchFunc(object[keys[i]])) { + matches.push(keys[i]); + } } + + return matches; +} + +function getKeyFromRef(ref: string) { + return ref.split('/components/schemas/')[1]; } diff --git a/test/ignore.examples.spec.ts b/test/ignore.examples.spec.ts new file mode 100644 index 00000000..ee39c6bc --- /dev/null +++ b/test/ignore.examples.spec.ts @@ -0,0 +1,143 @@ +import * as request from 'supertest'; +import { createApp } from './common/app'; +import * as packageJson from '../package.json'; + +describe(packageJson.name, () => { + let app = null; + + before(async () => { + // set up express app + app = await createApp( + { + apiSpec: apiSpec(), + validateRequests: true, + validateResponses: true, + }, + 3001, + (app) => { + app.post('/ping', (req: any, res: any) => { + res.json({ + id: req.body.id, + message: 'Pong!', + }); + }); + }, + false, + ); + }); + + after(() => { + app.server.close(); + }); + + it('should not throw an error when more than one example uses the same the value for a property "id"', async () => + request(app) + .post('/ping') + .send({ id: 'id', message: 'Ping!' }) + .expect(200)); +}); + +function apiSpec(): any { + return { + openapi: '3.0.0', + info: { + version: 'v1', + title: 'Validation Error', + description: + 'A test spec that triggers an validation error on identical id fields in examples.', + }, + paths: { + '/ping': { + post: { + description: 'ping then pong!', + operationId: 'ping', + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Data', + }, + examples: { + request1: { + summary: 'Request 1', + value: { + id: 'Some_ID_A', + message: 'Ping!', + }, + }, + request2: { + summary: 'Request 2', + value: { + id: 'Some_ID_A', + message: 'Ping!', + }, + }, + }, + }, + }, + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Data', + }, + examples: { + response1: { + summary: 'Response 1', + value: { + id: 'Some_ID_B', + message: 'Pong!', + }, + }, + response2: { + summary: 'Response 2', + value: { + id: 'Some_ID_B', + message: 'Pong!', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Data: { + required: ['id', 'message'], + properties: { + id: { + type: 'string', + }, + message: { + type: 'string', + }, + }, + }, + }, + examples: { + example1: { + summary: 'Example 1', + value: { + id: 'Some_ID_C', + message: 'Example!', + }, + }, + response2: { + summary: 'Example 2', + value: { + id: 'Some_ID_C', + message: 'Example!', + }, + }, + }, + }, + }; +}