Skip to content

Commit

Permalink
refactor: add Parser class (#511)
Browse files Browse the repository at this point in the history
  • Loading branch information
magicmatatjahu authored Apr 5, 2022
1 parent 032735b commit 40c40ee
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 97 deletions.
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

0 comments on commit 40c40ee

Please sign in to comment.