diff --git a/src/constants.ts b/src/constants.ts index 668b139a6..ed7d80309 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,7 @@ export const xParserOriginalPayload = 'x-parser-original-payload'; export const xParserOriginalTraits = 'x-parser-original-traits'; export const xParserCircular = 'x-parser-circular'; +export const xParserCircularProps = 'x-parser-circular-props'; export const EXTENSION_REGEX = /^x-[\w\d\.\-\_]+$/; diff --git a/src/models/v2/mixins.ts b/src/models/v2/mixins.ts index 76478cca1..3d084e4b6 100644 --- a/src/models/v2/mixins.ts +++ b/src/models/v2/mixins.ts @@ -52,9 +52,9 @@ export function hasExternalDocs(model: BaseModel<{ externalDocs?: v2.ExternalDoc return Object.keys(model.json('externalDocs') || {}).length > 0; }; -export function externalDocs(model: BaseModel): ExternalDocumentationInterface | undefined { +export function externalDocs(model: BaseModel<{ externalDocs?: v2.ExternalDocumentationObject }>): ExternalDocumentationInterface | undefined { if (hasExternalDocs(model)) { - return new ExternalDocumentation(model.json('externalDocs')); + return new ExternalDocumentation(model.json('externalDocs') as v2.ExternalDocumentationObject); } return; }; diff --git a/src/old-api/asyncapi.ts b/src/old-api/asyncapi.ts new file mode 100644 index 000000000..b12f18dbb --- /dev/null +++ b/src/old-api/asyncapi.ts @@ -0,0 +1,164 @@ +import { SpecificationExtensionsModel, hasExternalDocs, externalDocs, tagsMixins, createMapOfType, getMapValue } from './mixins'; +import { Info } from './info'; +import { Server } from './server'; +import { Channel } from './channel'; +import { Components } from './components'; +import { Message } from './message'; +import { Schema } from './schema'; + +import { xParserCircular } from '../constants'; +import { stringify, unstringify } from '../stringify'; + +import type { v2 } from '../spec-types'; +import type { Operation } from './operation'; + +export class AsyncAPIDocument extends SpecificationExtensionsModel { + version() { + return this._json.asyncapi; + } + + info() { + return new Info(this._json.info); + } + + id() { + return this._json.id; + } + + externalDocs() { + return externalDocs(this); + } + + hasExternalDocs() { + return hasExternalDocs(this); + } + + hasTags() { + return tagsMixins.hasTags(this); + } + + tags() { + return tagsMixins.tags(this); + } + + tagNames() { + return tagsMixins.tagNames(this); + } + + hasTag(name: string) { + return tagsMixins.hasTag(this, name); + } + + tag(name: string) { + return tagsMixins.tag(this, name); + } + + hasServers() { + return !!this._json.servers; + } + + servers() { + return createMapOfType(this._json.servers, Server); + } + + serverNames() { + if (!this._json.servers) return []; + return Object.keys(this._json.servers); + } + + server(name: string) { + return getMapValue(this._json.servers, name, Server); + } + + hasDefaultContentType() { + return !!this._json.defaultContentType; + } + + defaultContentType() { + return this._json.defaultContentType || null; + } + + hasChannels() { + return !!this._json.channels; + } + + channels() { + return createMapOfType(this._json.channels, Channel); + } + + channelNames() { + if (!this._json.channels) return []; + return Object.keys(this._json.channels); + } + + channel(name: string) { + return getMapValue(this._json.channels, name, Channel); + } + + hasComponents() { + return !!this._json.components; + } + + components() { + if (!this._json.components) return null; + return new Components(this._json.components); + } + + hasMessages() { + return !!this.allMessages().size; + } + + allMessages(): Map { + const messages = new Map(); + + if (this.hasChannels()) { + this.channelNames().forEach(channelName => { + const channel = this.channel(channelName); + if (channel) { + if (channel.hasPublish()) { + (channel.publish() as Operation).messages().forEach(m => { + messages.set(m.uid(), m); + }); + } + if (channel.hasSubscribe()) { + (channel.subscribe() as Operation).messages().forEach(m => { + messages.set(m.uid(), m); + }); + } + } + }); + } + if (this.hasComponents()) { + Object.values((this.components() as Components).messages()).forEach(m => { + messages.set(m.uid(), m); + }); + } + + return messages; + } + + // TODO: Retrieve all schemas + allSchemas(): Map { + const schemas = new Map(); + + return schemas; + } + + hasCircular() { + return !!this._json[xParserCircular]; + } + + // TODO: Make traversing for old API and enable that function + // traverseSchemas(callback, schemaTypesToIterate) { + // traverseAsyncApiDocument(this, callback, schemaTypesToIterate); + // } + + static stringify(doc: AsyncAPIDocument, space: number): string | undefined { + return stringify(doc, { space }); + } + + static parse(doc: string): AsyncAPIDocument | undefined { + const possibleDocument = unstringify(doc); + return possibleDocument ? new AsyncAPIDocument(possibleDocument.json()) : undefined; + } +} \ No newline at end of file diff --git a/src/old-api/base.ts b/src/old-api/base.ts new file mode 100644 index 000000000..fc5f26c7b --- /dev/null +++ b/src/old-api/base.ts @@ -0,0 +1,14 @@ +export abstract class Base = Record> { + constructor( + protected readonly _json: J, // TODO: Add error here like in original codebase + protected readonly _meta: M = {} as M, + ) {} + + json(): T; + json(key: K): J[K]; + json(key?: keyof J) { + if (key === undefined || typeof this._json !== 'object') return this._json; + if (!this._json) return; + return this._json[key]; + } +} diff --git a/src/old-api/channel-parameter.ts b/src/old-api/channel-parameter.ts new file mode 100644 index 000000000..8e70aa2c4 --- /dev/null +++ b/src/old-api/channel-parameter.ts @@ -0,0 +1,23 @@ +import { SpecificationExtensionsModel, description, hasDescription } from './mixins'; +import { Schema } from './schema'; + +import type { v2 } from '../spec-types'; + +export class ChannelParameter extends SpecificationExtensionsModel { + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + schema() { + if (!this._json.schema) return null; + return new Schema(this._json.schema as any); + } + + location() { + return this._json.location; + } +} \ No newline at end of file diff --git a/src/old-api/channel.ts b/src/old-api/channel.ts new file mode 100644 index 000000000..068fc52c5 --- /dev/null +++ b/src/old-api/channel.ts @@ -0,0 +1,81 @@ +import { SpecificationExtensionsModel, hasDescription, description, createMapOfType, bindingsMixins, getMapValue } from './mixins'; +import { ChannelParameter } from './channel-parameter'; +import { Operation } from './operation'; + +import type { v2 } from '../spec-types'; + +export class Channel extends SpecificationExtensionsModel { + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + hasParameters() { + return !!this._json.parameters; + } + + parameters() { + return createMapOfType(this._json.parameters as Record, ChannelParameter); + } + + parameter(name: string) { + return getMapValue(this._json.parameters as Record, name, ChannelParameter); + } + + hasServers() { + return !!this._json.servers; + } + + servers() { + if (!this._json.servers) return []; + return this._json.servers; + } + + server(index: number | string) { + if (!this._json.servers) return null; + if (typeof index !== 'number') return null; + if (index > this._json.servers.length - 1) return null; + return this._json.servers[+index]; + } + + publish() { + if (!this._json.publish) return null; + return new Operation(this._json.publish, { kind: 'publish' }); + } + + subscribe() { + if (!this._json.subscribe) return null; + return new Operation(this._json.subscribe, { kind: 'subscribe' }); + } + + hasPublish() { + return !!this._json.publish; + } + + hasSubscribe() { + return !!this._json.subscribe; + } + + hasBindings() { + return bindingsMixins.hasBindings(this as any); + } + + bindings() { + return bindingsMixins.bindings(this as any); + } + + bindingProtocols() { + return bindingsMixins.bindingProtocols(this as any); + } + + hasBinding(name: string): boolean { + return bindingsMixins.hasBinding(this as any, name); + } + + binding(name: string) { + return bindingsMixins.binding(this as any, name); + } +} \ No newline at end of file diff --git a/src/old-api/components.ts b/src/old-api/components.ts new file mode 100644 index 000000000..e4db7cb41 --- /dev/null +++ b/src/old-api/components.ts @@ -0,0 +1,135 @@ +import { SpecificationExtensionsModel, createMapOfType, getMapValue } from './mixins'; +import { Channel } from './channel'; +import { Message } from './message'; +import { Schema } from './schema'; +import { SecurityScheme } from './security-scheme'; +import { Server } from './server'; +import { ChannelParameter } from './channel-parameter'; +import { CorrelationId } from './correlation-id'; +import { OperationTrait } from './operation-trait'; +import { MessageTrait } from './message-trait'; +import { ServerVariable } from './server-variable'; + +import type { v2 } from '../spec-types'; + +export class Components extends SpecificationExtensionsModel { + hasChannels() { + return !!this._json.channels; + } + + channels() { + return createMapOfType(this._json.channels as Record, Channel); + } + + channel(name: string) { + return getMapValue(this._json.channels as Record, name, Channel); + } + + hasMessages() { + return !!this._json.messages; + } + + messages() { + return createMapOfType(this._json.messages as Record, Message); + } + + message(name: string) { + return getMapValue(this._json.messages as Record, name, Message); + } + + hasSchemas() { + return !!this._json.schemas; + } + + schemas() { + return createMapOfType(this._json.schemas as any, Schema); + } + + schema(name: string) { + return getMapValue(this._json.schemas as any, name, Schema); + } + + hasSecuritySchemes() { + return !!this._json.securitySchemes; + } + + securitySchemes() { + return createMapOfType(this._json.securitySchemes as Record, SecurityScheme); + } + + securityScheme(name: string) { + return getMapValue(this._json.securitySchemes as Record, name, SecurityScheme); + } + + hasServers() { + return !!this._json.servers; + } + + servers() { + return createMapOfType(this._json.servers as Record, Server); + } + + server(name: string) { + return getMapValue(this._json.servers as Record, name, Server); + } + + hasParameters() { + return !!this._json.parameters; + } + + parameters() { + return createMapOfType(this._json.parameters as Record, ChannelParameter); + } + + parameter(name: string) { + return getMapValue(this._json.parameters as Record, name, ChannelParameter); + } + + hasCorrelationIds() { + return !!this._json.correlationIds; + } + + correlationIds() { + return createMapOfType(this._json.correlationIds as Record, CorrelationId); + } + + correlationId(name: string) { + return getMapValue(this._json.correlationIds as Record, name, CorrelationId); + } + + hasOperationTraits() { + return !!this._json.operationTraits; + } + + operationTraits() { + return createMapOfType(this._json.operationTraits as Record, OperationTrait); + } + + operationTrait(name: string) { + return getMapValue(this._json.operationTraits as Record, name, OperationTrait); + } + + hasMessageTraits() { + return !!this._json.messageTraits; + } + + messageTraits() { + return createMapOfType(this._json.messageTraits as Record, MessageTrait); + } + + messageTrait(name: string) { + return getMapValue(this._json.messageTraits as Record, name, MessageTrait); + } + + hasServerVariables() { + return !!this._json.serverVariables; + } + + serverVariables() { + return createMapOfType(this._json.serverVariables as Record, ServerVariable); + } + + serverVariable(name: string) { + return getMapValue(this._json.serverVariables as Record, name, ServerVariable); + } +} \ No newline at end of file diff --git a/src/old-api/contact.ts b/src/old-api/contact.ts new file mode 100644 index 000000000..b2ff33346 --- /dev/null +++ b/src/old-api/contact.ts @@ -0,0 +1,17 @@ +import { SpecificationExtensionsModel } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class Contact extends SpecificationExtensionsModel { + name() { + return this._json.name; + } + + url() { + return this._json.url; + } + + email() { + return this._json.email; + } +} \ No newline at end of file diff --git a/src/old-api/correlation-id.ts b/src/old-api/correlation-id.ts new file mode 100644 index 000000000..7f43d5bca --- /dev/null +++ b/src/old-api/correlation-id.ts @@ -0,0 +1,17 @@ +import { SpecificationExtensionsModel, description, hasDescription } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class CorrelationId extends SpecificationExtensionsModel { + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + location() { + return this._json.location; + } +} \ No newline at end of file diff --git a/src/old-api/external-docs.ts b/src/old-api/external-docs.ts new file mode 100644 index 000000000..1602bbcc7 --- /dev/null +++ b/src/old-api/external-docs.ts @@ -0,0 +1,17 @@ +import { SpecificationExtensionsModel, description, hasDescription } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class ExternalDocs extends SpecificationExtensionsModel { + url() { + return this._json.url; + } + + hasDescription() { + return hasDescription(this); + } + + description() { + return description(this); + } +} \ No newline at end of file diff --git a/src/old-api/info.ts b/src/old-api/info.ts new file mode 100644 index 000000000..3a4fa1d23 --- /dev/null +++ b/src/old-api/info.ts @@ -0,0 +1,37 @@ +import { Contact } from './contact'; +import { License } from './license'; +import { SpecificationExtensionsModel, description, hasDescription } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class Info extends SpecificationExtensionsModel { + title() { + return this._json.title; + } + + version() { + return this._json.version; + } + + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + termsOfService() { + return this._json.termsOfService; + } + + license() { + if (!this._json.license) return null; + return new License(this._json.license); + } + + contact() { + if (!this._json.contact) return null; + return new Contact(this._json.contact); + } +} \ No newline at end of file diff --git a/src/old-api/license.ts b/src/old-api/license.ts new file mode 100644 index 000000000..c39ce3330 --- /dev/null +++ b/src/old-api/license.ts @@ -0,0 +1,13 @@ +import { SpecificationExtensionsModel } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class License extends SpecificationExtensionsModel { + name() { + return this._json.name; + } + + url() { + return this._json.url; + } +} \ No newline at end of file diff --git a/src/old-api/message-trait.ts b/src/old-api/message-trait.ts new file mode 100644 index 000000000..5feec3545 --- /dev/null +++ b/src/old-api/message-trait.ts @@ -0,0 +1,106 @@ +import { SpecificationExtensionsModel, description, hasDescription, hasExternalDocs, externalDocs, tagsMixins, bindingsMixins, getMapValue } from './mixins'; +import { CorrelationId } from './correlation-id'; +import { Schema } from './schema'; + +import type { v2 } from '../spec-types'; + +export class MessageTrait extends SpecificationExtensionsModel { + id() { + return this._json.messageId; + } + + headers() { + if (!this._json.headers) return null; + return new Schema(this._json.headers as any); + } + + header(name: string) { + if (!this._json.headers) return null; + return getMapValue((this._json.headers as any).properties || {}, name, Schema); + } + + correlationId() { + if (!this._json.correlationId) return null; + return new CorrelationId(this._json.correlationId as v2.CorrelationIDObject); + } + + schemaFormat() { + return this._json.schemaFormat as string; // Old API points always to the default schema format for given AsyncAPI version, so we need to force returned type as string. + } + + contentType() { + return this._json.contentType; + } + + name() { + return this._json.name; + } + + title() { + return this._json.title; + } + + summary() { + return this._json.summary; + } + + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + externalDocs() { + return externalDocs(this); + } + + hasExternalDocs() { + return hasExternalDocs(this); + } + + hasTags() { + return tagsMixins.hasTags(this); + } + + tags() { + return tagsMixins.tags(this); + } + + tagNames() { + return tagsMixins.tagNames(this); + } + + hasTag(name: string) { + return tagsMixins.hasTag(this, name); + } + + tag(name: string) { + return tagsMixins.tag(this, name); + } + + hasBindings() { + return bindingsMixins.hasBindings(this as any); + } + + bindings() { + return bindingsMixins.bindings(this as any); + } + + bindingProtocols() { + return bindingsMixins.bindingProtocols(this as any); + } + + hasBinding(name: string): boolean { + return bindingsMixins.hasBinding(this as any, name); + } + + binding(name: string) { + return bindingsMixins.binding(this as any, name); + } + + examples() { + return this._json.examples; + } +} diff --git a/src/old-api/message.ts b/src/old-api/message.ts new file mode 100644 index 000000000..9211d206c --- /dev/null +++ b/src/old-api/message.ts @@ -0,0 +1,33 @@ +import { MessageTrait } from './message-trait'; +import { Schema } from './schema'; + +import type { v2 } from '../spec-types'; + +export class Message extends MessageTrait { + uid() { + return this.id() || this.name() || this.ext('x-parser-message-name') as string || Buffer.from(JSON.stringify(this._json)).toString('base64'); + } + + payload() { + if (!this._json.payload) return null; + return new Schema(this._json.payload); + } + + traits() { + const traits: v2.MessageTraitObject[] = this._json['x-parser-original-traits'] || this._json.traits; + if (!traits) return []; + return traits.map(t => new MessageTrait(t)); + } + + hasTraits() { + return !!this._json['x-parser-original-traits'] || !!this._json.traits; + } + + originalPayload() { + return this._json['x-parser-original-payload'] || this.payload(); + } + + originalSchemaFormat() { + return this._json['x-parser-original-schema-format'] as string || this.schemaFormat(); + } +} diff --git a/src/old-api/mixins.ts b/src/old-api/mixins.ts new file mode 100644 index 000000000..6c5c19682 --- /dev/null +++ b/src/old-api/mixins.ts @@ -0,0 +1,145 @@ +import { Base } from './base'; +import { ExternalDocs } from './external-docs'; +import { Tag } from './tag'; + +import { EXTENSION_REGEX } from '../constants'; + +import type { v2 } from '../spec-types'; + +export abstract class SpecificationExtensionsModel = Record> extends Base { + hasExtensions() { + return !!this.extensionKeys().length; + } + + extensions(): v2.SpecificationExtensions { + const result: v2.SpecificationExtensions = {}; + Object.entries(this._json).forEach(([key, value]) => { + if (EXTENSION_REGEX.test(key)) { + result[key as `x-`] = value; + } + }); + return result; + } + + extensionKeys() { + return Object.keys(this.extensions()); + } + + extKeys() { + return this.extensionKeys(); + } + + hasExtension(extension: string) { + if (!extension.startsWith('x-')) { + return false; + } + return !!(this._json as Record)[extension]; + } + + extension(extension: string): v2.SpecificationExtension { + if (!extension.startsWith('x-')) { + return null; + } + return (this._json as Record)[extension]; + } + + hasExt(extension: string) { + return this.hasExtension(extension); + } + + ext(extension: string) { + return this.extension(extension); + } +} + +export function hasDescription(model: Base<{ description?: string }>) { + return Boolean(model.json('description')); +}; + +export function description(model: Base<{ description?: string }>): string | undefined { + return model.json('description'); +} + +export function hasExternalDocs(model: Base<{ externalDocs?: v2.ExternalDocumentationObject }>): boolean { + return Object.keys(model.json('externalDocs') || {}).length > 0; +}; + +export function externalDocs(model: Base<{ externalDocs?: v2.ExternalDocumentationObject }>): ExternalDocs | undefined { + if (hasExternalDocs(model)) { + return new ExternalDocs(model.json('externalDocs') as v2.ExternalDocumentationObject); + } + return; +}; + +export const bindingsMixins = { + hasBindings(model: Base<{ bindings?: Record }>) { + return !!Object.keys(bindingsMixins.bindings(model)).length; + }, + + bindings(model: Base<{ bindings?: Record }>): Record { + return model.json('bindings') || {} as Record; + }, + + bindingProtocols(model: Base<{ bindings?: Record }>) { + return Object.keys(bindingsMixins.bindings(model)); + }, + + hasBinding(model: Base<{ bindings?: Record }>, name: string): boolean { + return !!bindingsMixins.binding(model, name); + }, + + binding(model: Base<{ bindings?: Record }>, name: string) { + return getMapValue(model.json('bindings'), name); + }, +} + +export const tagsMixins = { + hasTags(model: Base<{ tags?: Array }>): boolean { + return !!tagsMixins.tags(model).length; + }, + + tags(model: Base<{ tags?: Array }>): Array { + const tags = model.json('tags'); + return tags ? tags.map(t => new Tag(t)) : []; + }, + + tagNames(model: Base<{ tags?: Array }>) { + const tags = model.json('tags'); + return tags ? tags.map(t => t.name) : []; + }, + + hasTag(model: Base<{ tags?: Array }>, name: string): boolean { + return !!tagsMixins.tag(model, name); + }, + + tag(model: Base<{ tags?: Array }>, name: string) { + const tg = (model.json('tags') || []).find(t => t.name === name); + return tg ? new Tag(tg) : null; + }, +} + +interface Constructor extends Function { + new (...any: any[]): T; +} +type InferModelData = T extends Base ? J : never; +type InferModelMetadata = T extends Base ? M : never; + +export function getMapValue, K extends keyof T>(obj: T | undefined, key: K): T[K] | null; +export function getMapValue(obj: Record> | undefined, key: string, Type: Constructor, meta?: InferModelMetadata): T | null; +export function getMapValue(obj: Record> | undefined, key: string, Type?: Constructor, meta?: InferModelMetadata) { + if (typeof key !== 'string' || !obj) return null; + const v = obj[String(key)]; + if (v === undefined) return null; + return Type ? new Type(v, meta) : v; +}; + +export function createMapOfType(obj: Record> | undefined, Type: Constructor, meta?: InferModelMetadata): Record { + const result: Record = {}; + if (!obj) return result; + + Object.entries(obj).forEach(([key, value]) => { + result[key] = new Type(value, meta); + }); + + return result; +}; diff --git a/src/old-api/oauth-flow.ts b/src/old-api/oauth-flow.ts new file mode 100644 index 000000000..b8e6574f6 --- /dev/null +++ b/src/old-api/oauth-flow.ts @@ -0,0 +1,21 @@ +import { SpecificationExtensionsModel } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class OAuthFlow extends SpecificationExtensionsModel { + authorizationUrl() { + return this._json.authorizationUrl; + } + + tokenUrl() { + return this._json.tokenUrl; + } + + refreshUrl() { + return this._json.refreshUrl; + } + + scopes() { + return this._json.scopes; + } +} \ No newline at end of file diff --git a/src/old-api/operation-trait.ts b/src/old-api/operation-trait.ts new file mode 100644 index 000000000..964e1ac4e --- /dev/null +++ b/src/old-api/operation-trait.ts @@ -0,0 +1,87 @@ +import { SpecificationExtensionsModel, description, hasDescription, hasExternalDocs, externalDocs, tagsMixins, bindingsMixins } from './mixins'; +import { SecurityRequirement } from './security-requirement'; + +import type { v2 } from '../spec-types'; + +export class OperationTrait extends SpecificationExtensionsModel { + isPublish() { + return this._meta.kind === 'publish'; + } + + isSubscribe() { + return this._meta.kind === 'subscribe'; + } + + kind() { + return this._meta.kind; + } + + id() { + return this._json.operationId; + } + + summary() { + return this._json.summary; + } + + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + externalDocs() { + return externalDocs(this); + } + + hasExternalDocs() { + return hasExternalDocs(this); + } + + hasTags() { + return tagsMixins.hasTags(this); + } + + tags() { + return tagsMixins.tags(this); + } + + tagNames() { + return tagsMixins.tagNames(this); + } + + hasTag(name: string) { + return tagsMixins.hasTag(this, name); + } + + tag(name: string) { + return tagsMixins.tag(this, name); + } + + hasBindings() { + return bindingsMixins.hasBindings(this as any); + } + + bindings() { + return bindingsMixins.bindings(this as any); + } + + bindingProtocols() { + return bindingsMixins.bindingProtocols(this as any); + } + + hasBinding(name: string): boolean { + return bindingsMixins.hasBinding(this as any, name); + } + + binding(name: string) { + return bindingsMixins.binding(this as any, name); + } + + security() { + if (!this._json.security) return null; + return this._json.security.map(sec => new SecurityRequirement(sec)); + } +} diff --git a/src/old-api/operation.ts b/src/old-api/operation.ts new file mode 100644 index 000000000..b456d12b3 --- /dev/null +++ b/src/old-api/operation.ts @@ -0,0 +1,39 @@ +import { OperationTrait } from './operation-trait'; +import { Message } from './message'; + +import type { v2 } from '../spec-types'; + +export class Operation extends OperationTrait { + traits() { + const traits: v2.OperationTraitObject[] = this._json['x-parser-original-traits'] || this._json.traits; + if (!traits) return []; + return traits.map(t => new OperationTrait(t)); + } + + hasTraits() { + return !!this._json['x-parser-original-traits'] || !!this._json.traits; + } + + hasMultipleMessages() { + const message = this._json.message as v2.MessageObject | { oneOf: v2.MessageObject[] } + if (message && (message as { oneOf: v2.MessageObject[] }).oneOf && (message as { oneOf: v2.MessageObject[] }).oneOf.length > 1) return true; + return false; + } + + messages() { + const message = this._json.message as { oneOf: v2.MessageObject[] } + if (!message) return []; + if (message.oneOf) return message.oneOf.map(m => new Message(m)); + return [new Message(message)]; + } + + message(index?: number | string) { + const message = this._json.message as v2.MessageObject | { oneOf: v2.MessageObject[] } + if (!message) return null; + if ((message as { oneOf: v2.MessageObject[] }).oneOf && (message as { oneOf: v2.MessageObject[] }).oneOf.length === 1) return new Message((message as { oneOf: v2.MessageObject[] }).oneOf[0]); + if (!(message as { oneOf: v2.MessageObject[] }).oneOf) return new Message(message); + if (typeof index !== 'number') return null; + if (index > (message as { oneOf: v2.MessageObject[] }).oneOf.length - 1) return null; + return new Message((message as { oneOf: v2.MessageObject[] }).oneOf[+index]); + } +} diff --git a/src/old-api/schema.ts b/src/old-api/schema.ts new file mode 100644 index 000000000..dc3711ddd --- /dev/null +++ b/src/old-api/schema.ts @@ -0,0 +1,290 @@ +import { SpecificationExtensionsModel, createMapOfType, getMapValue, description, hasDescription, hasExternalDocs, externalDocs } from './mixins'; +import { xParserCircular, xParserCircularProps } from '../constants'; + +import type { Base } from './base'; +import type { v2 } from '../spec-types'; + +export class Schema extends SpecificationExtensionsModel { + uid() { + return this.$id() || this.ext('x-parser-schema-id'); + } + + $id() { + return this.__get('$id'); + } + + multipleOf() { + return this.__get('multipleOf'); + } + + maximum() { + return this.__get('maximum'); + } + + exclusiveMaximum() { + return this.__get('exclusiveMaximum'); + } + + minimum() { + return this.__get('minimum'); + } + + exclusiveMinimum() { + return this.__get('exclusiveMinimum'); + } + + maxLength() { + return this.__get('maxLength'); + } + + minLength() { + return this.__get('minLength'); + } + + pattern() { + return this.__get('pattern'); + } + + maxItems() { + return this.__get('maxItems'); + } + + minItems() { + return this.__get('minItems'); + } + + uniqueItems() { + return this.__get('uniqueItems'); + } + + maxProperties() { + return this.__get('maxProperties'); + } + + minProperties() { + return this.__get('minProperties'); + } + + required() { + return this.__get('required'); + } + + enum() { + return this.__get('enum'); + } + + type() { + return this.__get('type'); + } + + allOf() { + const allOf = this.__get('allOf'); + return !allOf ? null : allOf.map(this.__createChild); + } + + oneOf() { + const oneOf = this.__get('oneOf'); + return !oneOf ? null : oneOf.map(this.__createChild); + } + + anyOf() { + const anyOf = this.__get('anyOf'); + return !anyOf ? null : anyOf.map(this.__createChild); + } + + not() { + const not = this.__get('not'); + return !not ? null : this.__createChild(not); + } + + items() { + const items = this.__get('items'); + if (!items) return null; + if (Array.isArray(items)) { + return items.map(this.__createChild); + } + return this.__createChild(items); + } + + properties() { + return createMapOfType(this.__get('properties') as any, Schema, { parent: this }); + } + + property(name: string) { + return getMapValue(this.__get('properties') as any, name, Schema, { parent: this }); + } + + additionalProperties() { + if (typeof this._json === 'boolean') return this._json; + const additionalProperties = this.__get('additionalProperties'); + if (additionalProperties === undefined) return true; + return new Schema(additionalProperties as any, { parent: this }); + } + + additionalItems() { + if (typeof this._json === 'boolean') return this._json; + const additionalItems = this.__get('additionalItems'); + if (additionalItems === undefined) return true; + return new Schema(additionalItems as any, { parent: this }); + } + + patternProperties() { + return createMapOfType(this.__get('patternProperties') as any, Schema, { parent: this }); + } + + const() { + return this.__get('const'); + } + + contains() { + const contains = this.__get('contains'); + return typeof contains === 'undefined' ? null : this.__createChild(contains); + } + + dependencies() { + const dependencies = this.__get('dependencies'); + if (!dependencies) return null; + const result: Record = {}; + Object.entries(dependencies).forEach(([key, value]) => { + result[key] = !Array.isArray(value) ? this.__createChild(value) : value; + }); + return result; + } + + propertyNames() { + const propertyNames = this.__get('propertyNames'); + return typeof propertyNames === 'undefined' ? null : this.__createChild(propertyNames); + } + + if() { + const _if = this.__get('if'); + return typeof _if === 'undefined' ? null : this.__createChild(_if); + } + + then() { + const _then = this.__get('then'); + return typeof _then === 'undefined' ? null : this.__createChild(_then); + } + + else() { + const _else = this.__get('else'); + return typeof _else === 'undefined' ? null : this.__createChild(_else); + } + + format() { + return this.__get('format'); + } + + contentEncoding() { + return this.__get('contentEncoding'); + } + + contentMediaType() { + return this.__get('contentMediaType'); + } + + definitions() { + return createMapOfType(this.__get('definitions') as any, Schema, { parent: this }); + } + + title() { + return this.__get('title'); + } + + default() { + return this.__get('default'); + } + + deprecated() { + return this.__get('deprecated'); + } + + discriminator() { + return this.__get('discriminator'); + } + + readOnly() { + return this.__get('readOnly'); + } + + writeOnly() { + return this.__get('writeOnly'); + } + + examples() { + return this.__get('examples'); + } + + isBooleanSchema() { + return typeof this._json === 'boolean'; + } + + description() { + return description(this as Base); + } + + hasDescription() { + return hasDescription(this as Base); + } + + externalDocs() { + return externalDocs(this as Base); + } + + hasExternalDocs() { + return hasExternalDocs(this as Base); + } + + isCircular() { + if (!!this.ext(xParserCircular)) { + return true; + } + + let parent = this._meta.parent; + while (parent) { + if (parent._json === this._json) return true; + parent = parent._meta && parent._meta.parent; + } + return false; + } + + circularSchema() { + let parent = this._meta.parent; + while (parent) { + if (parent._json === this._json) return parent; + parent = parent._meta && parent._meta.parent; + } + } + + hasCircularProps() { + if (Array.isArray(this.ext(xParserCircularProps))) { + return this.ext(xParserCircularProps).length > 0; + } + return Object.entries(this.properties() || {}) + .map(([propertyName, property]) => { + if (property.isCircular()) return propertyName; + }) + .filter(Boolean) + .length > 0; + } + + circularProps() { + if (Array.isArray(this.ext(xParserCircularProps))) { + return this.ext(xParserCircularProps); + } + return Object.entries(this.properties() || {}) + .map(([propertyName, property]) => { + if (property.isCircular()) return propertyName; + }) + .filter(Boolean); + } + + protected __get(key: K): v2.AsyncAPISchemaDefinition[K] | undefined { + if (typeof this._json === 'boolean') return; + return this._json[key]; + } + + protected __createChild(s: v2.AsyncAPISchemaObject) { + return new Schema(s as any, { parent: this }) + } +} \ No newline at end of file diff --git a/src/old-api/security-requirement.ts b/src/old-api/security-requirement.ts new file mode 100644 index 000000000..2fcd885a2 --- /dev/null +++ b/src/old-api/security-requirement.ts @@ -0,0 +1,5 @@ +import { Base } from './base'; + +import type { v2 } from '../spec-types'; + +export class SecurityRequirement extends Base {} \ No newline at end of file diff --git a/src/old-api/security-scheme.ts b/src/old-api/security-scheme.ts new file mode 100644 index 000000000..9f4a70273 --- /dev/null +++ b/src/old-api/security-scheme.ts @@ -0,0 +1,42 @@ +import { SpecificationExtensionsModel, description, hasDescription, createMapOfType } from './mixins'; +import { OAuthFlow } from './oauth-flow'; + +import type { v2 } from '../spec-types'; + +export class SecurityScheme extends SpecificationExtensionsModel { // TODO: Add bindings and tags + type() { + return this._json.type; + } + + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + name() { + return this._json.name; + } + + in() { + return this._json.in; + } + + scheme() { + return this._json.scheme; + } + + bearerFormat() { + return this._json.bearerFormat; + } + + openIdConnectUrl() { + return this._json.openIdConnectUrl; + } + + flows() { + return createMapOfType(this._json.flows as Record, OAuthFlow); + } +} diff --git a/src/old-api/server-variable.ts b/src/old-api/server-variable.ts new file mode 100644 index 000000000..7d12a3f13 --- /dev/null +++ b/src/old-api/server-variable.ts @@ -0,0 +1,38 @@ +import { SpecificationExtensionsModel, description, hasDescription } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class ServerVariable extends SpecificationExtensionsModel { + allowedValues() { + return this._json.enum; + } + + allows(name: string) { + if (this._json.enum === undefined) return true; + return this._json.enum.includes(name); + } + + hasAllowedValues() { + return this._json.enum !== undefined; + } + + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + defaultValue() { + return this._json.default; + } + + hasDefaultValue() { + return this._json.default !== undefined; + } + + examples() { + return this._json.examples; + } +} diff --git a/src/old-api/server.ts b/src/old-api/server.ts new file mode 100644 index 000000000..7b553fe34 --- /dev/null +++ b/src/old-api/server.ts @@ -0,0 +1,64 @@ +import { SpecificationExtensionsModel, description, hasDescription, createMapOfType, bindingsMixins, getMapValue } from './mixins'; +import { ServerVariable } from './server-variable'; +import { SecurityRequirement } from './security-requirement'; + +import type { v2 } from '../spec-types'; + +export class Server extends SpecificationExtensionsModel { + url() { + return this._json.url; + } + + protocol() { + return this._json.protocol; + } + + protocolVersion() { + return this._json.protocolVersion; + } + + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + variables() { + return createMapOfType(this._json.variables, ServerVariable); + } + + variable(name: string) { + return getMapValue(this._json.variables, name, ServerVariable); + } + + hasVariables() { + return !!this._json.variables; + } + + security() { + if (!this._json.security) return null; + return this._json.security.map(sec => new SecurityRequirement(sec)); + } + + hasBindings() { + return bindingsMixins.hasBindings(this as any); + } + + bindings() { + return bindingsMixins.bindings(this as any); + } + + bindingProtocols() { + return bindingsMixins.bindingProtocols(this as any); + } + + hasBinding(name: string): boolean { + return bindingsMixins.hasBinding(this as any, name); + } + + binding(name: string) { + return bindingsMixins.binding(this as any, name); + } +} diff --git a/src/old-api/tag.ts b/src/old-api/tag.ts new file mode 100644 index 000000000..578143700 --- /dev/null +++ b/src/old-api/tag.ts @@ -0,0 +1,25 @@ +import { SpecificationExtensionsModel, hasDescription, description, hasExternalDocs, externalDocs } from './mixins'; + +import type { v2 } from '../spec-types'; + +export class Tag extends SpecificationExtensionsModel { + name() { + return this._json.name; + } + + description() { + return description(this); + } + + hasDescription() { + return hasDescription(this); + } + + externalDocs() { + return externalDocs(this); + } + + hasExternalDocs() { + return hasExternalDocs(this); + } +}