diff --git a/src/custom-operations/index.ts b/src/custom-operations/index.ts index c900d3a02..377dcacc3 100644 --- a/src/custom-operations/index.ts +++ b/src/custom-operations/index.ts @@ -1,27 +1,28 @@ import { applyTraitsV2, applyTraitsV3 } from './apply-traits'; import { parseSchemasV2 } from './parse-schema'; +import type { Parser } from '../parser'; import type { ParseOptions } from "../parse"; import type { DetailedAsyncAPI } from "../types"; -export async function customOperations(detailed: DetailedAsyncAPI, options: ParseOptions): Promise { +export async function customOperations(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise { switch (detailed.semver.major) { - case 2: return operationsV2(detailed, options); - case 3: return operationsV3(detailed, options); + case 2: return operationsV2(parser, detailed, options); + case 3: return operationsV3(parser, detailed, options); } } -async function operationsV2(detailed: DetailedAsyncAPI, options: ParseOptions): Promise { +async function operationsV2(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise { if (options.applyTraits) { applyTraitsV2(detailed.parsed); } if (options.parseSchemas) { - await parseSchemasV2(detailed); + await parseSchemasV2(parser, detailed); } } -async function operationsV3(detailed: DetailedAsyncAPI, options: ParseOptions): Promise { +async function operationsV3(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise { if (options.applyTraits) { applyTraitsV3(detailed.parsed); } -} \ No newline at end of file +} diff --git a/src/custom-operations/parse-schema.ts b/src/custom-operations/parse-schema.ts index 2196368ac..0884d5f41 100644 --- a/src/custom-operations/parse-schema.ts +++ b/src/custom-operations/parse-schema.ts @@ -4,6 +4,7 @@ import { toPath } from 'lodash'; import { parseSchema, getDefaultSchemaFormat } from '../schema-parser'; import { xParserOriginalSchemaFormat } from '../constants'; +import type { Parser } from '../parser'; import type { ParseSchemaInput } from "../schema-parser"; import type { DetailedAsyncAPI } from "../types"; @@ -20,7 +21,7 @@ const customSchemasPathsV2 = [ '$.components.messages.*', ]; -export async function parseSchemasV2(detailed: DetailedAsyncAPI) { +export async function parseSchemasV2(parser: Parser, detailed: DetailedAsyncAPI) { const defaultSchemaFormat = getDefaultSchemaFormat(detailed.parsed.asyncapi as string); const parseItems: Array = []; @@ -57,10 +58,10 @@ export async function parseSchemasV2(detailed: DetailedAsyncAPI) { }); }); - return Promise.all(parseItems.map(parseSchemaV2)); + return Promise.all(parseItems.map(item => parseSchemaV2(parser, item))); } -async function parseSchemaV2(item: ToParseItem) { +async function parseSchemaV2(parser: Parser, item: ToParseItem) { item.value[xParserOriginalSchemaFormat] = item.input.schemaFormat; - item.value.payload = await parseSchema(item.input); + item.value.payload = await parseSchema(parser, item.input); } diff --git a/src/index.ts b/src/index.ts index 6f8386342..178636e8a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,11 @@ export * from './models'; -export { lint, validate } from './lint'; -export { parse } from './parse'; +export { Parser } from './parser'; export { stringify, unstringify } from './stringify'; - -export { registerSchemaParser } from './schema-parser'; export { AsyncAPISchemaParser } from './schema-parser/asyncapi-schema-parser'; +export type { AsyncAPISemver, Diagnostic, SchemaValidateResult } from './types'; export type { LintOptions, ValidateOptions, ValidateOutput } from './lint'; +export type { ParseInput, ParseOptions, ParseOutput } from './parse'; export type { StringifyOptions } from './stringify'; -export type { ParseOptions } from './parse'; -export type { AsyncAPISemver, ParserInput, ParserOutput, Diagnostic, SchemaValidateResult } from './types'; - export type { ValidateSchemaInput, ParseSchemaInput, SchemaParser } from './schema-parser' diff --git a/src/lint.ts b/src/lint.ts index 33d2559de..8035c5ead 100644 --- a/src/lint.ts +++ b/src/lint.ts @@ -1,22 +1,19 @@ -import { - IConstructorOpts, - IRunOpts, - Spectral, - Ruleset, - RulesetDefinition, -} from "@stoplight/spectral-core"; -import { asyncapi as aasRuleset } from "@stoplight/spectral-rulesets"; - +import { Document } from "@stoplight/spectral-core"; +import { Yaml } from "@stoplight/spectral-parsers"; import { toAsyncAPIDocument, normalizeInput, hasWarningDiagnostic, hasErrorDiagnostic } from "./utils"; +import type { IRunOpts } from "@stoplight/spectral-core"; +import type { Parser } from './parser'; import type { AsyncAPIDocumentInterface } from "./models/asyncapi"; -import type { ParserInput, Diagnostic } from "./types"; +import type { ParseInput } from "./parse"; +import type { Diagnostic } from "./types"; -export interface LintOptions extends IConstructorOpts, IRunOpts { - ruleset?: RulesetDefinition | Ruleset; +export interface LintOptions extends IRunOpts { + path?: string; } -export interface ValidateOptions extends LintOptions { +export interface ValidateOptions extends IRunOpts { + path?: string; allowedSeverity?: { warning?: boolean; }; @@ -27,20 +24,24 @@ export interface ValidateOutput { diagnostics: Diagnostic[]; } -export async function lint(asyncapi: ParserInput, options?: LintOptions): Promise { +export async function lint(parser: Parser, asyncapi: ParseInput, options?: LintOptions): Promise { + const result = await validate(parser, asyncapi, options); + return result.diagnostics; +} + +export async function validate(parser: Parser, asyncapi: ParseInput, options: ValidateOptions = {}): Promise { if (toAsyncAPIDocument(asyncapi)) { - return; + return { + validated: asyncapi, + diagnostics: [], + } } - const document = normalizeInput(asyncapi as Exclude); - return (await validate(document, options)).diagnostics; -} -export async function validate(asyncapi: string, options?: ValidateOptions): Promise { - const { ruleset, allowedSeverity, ...restOptions } = normalizeOptions(options); - const spectral = new Spectral(restOptions); + const stringifiedDocument = normalizeInput(asyncapi as Exclude); + const document = new Document(stringifiedDocument, Yaml, options.path); - spectral.setRuleset(ruleset!); - let { resolved, results } = await spectral.runWithResolved(asyncapi); + const { allowedSeverity } = normalizeOptions(options); + let { resolved, results } = await parser.spectral.runWithResolved(document); if ( hasErrorDiagnostic(results) || @@ -53,8 +54,6 @@ export async function validate(asyncapi: string, options?: ValidateOptions): Pro } const defaultOptions: ValidateOptions = { - // TODO: fix that type - ruleset: aasRuleset as any, allowedSeverity: { warning: true, } @@ -65,7 +64,6 @@ function normalizeOptions(options?: ValidateOptions): ValidateOptions { } // shall copy options = { ...defaultOptions, ...options }; - // severity options.allowedSeverity = { ...defaultOptions.allowedSeverity, ...(options.allowedSeverity || {}) }; diff --git a/src/parse.ts b/src/parse.ts index 2564f64af..80c952e84 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -2,13 +2,20 @@ import { AsyncAPIDocumentInterface, newAsyncAPIDocument } from "./models"; import { customOperations } from './custom-operations'; import { validate } from "./lint"; -import { stringify, unstringify } from './stringify'; import { createDetailedAsyncAPI, normalizeInput, toAsyncAPIDocument } from "./utils"; import { xParserSpecParsed } from './constants'; -import type { ParserInput, ParserOutput } from './types'; +import type { Parser } from './parser'; import type { ValidateOptions } from './lint'; +import type { MaybeAsyncAPI, Diagnostic } from './types'; + +export type ParseInput = string | MaybeAsyncAPI | AsyncAPIDocumentInterface; +export interface ParseOutput { + source: ParseInput; + parsed: AsyncAPIDocumentInterface | undefined; + diagnostics: Diagnostic[]; +} export interface ParseOptions { applyTraits?: boolean; @@ -16,7 +23,7 @@ export interface ParseOptions { validateOptions?: ValidateOptions; } -export async function parse(asyncapi: ParserInput, options?: ParseOptions): Promise { +export async function parse(parser: Parser, asyncapi: ParseInput, options?: ParseOptions): Promise { let maybeDocument = toAsyncAPIDocument(asyncapi); if (maybeDocument) { return { @@ -27,10 +34,10 @@ export async function parse(asyncapi: ParserInput, options?: ParseOptions): Prom } try { - const document = normalizeInput(asyncapi as Exclude); + const document = normalizeInput(asyncapi as Exclude); options = normalizeOptions(options); - const { validated, diagnostics } = await validate(document, options.validateOptions); + const { validated, diagnostics } = await validate(parser, document, options.validateOptions); if (validated === undefined) { return { source: asyncapi, @@ -39,14 +46,12 @@ export async function parse(asyncapi: ParserInput, options?: ParseOptions): Prom }; } - const doc = { - ...(validated as Record), - [xParserSpecParsed]: true, - } - const parsed = unstringify(stringify(doc))?.json()!; + // unfreeze the object - Spectral makes resolved document "freezed" + const validatedDoc = JSON.parse(JSON.stringify(validated)); + validatedDoc[String(xParserSpecParsed)] = true; - const detailed = createDetailedAsyncAPI(asyncapi as string | Record, parsed); - await customOperations(detailed, options); + const detailed = createDetailedAsyncAPI(asyncapi as string | Record, validatedDoc); + await customOperations(parser, detailed, options); const parsedDoc = newAsyncAPIDocument(detailed); return { diff --git a/src/parser.ts b/src/parser.ts new file mode 100644 index 000000000..3d106fe50 --- /dev/null +++ b/src/parser.ts @@ -0,0 +1,48 @@ +import { Spectral } from "@stoplight/spectral-core"; +import { asyncapi as aasRuleset } from "@stoplight/spectral-rulesets"; + +import { parse } from "./parse"; +import { lint, validate } from "./lint"; +import { registerSchemaParser } from './schema-parser'; + +import type { IConstructorOpts } from "@stoplight/spectral-core"; +import type { ParseInput, ParseOptions } from "./parse"; +import type { LintOptions, ValidateOptions } from "./lint"; +import type { SchemaParser } from './schema-parser'; + +export interface ParserOptions { + spectral?: Spectral | IConstructorOpts; +} + +export class Parser { + public readonly parserRegistry = new Map(); + public readonly spectral: Spectral; + + constructor(options?: ParserOptions) { + const { spectral } = options || {}; + if (spectral instanceof Spectral) { + this.spectral = spectral; + } else { + this.spectral = new Spectral(spectral); + } + + // TODO: fix type + this.spectral.setRuleset(aasRuleset as any); + } + + parse(asyncapi: ParseInput, options?: ParseOptions) { + return parse(this, asyncapi, options); + } + + lint(asyncapi: ParseInput, options?: LintOptions) { + return lint(this, asyncapi, options); + } + + validate(asyncapi: ParseInput, options?: ValidateOptions) { + return validate(this, asyncapi, options); + } + + registerSchemaParser(parser: SchemaParser) { + return registerSchemaParser(this, parser); + } +} diff --git a/src/schema-parser/index.ts b/src/schema-parser/index.ts index e2be6368f..3966782fe 100644 --- a/src/schema-parser/index.ts +++ b/src/schema-parser/index.ts @@ -1,3 +1,4 @@ +import type { Parser } from '../parser'; import type { DetailedAsyncAPI, SchemaValidateResult } from '../types'; export interface ValidateSchemaInput { @@ -24,44 +25,38 @@ export interface SchemaParser { getMimeTypes: () => Array; } -const PARSERS = new Map(); - -export async function validateSchema(input: ParseSchemaInput) { - const parser = getSchemaParser(input.schemaFormat); - if (parser === undefined) { +export async function validateSchema(parser: Parser, input: ParseSchemaInput) { + const schemaParser = parser.parserRegistry.get(input.schemaFormat); + if (schemaParser === undefined) { // throw appropriate error throw new Error(); } - return parser.validate(input); + return schemaParser.validate(input); } -export async function parseSchema(input: ParseSchemaInput) { - const parser = getSchemaParser(input.schemaFormat); - if (parser === undefined) { +export async function parseSchema(parser: Parser, input: ParseSchemaInput) { + const schemaParser = parser.parserRegistry.get(input.schemaFormat); + if (schemaParser === undefined) { return; } - return parser.parse(input); + return schemaParser.parse(input); } -export function registerSchemaParser(parser: SchemaParser) { +export function registerSchemaParser(parser: Parser, schemaParser: SchemaParser) { if ( - typeof parser !== 'object' - || typeof parser.validate !== 'function' - || typeof parser.parse !== 'function' - || typeof parser.getMimeTypes !== 'function' + typeof schemaParser !== 'object' + || typeof schemaParser.validate !== 'function' + || typeof schemaParser.parse !== 'function' + || typeof schemaParser.getMimeTypes !== 'function' ) { throw new Error('custom parser must have "parse()", "validate()" and "getMimeTypes()" functions.'); } - parser.getMimeTypes().forEach(schemaFormat => { - PARSERS.set(schemaFormat, parser); + schemaParser.getMimeTypes().forEach(schemaFormat => { + parser.parserRegistry.set(schemaFormat, schemaParser); }); } -export function getSchemaParser(mimeType: string) { - return PARSERS.get(mimeType); -} - export function getDefaultSchemaFormat(asyncapiVersion: string) { return `application/vnd.aai.asyncapi;version=${asyncapiVersion}`; } diff --git a/src/types.ts b/src/types.ts index 13e882b8a..94f03e353 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,4 @@ import type { ISpectralDiagnostic, IFunctionResult } from '@stoplight/spectral-core'; -import type { AsyncAPIDocumentInterface } from './models/asyncapi'; export type MaybeAsyncAPI = { asyncapi: string } & Record; export interface AsyncAPISemver { @@ -16,17 +15,5 @@ export interface DetailedAsyncAPI { semver: AsyncAPISemver; } -export type ParserInput = string | MaybeAsyncAPI | AsyncAPIDocumentInterface; - export type Diagnostic = ISpectralDiagnostic; export type SchemaValidateResult = IFunctionResult; - -export interface ParserOutput { - source: ParserInput; - parsed: AsyncAPIDocumentInterface | undefined; - diagnostics: Diagnostic[]; -} - -export interface Constructor { - new (...args: any[]): T -} \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index da88db8ad..0a3db9522 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -62,6 +62,9 @@ export function getSemver(version: string): AsyncAPISemver { } export function normalizeInput(asyncapi: string | MaybeAsyncAPI): string { + if (typeof asyncapi === 'string') { + return asyncapi; + } return JSON.stringify(asyncapi, undefined, 2); }; diff --git a/test/lint.spec.ts b/test/lint.spec.ts index 8e4d657fc..d0ee912c7 100644 --- a/test/lint.spec.ts +++ b/test/lint.spec.ts @@ -1,7 +1,10 @@ import { lint, validate } from '../src/lint'; +import { Parser } from '../src/parser'; import { hasErrorDiagnostic, hasWarningDiagnostic } from '../src/utils'; describe('lint() & validate()', function() { + const parser = new Parser(); + describe('lint()', function() { it('should lint invalid document', async function() { const document = { @@ -12,7 +15,7 @@ describe('lint() & validate()', function() { }, } - const diagnostics = await lint(document); + const diagnostics = await lint(parser, document); if (!diagnostics) { return; } @@ -32,7 +35,7 @@ describe('lint() & validate()', function() { channels: {} } - const diagnostics = await lint(document); + const diagnostics = await lint(parser, document); if (!diagnostics) { return; } @@ -52,7 +55,7 @@ describe('lint() & validate()', function() { version: '1.0', }, }, undefined, 2); - const { validated, diagnostics } = await validate(document); + const { validated, diagnostics } = await validate(parser, document); expect(validated).toBeUndefined(); expect(diagnostics.length > 0).toEqual(true); @@ -67,7 +70,7 @@ describe('lint() & validate()', function() { }, channels: {} }, undefined, 2); - const { validated, diagnostics } = await validate(document); + const { validated, diagnostics } = await validate(parser, document); expect(validated).not.toBeUndefined(); expect(diagnostics.length > 0).toEqual(true); @@ -82,7 +85,7 @@ describe('lint() & validate()', function() { }, channels: {} }, undefined, 2); - const { validated, diagnostics } = await validate(document, { allowedSeverity: { warning: false } }); + const { validated, diagnostics } = await validate(parser, document, { allowedSeverity: { warning: false } }); expect(validated).toBeUndefined(); expect(diagnostics.length > 0).toEqual(true); diff --git a/test/parse.spec.ts b/test/parse.spec.ts index 964fcb4f6..4c4e23c3b 100644 --- a/test/parse.spec.ts +++ b/test/parse.spec.ts @@ -1,7 +1,10 @@ import { AsyncAPIDocumentV2 } from '../src/models'; +import { Parser } from '../src/parser'; import { parse } from '../src/parse'; describe('parse()', function() { + const parser = new Parser(); + it('should parse valid document', async function() { const document = { asyncapi: '2.0.0', @@ -11,7 +14,7 @@ describe('parse()', function() { }, channels: {} } - const { parsed, diagnostics } = await parse(document); + const { parsed, diagnostics } = await parse(parser, document); expect(parsed).toBeInstanceOf(AsyncAPIDocumentV2); expect(diagnostics.length > 0).toEqual(true); @@ -25,7 +28,7 @@ describe('parse()', function() { version: '1.0', }, } - const { parsed, diagnostics } = await parse(document); + const { parsed, diagnostics } = await parse(parser, document); expect(parsed).toEqual(undefined); expect(diagnostics.length > 0).toEqual(true); diff --git a/test/parser.spec.ts b/test/parser.spec.ts new file mode 100644 index 000000000..8f17a0554 --- /dev/null +++ b/test/parser.spec.ts @@ -0,0 +1,29 @@ +import { Spectral } from "@stoplight/spectral-core"; +import { Parser } from '../src/parser'; +import { AsyncAPISchemaParser } from '../src/schema-parser/asyncapi-schema-parser'; + +describe('Parser class', function() { + it('should create Parser instance', async function() { + const parser = new Parser(); + expect(parser).toBeInstanceOf(Parser); + }); + + it('should have default Spectral instance if no instance is specified in the constructor', async function() { + const parser = new Parser(); + expect(parser.spectral).toBeInstanceOf(Spectral); + }); + + it('should have Spectral instance given in constructor', async function() { + const spectral = new Spectral(); + const parser = new Parser({ spectral }); + expect(parser.spectral).toBeInstanceOf(Spectral); + expect(parser.spectral).toEqual(spectral); + }); + + it('should register schema parser', async function() { + const parser = new Parser(); + const schemaParser = AsyncAPISchemaParser(); + parser.registerSchemaParser(schemaParser); + expect(parser.parserRegistry.size).toBeGreaterThan(1); + }); +});