diff --git a/src/models/asyncapi.ts b/src/models/asyncapi.ts index 4787ad961..3bed0ec48 100644 --- a/src/models/asyncapi.ts +++ b/src/models/asyncapi.ts @@ -9,9 +9,9 @@ import type { SchemasInterface } from './schemas'; import type { SecuritySchemesInterface } from './security-schemes'; import type { ServersInterface } from './servers'; -import type { v2 } from '../spec-types'; +import type { AsyncAPIObject } from '../types'; -export interface AsyncAPIDocumentInterface extends BaseModel, ExtensionsMixinInterface { +export interface AsyncAPIDocumentInterface extends BaseModel, ExtensionsMixinInterface { version(): string; defaultContentType(): string | undefined; hasDefaultContentType(): boolean; diff --git a/src/models/v3/asyncapi.ts b/src/models/v3/asyncapi.ts index f26a22e98..29e3b8687 100644 --- a/src/models/v3/asyncapi.ts +++ b/src/models/v3/asyncapi.ts @@ -1,7 +1,55 @@ import { BaseModel } from '../base'; -export class AsyncAPIDocument extends BaseModel { +import type { AsyncAPIDocumentInterface } from '../asyncapi'; + +import type { v3 } from '../../spec-types'; + +export class AsyncAPIDocument extends BaseModel implements AsyncAPIDocumentInterface { version(): string { return this._json.asyncapi; } + + defaultContentType(): string | undefined { + return this._json.defaultContentType; + } + + hasDefaultContentType(): boolean { + return !!this._json.defaultContentType; + } + + info() { + return null as any; + } + + servers() { + return null as any; + } + + channels() { + return null as any; + } + + operations() { + return null as any; + } + + messages() { + return null as any; + } + + schemas() { + return null as any; + } + + securitySchemes() { + return null as any; + } + + components() { + return null as any; + } + + extensions() { + return null as any; + } } diff --git a/src/models/v3/binding.ts b/src/models/v3/binding.ts new file mode 100644 index 000000000..56fde118c --- /dev/null +++ b/src/models/v3/binding.ts @@ -0,0 +1,28 @@ +import { BaseModel } from '../base'; + +import { extensions } from './mixins'; + +import type { BindingInterface } from '../binding'; +import type { ExtensionsInterface } from '../extensions'; + +import type { v3 } from '../../spec-types'; + +export class Binding = Record> extends BaseModel implements BindingInterface { + protocol(): string { + return this._meta.protocol; + } + + version(): string { + return this._json.bindingVersion || 'latest'; + } + + value(): V { + const value = { ...this._json }; + delete (value as any).bindingVersion; + return value as unknown as V; + } + + extensions(): ExtensionsInterface { + return extensions(this); + } +} diff --git a/src/models/v3/bindings.ts b/src/models/v3/bindings.ts new file mode 100644 index 000000000..c17208d60 --- /dev/null +++ b/src/models/v3/bindings.ts @@ -0,0 +1,31 @@ +import { Collection } from '../collection'; +import { Extensions } from './extensions'; +import { Extension } from './extension'; + +import { createModel } from '../utils'; +import { EXTENSION_REGEX } from '../../constants'; + +import type { BindingsInterface } from '../bindings'; +import type { BindingInterface } from '../binding'; +import type { ExtensionsInterface } from '../extensions'; +import type { ExtensionInterface } from '../extension'; + +import type { v3 } from '../../spec-types'; + +export class Bindings extends Collection implements BindingsInterface { + override get = Record>(name: string): BindingInterface | undefined { + return this.collections.find(binding => binding.protocol() === name); + } + + extensions(): ExtensionsInterface { + const extensions: ExtensionInterface[] = []; + Object.entries(this._meta.originalData as v3.SpecificationExtensions || {}).forEach(([id, value]) => { + if (EXTENSION_REGEX.test(id)) { + extensions.push( + createModel(Extension, value, { id, pointer: `${this._meta.pointer}/${id}`, asyncapi: this._meta.asyncapi }) as Extension + ); + } + }); + return new Extensions(extensions); + } +} diff --git a/src/models/v3/extension.ts b/src/models/v3/extension.ts new file mode 100644 index 000000000..d747afc2d --- /dev/null +++ b/src/models/v3/extension.ts @@ -0,0 +1,19 @@ +import { BaseModel } from '../base'; + +import type { ExtensionInterface } from '../extension'; + +import type { v3 } from '../../spec-types'; + +export class Extension extends BaseModel, { id: string }> implements ExtensionInterface { + id(): string { + return this._meta.id; + } + + version(): string { + return 'to implement'; + } + + value(): V { + return this._json as unknown as V; + } +} diff --git a/src/models/v3/extensions.ts b/src/models/v3/extensions.ts new file mode 100644 index 000000000..e529b35d3 --- /dev/null +++ b/src/models/v3/extensions.ts @@ -0,0 +1,11 @@ +import { Collection } from '../collection'; + +import type { ExtensionsInterface } from '../extensions'; +import type { ExtensionInterface } from '../extension'; + +export class Extensions extends Collection implements ExtensionsInterface { + override get(id: string): ExtensionInterface | undefined { + id = id.startsWith('x-') ? id : `x-${id}`; + return this.collections.find(ext => ext.id() === id); + } +} diff --git a/src/models/v3/external-docs.ts b/src/models/v3/external-docs.ts new file mode 100644 index 000000000..edafb417c --- /dev/null +++ b/src/models/v3/external-docs.ts @@ -0,0 +1,26 @@ +import { BaseModel } from '../base'; + +import { hasDescription, description, extensions } from './mixins'; + +import type { ExternalDocumentationInterface } from '../external-docs'; +import type { ExtensionsInterface } from '../extensions'; + +import type { v3 } from '../../spec-types'; + +export class ExternalDocumentation extends BaseModel implements ExternalDocumentationInterface { + url(): string { + return this._json.url; + } + + hasDescription(): boolean { + return hasDescription(this); + } + + description(): string | undefined { + return description(this); + } + + extensions(): ExtensionsInterface { + return extensions(this); + } +} \ No newline at end of file diff --git a/src/models/v3/mixins.ts b/src/models/v3/mixins.ts new file mode 100644 index 000000000..405d1cd6b --- /dev/null +++ b/src/models/v3/mixins.ts @@ -0,0 +1,67 @@ +import { Bindings } from './bindings'; +import { Binding } from './binding'; +import { Extensions } from './extensions'; +import { Extension } from './extension'; +import { ExternalDocumentation } from './external-docs'; +import { Tags } from './tags'; +import { Tag } from './tag'; + +import { createModel } from '../utils'; +import { EXTENSION_REGEX } from '../../constants'; + +import type { BaseModel } from '../base'; +import type { BindingsInterface } from '../bindings'; +import type { ExtensionsInterface } from '../extensions'; +import type { ExtensionInterface } from '../extension'; +import type { ExternalDocumentationInterface } from '../external-docs'; +import type { TagsInterface } from '../tags'; + +import type { v3 } from '../../spec-types'; + +export function bindings(model: BaseModel<{ bindings?: Record }>): BindingsInterface { + const bindings = model.json('bindings') || {}; + return new Bindings( + Object.entries(bindings || {}).map(([protocol, binding]) => + createModel(Binding, binding, { protocol, pointer: model.jsonPath(`bindings/${protocol}`) }, model) + ), + { originalData: bindings, asyncapi: model.meta('asyncapi'), pointer: model.jsonPath('bindings') } + ); +} + +export function hasDescription(model: BaseModel<{ description?: string }>) { + return Boolean(description(model)); +} + +export function description(model: BaseModel<{ description?: string }>): string | undefined { + return model.json('description'); +} + +export function extensions(model: BaseModel): ExtensionsInterface { + const extensions: ExtensionInterface[] = []; + Object.entries(model.json()).forEach(([id, value]: [string, any]) => { + if (EXTENSION_REGEX.test(id)) { + extensions.push( + createModel(Extension, value, { id, pointer: model.jsonPath(id) } as any, model) as Extension + ); + } + }); + return new Extensions(extensions); +} + +export function hasExternalDocs(model: BaseModel<{ externalDocs?: v3.ExternalDocumentationObject }>): boolean { + return Object.keys(model.json('externalDocs') || {}).length > 0; +} + +export function externalDocs(model: BaseModel<{ externalDocs?: v3.ExternalDocumentationObject }>): ExternalDocumentationInterface | undefined { + if (hasExternalDocs(model)) { + return new ExternalDocumentation(model.json('externalDocs') as v3.ExternalDocumentationObject); + } +} + +export function tags(model: BaseModel<{ tags?: v3.TagsObject }>): TagsInterface { + return new Tags( + (model.json('tags') || []).map((tag, idx) => + createModel(Tag, tag, { pointer: model.jsonPath(`tags/${idx}`) }, model) + ) + ); +} diff --git a/src/models/v3/tag.ts b/src/models/v3/tag.ts new file mode 100644 index 000000000..84ea47133 --- /dev/null +++ b/src/models/v3/tag.ts @@ -0,0 +1,35 @@ +import { BaseModel } from '../base'; + +import { hasDescription, description, extensions, hasExternalDocs, externalDocs } from './mixins'; + +import type { ExtensionsInterface } from '../extensions'; +import type{ ExternalDocumentationInterface } from '../external-docs'; +import type { TagInterface } from '../tag'; + +import type { v3 } from '../../spec-types'; + +export class Tag extends BaseModel implements TagInterface { + name(): string { + return this._json.name; + } + + hasDescription(): boolean { + return hasDescription(this); + } + + description(): string | undefined { + return description(this); + } + + extensions(): ExtensionsInterface { + return extensions(this); + } + + hasExternalDocs(): boolean { + return hasExternalDocs(this); + } + + externalDocs(): ExternalDocumentationInterface | undefined { + return externalDocs(this); + } +} diff --git a/src/models/v3/tags.ts b/src/models/v3/tags.ts new file mode 100644 index 000000000..6048620ff --- /dev/null +++ b/src/models/v3/tags.ts @@ -0,0 +1,10 @@ +import { Collection } from '../collection'; + +import type { TagsInterface } from '../tags'; +import type { TagInterface } from '../tag'; + +export class Tags extends Collection implements TagsInterface { + override get(name: string): TagInterface | undefined { + return this.collections.find(tag => tag.name() === name); + } +} diff --git a/src/spec-types/index.ts b/src/spec-types/index.ts index 1c818d3fa..5ab639dfa 100644 --- a/src/spec-types/index.ts +++ b/src/spec-types/index.ts @@ -1 +1,2 @@ export * as v2 from './v2'; +export * as v3 from './v3'; diff --git a/src/spec-types/v3.ts b/src/spec-types/v3.ts new file mode 100644 index 000000000..d9c114224 --- /dev/null +++ b/src/spec-types/v3.ts @@ -0,0 +1,113 @@ +import type { JSONSchema7Version, JSONSchema7TypeName, JSONSchema7Type } from 'json-schema'; + +export type AsyncAPIVersion = string; +export type Identifier = string; +export type DefaultContentType = string; + +export interface AsyncAPIObject extends SpecificationExtensions { + asyncapi: AsyncAPIVersion; + id?: Identifier; + defaultContentType?: DefaultContentType; +} + +export type TagsObject = Array; + +export interface TagObject extends SpecificationExtensions { + name: string; + description?: string; + externalDocs?: ExternalDocumentationObject; +} + +export interface ExternalDocumentationObject extends SpecificationExtensions { + url: string; + description?: string; +} + +export type SchemaObject = AsyncAPISchemaObject | ReferenceObject; + +export type AsyncAPISchemaObject = AsyncAPISchemaDefinition | boolean; +export interface AsyncAPISchemaDefinition extends SpecificationExtensions { + $id?: string; + $schema?: JSONSchema7Version; + $comment?: string; + + type?: JSONSchema7TypeName | JSONSchema7TypeName[]; + enum?: JSONSchema7Type[]; + const?: JSONSchema7Type; + + multipleOf?: number; + maximum?: number; + exclusiveMaximum?: number; + minimum?: number; + exclusiveMinimum?: number; + + maxLength?: number; + minLength?: number; + pattern?: string; + + items?: AsyncAPISchemaObject | AsyncAPISchemaObject[]; + additionalItems?: AsyncAPISchemaObject; + maxItems?: number; + minItems?: number; + uniqueItems?: boolean; + contains?: AsyncAPISchemaObject; + + maxProperties?: number; + minProperties?: number; + required?: string[]; + properties?: { + [key: string]: AsyncAPISchemaObject; + }; + patternProperties?: { + [key: string]: AsyncAPISchemaObject; + }; + additionalProperties?: AsyncAPISchemaObject; + dependencies?: { + [key: string]: AsyncAPISchemaObject | string[]; + }; + propertyNames?: AsyncAPISchemaObject; + + if?: AsyncAPISchemaObject; + then?: AsyncAPISchemaObject; + else?: AsyncAPISchemaObject; + + allOf?: AsyncAPISchemaObject[]; + anyOf?: AsyncAPISchemaObject[]; + oneOf?: AsyncAPISchemaObject[]; + not?: AsyncAPISchemaObject; + + format?: string; + + contentMediaType?: string; + contentEncoding?: string; + + definitions?: { + [key: string]: AsyncAPISchemaObject; + }; + + title?: string; + description?: string; + default?: JSONSchema7Type; + readOnly?: boolean; + writeOnly?: boolean; + examples?: Array; + + discriminator?: string; + externalDocs?: ExternalDocumentationObject; + deprecated?: boolean; + [keyword: string]: any; +} + +export interface Binding { + bindingVersion?: string; +} + +export interface SpecificationExtensions { + [extension: `x-${string}`]: SpecificationExtension; +} + +export type SpecificationExtension = T; + +export interface ReferenceObject { + $ref: string; +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index 1e2cdacfe..4db6c21d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import type { ISpectralDiagnostic, IFunctionResult } from '@stoplight/spectral-core'; import type { AsyncAPIDocumentInterface } from './models'; -import type { v2 } from './spec-types'; +import type { v2, v3 } from './spec-types'; export type MaybeAsyncAPI = { asyncapi: string } & Record; export interface AsyncAPISemver { @@ -20,5 +20,5 @@ export interface DetailedAsyncAPI { export type Input = string | MaybeAsyncAPI | AsyncAPIDocumentInterface; export type Diagnostic = ISpectralDiagnostic; export type SchemaValidateResult = IFunctionResult; -export type AsyncAPIObject = v2.AsyncAPIObject; -export type AsyncAPISchema = v2.AsyncAPISchemaObject; +export type AsyncAPIObject = v2.AsyncAPIObject | v3.AsyncAPIObject; +export type AsyncAPISchema = v2.AsyncAPISchemaObject | v3.AsyncAPISchemaObject;