Skip to content

Commit

Permalink
refactor: init parse, lint and validate functions (#487)
Browse files Browse the repository at this point in the history
  • Loading branch information
magicmatatjahu authored and derberg committed Oct 4, 2022
1 parent aaef7e6 commit 24a6eea
Show file tree
Hide file tree
Showing 11 changed files with 401 additions and 5 deletions.
5 changes: 5 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const config: Config.InitialOptions = {
// The root of your source code, typically /src
// `<rootDir>` is a token Jest substitutes
roots: ['<rootDir>'],
moduleNameMapper: {
'^nimma/legacy$': '<rootDir>/node_modules/nimma/dist/legacy/cjs/index.js',
'^nimma/(.*)': '<rootDir>/node_modules/nimma/dist/cjs/$1',
'^@stoplight/spectral-ruleset-bundler/(.*)$': '<rootDir>/node_modules/@stoplight/spectral-ruleset-bundler/dist/$1'
},

// Test spec file resolution pattern
// Matches parent folder `__tests__` and filename
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export * from './models';

export { lint, validate } from './lint';
export { parse } from './parse';
export { stringify, unstringify } from './stringify';

export type { LintOptions, ValidateOptions, ValidateOutput } from './lint';
export type { StringifyOptions } from './stringify';
export type { ParseOptions } from './parse';
export type { ParserInput, ParserOutput, Diagnostic } from './types';
73 changes: 73 additions & 0 deletions src/lint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
IConstructorOpts,
IRunOpts,
Spectral,
Ruleset,
RulesetDefinition,
} from "@stoplight/spectral-core";
import { asyncapi as aasRuleset } from "@stoplight/spectral-rulesets";

import { toAsyncAPIDocument, normalizeInput, hasWarningDiagnostic, hasErrorDiagnostic } from "./utils";

import type { AsyncAPIDocument } from "./models/asyncapi";
import type { ParserInput, Diagnostic } from "./types";

export interface LintOptions extends IConstructorOpts, IRunOpts {
ruleset?: RulesetDefinition | Ruleset;
}

export interface ValidateOptions extends LintOptions {
allowedSeverity?: {
warning?: boolean;
};
}

export interface ValidateOutput {
validated: unknown;
diagnostics: Diagnostic[];
}

export async function lint(asyncapi: ParserInput, options?: LintOptions): Promise<Diagnostic[] | undefined> {
if (toAsyncAPIDocument(asyncapi)) {
return;
}
const document = normalizeInput(asyncapi as Exclude<ParserInput, AsyncAPIDocument>);
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);

spectral.setRuleset(ruleset!);
let { resolved, results } = await spectral.runWithResolved(asyncapi);

if (
hasErrorDiagnostic(results) ||
(!allowedSeverity?.warning && hasWarningDiagnostic(results))
) {
resolved = undefined;
}

return { validated: resolved, diagnostics: results };
}

const defaultOptions: ValidateOptions = {
// TODO: fix that type
ruleset: aasRuleset as any,
allowedSeverity: {
warning: true,
}
};
function normalizeOptions(options?: ValidateOptions): ValidateOptions {
if (!options || typeof options !== 'object') {
return defaultOptions;
}
// shall copy
options = { ...defaultOptions, ...options };

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

return options;
}
4 changes: 2 additions & 2 deletions src/models/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ export class BaseModel {
private readonly _json: Record<string, any>,
) {}

