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: init parse, lint and validate functions #487

Merged
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
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