Skip to content

Commit 24a6eea

Browse files
magicmatatjahuderberg
authored andcommitted
refactor: init parse, lint and validate functions (#487)
1 parent aaef7e6 commit 24a6eea

11 files changed

+401
-5
lines changed

jest.config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const config: Config.InitialOptions = {
1010
// The root of your source code, typically /src
1111
// `<rootDir>` is a token Jest substitutes
1212
roots: ['<rootDir>'],
13+
moduleNameMapper: {
14+
'^nimma/legacy$': '<rootDir>/node_modules/nimma/dist/legacy/cjs/index.js',
15+
'^nimma/(.*)': '<rootDir>/node_modules/nimma/dist/cjs/$1',
16+
'^@stoplight/spectral-ruleset-bundler/(.*)$': '<rootDir>/node_modules/@stoplight/spectral-ruleset-bundler/dist/$1'
17+
},
1318

1419
// Test spec file resolution pattern
1520
// Matches parent folder `__tests__` and filename

src/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
export * from './models';
22

3+
export { lint, validate } from './lint';
4+
export { parse } from './parse';
35
export { stringify, unstringify } from './stringify';
6+
7+
export type { LintOptions, ValidateOptions, ValidateOutput } from './lint';
8+
export type { StringifyOptions } from './stringify';
9+
export type { ParseOptions } from './parse';
10+
export type { ParserInput, ParserOutput, Diagnostic } from './types';

src/lint.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {
2+
IConstructorOpts,
3+
IRunOpts,
4+
Spectral,
5+
Ruleset,
6+
RulesetDefinition,
7+
} from "@stoplight/spectral-core";
8+
import { asyncapi as aasRuleset } from "@stoplight/spectral-rulesets";
9+
10+
import { toAsyncAPIDocument, normalizeInput, hasWarningDiagnostic, hasErrorDiagnostic } from "./utils";
11+
12+
import type { AsyncAPIDocument } from "./models/asyncapi";
13+
import type { ParserInput, Diagnostic } from "./types";
14+
15+
export interface LintOptions extends IConstructorOpts, IRunOpts {
16+
ruleset?: RulesetDefinition | Ruleset;
17+
}
18+
19+
export interface ValidateOptions extends LintOptions {
20+
allowedSeverity?: {
21+
warning?: boolean;
22+
};
23+
}
24+
25+
export interface ValidateOutput {
26+
validated: unknown;
27+
diagnostics: Diagnostic[];
28+
}
29+
30+
export async function lint(asyncapi: ParserInput, options?: LintOptions): Promise<Diagnostic[] | undefined> {
31+
if (toAsyncAPIDocument(asyncapi)) {
32+
return;
33+
}
34+
const document = normalizeInput(asyncapi as Exclude<ParserInput, AsyncAPIDocument>);
35+
return (await validate(document, options)).diagnostics;
36+
}
37+
38+
export async function validate(asyncapi: string, options?: ValidateOptions): Promise<ValidateOutput> {
39+
const { ruleset, allowedSeverity, ...restOptions } = normalizeOptions(options);
40+
const spectral = new Spectral(restOptions);
41+
42+
spectral.setRuleset(ruleset!);
43+
let { resolved, results } = await spectral.runWithResolved(asyncapi);
44+
45+
if (
46+
hasErrorDiagnostic(results) ||
47+
(!allowedSeverity?.warning && hasWarningDiagnostic(results))
48+
) {
49+
resolved = undefined;
50+
}
51+
52+
return { validated: resolved, diagnostics: results };
53+
}
54+
55+
const defaultOptions: ValidateOptions = {
56+
// TODO: fix that type
57+
ruleset: aasRuleset as any,
58+
allowedSeverity: {
59+
warning: true,
60+
}
61+
};
62+
function normalizeOptions(options?: ValidateOptions): ValidateOptions {
63+
if (!options || typeof options !== 'object') {
64+
return defaultOptions;
65+
}
66+
// shall copy
67+
options = { ...defaultOptions, ...options };
68+
69+
// severity
70+
options.allowedSeverity = { ...defaultOptions.allowedSeverity, ...(options.allowedSeverity || {}) };
71+
72+
return options;
73+
}

src/models/base.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ export class BaseModel {
33
private readonly _json: Record<string, any>,
44
) {}
55

6-
json<T = Record<string, unknown>>(): T;
7-
json<T = unknown>(key: string | number): T;
6+
json<T = Record<string, any>>(): T;
7+
json<T = any>(key: string | number): T;
88
json(key?: string | number) {
99
if (key === undefined) return this._json;
1010
if (!this._json) return;

src/parse.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { AsyncAPIDocument } from "./models";
2+
import { normalizeInput, toAsyncAPIDocument } from "./utils";
3+
import { validate } from "./lint";
4+
5+
import type { ParserInput, ParserOutput } from './types';
6+
import type { ValidateOptions } from './lint';
7+
8+
export interface ParseOptions {
9+
applyTraits?: boolean;
10+
validateOptions?: ValidateOptions;
11+
}
12+
13+
export async function parse(asyncapi: ParserInput, options?: ParseOptions): Promise<ParserOutput> {
14+
let maybeDocument = toAsyncAPIDocument(asyncapi);
15+
if (maybeDocument) {
16+
return {
17+
source: asyncapi,
18+
parsed: maybeDocument,
19+
diagnostics: [],
20+
};
21+
}
22+
23+
try {
24+
const document = normalizeInput(asyncapi as Exclude<ParserInput, AsyncAPIDocument>);
25+
options = normalizeOptions(options);
26+
27+
const { validated, diagnostics } = await validate(document, options.validateOptions);
28+
if (validated === undefined) {
29+
return {
30+
source: asyncapi,
31+
parsed: undefined,
32+
diagnostics,
33+
};
34+
}
35+
36+
const parsed = new AsyncAPIDocument(validated as Record<string, unknown>);
37+
return {
38+
source: asyncapi,
39+
parsed,
40+
diagnostics,
41+
};
42+
} catch(err) {
43+
// TODO: throw proper error
44+
throw Error();
45+
}
46+
}
47+
48+
const defaultOptions: ParseOptions = {
49+
applyTraits: true,
50+
};
51+
function normalizeOptions(options?: ParseOptions): ParseOptions {
52+
if (!options || typeof options !== 'object') {
53+
return defaultOptions;
54+
}
55+
// shall copy
56+
options = { ...defaultOptions, ...options };
57+
58+
// traits
59+
if (options.applyTraits === undefined) {
60+
options.applyTraits = true;
61+
}
62+
63+
return options;
64+
}

src/stringify.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { AsyncAPIDocument } from './models';
33
import { isAsyncAPIDocument, isParsedDocument, isStringifiedDocument } from './utils';
44
import { xParserSpecStringified } from './constants';
55

6-
export function stringify(document: unknown, space?: string | number): string | undefined {
6+
export interface StringifyOptions {
7+
space?: string | number;
8+
}
9+
10+
export function stringify(document: unknown, options: StringifyOptions = {}): string | undefined {
711
if (isAsyncAPIDocument(document)) {
812
document = document.json();
913
} else if (isParsedDocument(document)) {
@@ -18,7 +22,7 @@ export function stringify(document: unknown, space?: string | number): string |
1822
return JSON.stringify({
1923
...document as Record<string, unknown>,
2024
[String(xParserSpecStringified)]: true,
21-
}, refReplacer(), space);
25+
}, refReplacer(), options.space || 2);
2226
}
2327

2428
export function unstringify(document: unknown): AsyncAPIDocument | undefined {

src/types.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
2+
import type { AsyncAPIDocument } from './models/asyncapi';
3+
4+
export type MaybeAsyncAPI = { asyncapi: unknown } & Record<string, unknown>;
5+
export type ParserInput = string | MaybeAsyncAPI | AsyncAPIDocument;
6+
7+
export type Diagnostic = ISpectralDiagnostic;
8+
9+
export interface ParserOutput {
10+
source: ParserInput;
11+
parsed: AsyncAPIDocument | undefined;
12+
diagnostics: Diagnostic[];
13+
}

src/utils.ts

+16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DiagnosticSeverity } from '@stoplight/types';
12
import { AsyncAPIDocument } from './models';
23
import { unstringify } from './stringify';
34

@@ -6,6 +7,9 @@ import {
67
xParserSpecStringified,
78
} from './constants';
89

10+
import type { ISpectralDiagnostic } from '@stoplight/spectral-core';
11+
import type { MaybeAsyncAPI } from 'types';
12+
913
export function toAsyncAPIDocument(maybeDoc: unknown): AsyncAPIDocument | undefined {
1014
if (isAsyncAPIDocument(maybeDoc)) {
1115
return maybeDoc;
@@ -36,3 +40,15 @@ export function isStringifiedDocument(maybeDoc: unknown): maybeDoc is Record<str
3640
Boolean((maybeDoc as Record<string, unknown>)[xParserSpecStringified])
3741
);
3842
}
43+
44+
export function normalizeInput(asyncapi: string | MaybeAsyncAPI): string {
45+
return JSON.stringify(asyncapi, undefined, 2);
46+
};
47+
48+
export function hasErrorDiagnostic(diagnostics: ISpectralDiagnostic[]): boolean {
49+
return diagnostics.some(diagnostic => diagnostic.severity === DiagnosticSeverity.Error);
50+
}
51+
52+
export function hasWarningDiagnostic(diagnostics: ISpectralDiagnostic[]): boolean {
53+
return diagnostics.some(diagnostic => diagnostic.severity === DiagnosticSeverity.Warning);
54+
}

test/lint.spec.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { lint, validate } from '../src/lint';
2+
import { hasErrorDiagnostic, hasWarningDiagnostic } from '../src/utils';
3+
4+
describe('lint() & validate()', function() {
5+
describe('lint()', function() {
6+
it('should lint invalid document', async function() {
7+
const document = {
8+
asyncapi: '2.0.0',
9+
info: {
10+
title: 'Valid AsyncApi document',
11+
version: '1.0',
12+
},
13+
}
14+
15+
const diagnostics = await lint(document);
16+
if (!diagnostics) {
17+
return;
18+
}
19+
20+
expect(diagnostics.length > 0).toEqual(true);
21+
expect(hasErrorDiagnostic(diagnostics)).toEqual(true);
22+
expect(hasWarningDiagnostic(diagnostics)).toEqual(true);
23+
});
24+
25+
it('should lint valid document', async function() {
26+
const document = {
27+
asyncapi: '2.0.0',
28+
info: {
29+
title: 'Valid AsyncApi document',
30+
version: '1.0',
31+
},
32+
channels: {}
33+
}
34+
35+
const diagnostics = await lint(document);
36+
if (!diagnostics) {
37+
return;
38+
}
39+
40+
expect(diagnostics.length > 0).toEqual(true);
41+
expect(hasErrorDiagnostic(diagnostics)).toEqual(false);
42+
expect(hasWarningDiagnostic(diagnostics)).toEqual(true);
43+
});
44+
});
45+
46+
describe('validate()', function() {
47+
it('should validate invalid document', async function() {
48+
const document = JSON.stringify({
49+
asyncapi: '2.0.0',
50+
info: {
51+
title: 'Valid AsyncApi document',
52+
version: '1.0',
53+
},
54+
}, undefined, 2);
55+
const { validated, diagnostics } = await validate(document);
56+
57+
expect(validated).toBeUndefined();
58+
expect(diagnostics.length > 0).toEqual(true);
59+
});
60+
61+
it('should validate valid document', async function() {
62+
const document = JSON.stringify({
63+
asyncapi: '2.0.0',
64+
info: {
65+
title: 'Valid AsyncApi document',
66+
version: '1.0',
67+
},
68+
channels: {}
69+
}, undefined, 2);
70+
const { validated, diagnostics } = await validate(document);
71+
72+
expect(validated).not.toBeUndefined();
73+
expect(diagnostics.length > 0).toEqual(true);
74+
});
75+
76+
it('should validate valid document - do not allow warning severity', async function() {
77+
const document = JSON.stringify({
78+
asyncapi: '2.0.0',
79+
info: {
80+
title: 'Valid AsyncApi document',
81+
version: '1.0',
82+
},
83+
channels: {}
84+
}, undefined, 2);
85+
const { validated, diagnostics } = await validate(document, { allowedSeverity: { warning: false } });
86+
87+
expect(validated).toBeUndefined();
88+
expect(diagnostics.length > 0).toEqual(true);
89+
});
90+
});
91+
});

test/parse.spec.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { AsyncAPIDocument } from '../src/models/asyncapi';
2+
import { parse } from '../src/parse';
3+
4+
describe('parse()', function() {
5+
it('should parse valid document', async function() {
6+
const document = {
7+
asyncapi: '2.0.0',
8+
info: {
9+
title: 'Valid AsyncApi document',
10+
version: '1.0',
11+
},
12+
channels: {}
13+
}
14+
const { parsed, diagnostics } = await parse(document);
15+
16+
expect(parsed).toBeInstanceOf(AsyncAPIDocument);
17+
expect(diagnostics.length > 0).toEqual(true);
18+
});
19+
20+
it('should parse invalid document', async function() {
21+
const document = {
22+
asyncapi: '2.0.0',
23+
info: {
24+
title: 'Valid AsyncApi document',
25+
version: '1.0',
26+
},
27+
}
28+
const { parsed, diagnostics } = await parse(document);
29+
30+
expect(parsed).toEqual(undefined);
31+
expect(diagnostics.length > 0).toEqual(true);
32+
});
33+
});

0 commit comments

Comments
 (0)