json<T = Record<string, unknown>>(): T;
json<T = unknown>(key: string | number): T;
json<T = Record<string, any>>(): T;
json<T = any>(key: string | number): T;
json(key?: string | number) {
if (key === undefined) return this._json;
if (!this._json) return;
Expand Down
64 changes: 64 additions & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { AsyncAPIDocument } from "./models";
import { normalizeInput, toAsyncAPIDocument } from "./utils";
import { validate } from "./lint";

import type { ParserInput, ParserOutput } from './types';
import type { ValidateOptions } from './lint';

export interface ParseOptions {
applyTraits?: boolean;
validateOptions?: ValidateOptions;
}

export async function parse(asyncapi: ParserInput, options?: ParseOptions): Promise<ParserOutput> {
let maybeDocument = toAsyncAPIDocument(asyncapi);
if (maybeDocument) {
return {
source: asyncapi,
parsed: maybeDocument,
diagnostics: [],
};
}

try {
const document = normalizeInput(asyncapi as Exclude<ParserInput, AsyncAPIDocument>);
options = normalizeOptions(options);

const { validated, diagnostics } = await validate(document, options.validateOptions);
if (validated === undefined) {
return {
source: asyncapi,
parsed: undefined,
diagnostics,
};
}

const parsed = new AsyncAPIDocument(validated as Record<string, unknown>);
return {
source: asyncapi,
parsed,
diagnostics,
};
} catch(err) {
// TODO: throw proper error
throw Error();
}
}

const defaultOptions: ParseOptions = {
applyTraits: true,
};
function normalizeOptions(options?: ParseOptions): ParseOptions {
if (!options || typeof options !== 'object') {
return defaultOptions;
}
// shall copy
options = { ...defaultOptions, ...options };

// traits
if (options.applyTraits === undefined) {
options.applyTraits = true;
}

return options;
}
8 changes: 6 additions & 2 deletions src/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { AsyncAPIDocument } from './models';
import { isAsyncAPIDocument, isParsedDocument, isStringifiedDocument } from './utils';
import { xParserSpecStringified } from './constants';

export function stringify(document: unknown, space?: string | number): string | undefined {
export interface StringifyOptions {
space?: string | number;
}

export function stringify(document: unknown, options: StringifyOptions = {}): string | undefined {
if (isAsyncAPIDocument(document)) {
document = document.json();
} else if (isParsedDocument(document)) {
Expand All @@ -18,7 +22,7 @@ export function stringify(document: unknown, space?: string | number): string |
return JSON.stringify({
...document as Record<string, unknown>,
[String(xParserSpecStringified)]: true,
}, refReplacer(), space);
}, refReplacer(), options.space || 2);
}

export function unstringify(document: unknown): AsyncAPIDocument | undefined {
Expand Down
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import type { AsyncAPIDocument } from './models/asyncapi';

export type MaybeAsyncAPI = { asyncapi: unknown } & Record<string, unknown>;
export type ParserInput = string | MaybeAsyncAPI | AsyncAPIDocument;

export type Diagnostic = ISpectralDiagnostic;

export interface ParserOutput {
source: ParserInput;
parsed: AsyncAPIDocument | undefined;
diagnostics: Diagnostic[];
}
16 changes: 16 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DiagnosticSeverity } from '@stoplight/types';
import { AsyncAPIDocument } from './models';
import { unstringify } from './stringify';

Expand All @@ -6,6 +7,9 @@ import {
xParserSpecStringified,
} from './constants';

import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
import type { MaybeAsyncAPI } from 'types';

export function toAsyncAPIDocument(maybeDoc: unknown): AsyncAPIDocument | undefined {
if (isAsyncAPIDocument(maybeDoc)) {
return maybeDoc;
Expand Down Expand Up @@ -36,3 +40,15 @@ export function isStringifiedDocument(maybeDoc: unknown): maybeDoc is Record<str
Boolean((maybeDoc as Record<string, unknown>)[xParserSpecStringified])
);
}

export function normalizeInput(asyncapi: string | MaybeAsyncAPI): string {
return JSON.stringify(asyncapi, undefined, 2);
};

export function hasErrorDiagnostic(diagnostics: ISpectralDiagnostic[]): boolean {
return diagnostics.some(diagnostic => diagnostic.severity === DiagnosticSeverity.Error);
}

export function hasWarningDiagnostic(diagnostics: ISpectralDiagnostic[]): boolean {
return diagnostics.some(diagnostic => diagnostic.severity === DiagnosticSeverity.Warning);
}
91 changes: 91 additions & 0 deletions test/lint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { lint, validate } from '../src/lint';
import { hasErrorDiagnostic, hasWarningDiagnostic } from '../src/utils';

describe('lint() & validate()', function() {
describe('lint()', function() {
it('should lint invalid document', async function() {
const document = {
asyncapi: '2.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0',
},
}

const diagnostics = await lint(document);
if (!diagnostics) {
return;
}

expect(diagnostics.length > 0).toEqual(true);
expect(hasErrorDiagnostic(diagnostics)).toEqual(true);
expect(hasWarningDiagnostic(diagnostics)).toEqual(true);
});

it('should lint valid document', async function() {
const document = {
asyncapi: '2.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0',
},
channels: {}
}

const diagnostics = await lint(document);
if (!diagnostics) {
return;
}

expect(diagnostics.length > 0).toEqual(true);
expect(hasErrorDiagnostic(diagnostics)).toEqual(false);
expect(hasWarningDiagnostic(diagnostics)).toEqual(true);
});
});

describe('validate()', function() {
it('should validate invalid document', async function() {
const document = JSON.stringify({
asyncapi: '2.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0',
},
}, undefined, 2);
const { validated, diagnostics } = await validate(document);

expect(validated).toBeUndefined();
expect(diagnostics.length > 0).toEqual(true);
});

it('should validate valid document', async function() {
const document = JSON.stringify({
asyncapi: '2.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0',
},
channels: {}
}, undefined, 2);
const { validated, diagnostics } = await validate(document);

expect(validated).not.toBeUndefined();
expect(diagnostics.length > 0).toEqual(true);
});

it('should validate valid document - do not allow warning severity', async function() {
const document = JSON.stringify({
asyncapi: '2.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0',
},
channels: {}
}, undefined, 2);
const { validated, diagnostics } = await validate(document, { allowedSeverity: { warning: false } });

expect(validated).toBeUndefined();
expect(diagnostics.length > 0).toEqual(true);
});
});
});
33 changes: 33 additions & 0 deletions test/parse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AsyncAPIDocument } from '../src/models/asyncapi';
import { parse } from '../src/parse';

describe('parse()', function() {
it('should parse valid document', async function() {
const document = {
asyncapi: '2.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0',
},
channels: {}
}
const { parsed, diagnostics } = await parse(document);

expect(parsed).toBeInstanceOf(AsyncAPIDocument);
expect(diagnostics.length > 0).toEqual(true);
});

it('should parse invalid document', async function() {
const document = {
asyncapi: '2.0.0',
info: {
title: 'Valid AsyncApi document',
version: '1.0',
},
}
const { parsed, diagnostics } = await parse(document);

expect(parsed).toEqual(undefined);
expect(diagnostics.length > 0).toEqual(true);
});
});
Loading

0 comments on commit 24a6eea

Please sign in to comment.