Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add Parser class #511

Merged
merged 1 commit into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/custom-operations/index.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
export async function customOperations(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
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<void> {
async function operationsV2(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
if (options.applyTraits) {
applyTraitsV2(detailed.parsed);
}
if (options.parseSchemas) {
await parseSchemasV2(detailed);
await parseSchemasV2(parser, detailed);
}
}

async function operationsV3(detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
async function operationsV3(parser: Parser, detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
if (options.applyTraits) {
applyTraitsV3(detailed.parsed);
}
}
}
9 changes: 5 additions & 4 deletions src/custom-operations/parse-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<ToParseItem> = [];

Expand Down Expand Up @@ -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);
}
10 changes: 3 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
50 changes: 24 additions & 26 deletions src/lint.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Expand All @@ -27,20 +24,24 @@ export interface ValidateOutput {
diagnostics: Diagnostic[];
}

export async function lint(asyncapi: ParserInput, options?: LintOptions): Promise<Diagnostic[] | undefined> {
export async function lint(parser: Parser, asyncapi: ParseInput, options?: LintOptions): Promise<Diagnostic[]> {
const result = await validate(parser, asyncapi, options);
return result.diagnostics;
}

export async function validate(parser: Parser, asyncapi: ParseInput, options: ValidateOptions = {}): Promise<ValidateOutput> {
if (toAsyncAPIDocument(asyncapi)) {
return;
return {
validated: asyncapi,
diagnostics: [],
}
}
const document = normalizeInput(asyncapi as Exclude<ParserInput, AsyncAPIDocumentInterface>);
return (await validate(document, options)).diagnostics;
}

export async function validate(asyncapi: string, options?: ValidateOptions): Promise<ValidateOutput> {
const { ruleset, allowedSeverity, ...restOptions } = normalizeOptions(options);
const spectral = new Spectral(restOptions);
const stringifiedDocument = normalizeInput(asyncapi as Exclude<ParseInput, AsyncAPIDocumentInterface>);
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) ||
Expand All @@ -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,
}
Expand All @@ -65,7 +64,6 @@ function normalizeOptions(options?: ValidateOptions): ValidateOptions {
}
// shall copy
options = { ...defaultOptions, ...options };

// severity
options.allowedSeverity = { ...defaultOptions.allowedSeverity, ...(options.allowedSeverity || {}) };

Expand Down
29 changes: 17 additions & 12 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,28 @@ 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;
parseSchemas?: boolean;
validateOptions?: ValidateOptions;
}

export async function parse(asyncapi: ParserInput, options?: ParseOptions): Promise<ParserOutput> {
export async function parse(parser: Parser, asyncapi: ParseInput, options?: ParseOptions): Promise<ParseOutput> {
let maybeDocument = toAsyncAPIDocument(asyncapi);
if (maybeDocument) {
return {
Expand All @@ -27,10 +34,10 @@ export async function parse(asyncapi: ParserInput, options?: ParseOptions): Prom
}

try {
const document = normalizeInput(asyncapi as Exclude<ParserInput, AsyncAPIDocumentInterface>);
const document = normalizeInput(asyncapi as Exclude<ParseInput, AsyncAPIDocumentInterface>);
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,
Expand All @@ -39,14 +46,12 @@ export async function parse(asyncapi: ParserInput, options?: ParseOptions): Prom
};
}

const doc = {
...(validated as Record<string, any>),
[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<string, unknown>, parsed);
await customOperations(detailed, options);
const detailed = createDetailedAsyncAPI(asyncapi as string | Record<string, unknown>, validatedDoc);
await customOperations(parser, detailed, options);
const parsedDoc = newAsyncAPIDocument(detailed);

return {
Expand Down
48 changes: 48 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -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<string, SchemaParser>();
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);
}
}
37 changes: 16 additions & 21 deletions src/schema-parser/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Parser } from '../parser';
import type { DetailedAsyncAPI, SchemaValidateResult } from '../types';

export interface ValidateSchemaInput<D = unknown, M = unknown> {
Expand All @@ -24,44 +25,38 @@ export interface SchemaParser<D = unknown, M = unknown> {
getMimeTypes: () => Array<string>;
}

const PARSERS = new Map<string, SchemaParser>();

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}`;
}
13 changes: 0 additions & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ISpectralDiagnostic, IFunctionResult } from '@stoplight/spectral-core';
import type { AsyncAPIDocumentInterface } from './models/asyncapi';

export type MaybeAsyncAPI = { asyncapi: string } & Record<string, unknown>;
export interface AsyncAPISemver {
Expand All @@ -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<T> {
new (...args: any[]): T
}
3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};

Expand Down
Loading