Skip to content

Commit 11aca9a

Browse files
refactor: add custom parser functionality
1 parent 136a632 commit 11aca9a

13 files changed

+374
-11
lines changed

package-lock.json

+16-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@semantic-release/release-notes-generator": "^9.0.1",
3434
"@types/jest": "^27.4.1",
3535
"@types/js-yaml": "^4.0.5",
36+
"@types/lodash": "^4.14.179",
3637
"conventional-changelog-conventionalcommits": "^4.2.3",
3738
"cross-env": "^7.0.3",
3839
"eslint": "^7.27.0",
@@ -50,7 +51,9 @@
5051
"@stoplight/spectral-core": "^1.10.1",
5152
"@stoplight/spectral-functions": "^1.5.1",
5253
"@stoplight/spectral-parsers": "^1.0.1",
53-
"@stoplight/spectral-rulesets": "^1.4.3"
54+
"@stoplight/spectral-rulesets": "^1.4.3",
55+
"jsonpath-plus": "^6.0.1",
56+
"lodash": "^4.17.21"
5457
},
5558
"release": {
5659
"branches": [

src/constants.ts

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export const xParserSpecStringified = 'x-parser-spec-stringified';
44
export const xParserMessageName = 'x-parser-message-name';
55
export const xParserSchemaId = 'x-parser-schema-id';
66

7-
export const xParserOriginalSchema = 'x-parser-original-schema';
87
export const xParserOriginalSchemaFormat = 'x-parser-original-schema-format';
98
export const xParserOriginalTraits = 'x-parser-original-traits';
109

src/custom-operations/apply-traits.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { JSONPath } from 'jsonpath-plus';
2+
3+
import { xParserOriginalTraits } from '../constants';
4+
import { mergePatch } from '../utils';
5+
6+
const v2TraitPaths = [
7+
// operations
8+
'$.channels.*.[publish,subscribe]',
9+
'$.components.channels.*.[publish,subscribe]',
10+
// messages
11+
'$.channels.*.[publish,subscribe].message',
12+
'$.channels.*.[publish,subscribe].message.oneOf.*',
13+
'$.components.channels.*.[publish,subscribe].message',
14+
'$.components.channels.*.[publish,subscribe].message.oneOf.*',
15+
'$.components.messages.*',
16+
];
17+
18+
export function applyTraitsV2(asyncapi: Record<string, unknown>) {
19+
applyAllTraits(asyncapi, v2TraitPaths);
20+
}
21+
22+
const v3TraitPaths = [
23+
// operations
24+
'$.channels.*.[publish,subscribe]',
25+
'$.components.channels.*.[publish,subscribe]',
26+
// messages
27+
'$.channels.*.[publish,subscribe].message',
28+
'$.channels.*.[publish,subscribe].message.oneOf.*',
29+
'$.components.channels.*.[publish,subscribe].message',
30+
'$.components.channels.*.[publish,subscribe].message.oneOf.*',
31+
'$.components.messages.*',
32+
];
33+
34+
export function applyTraitsV3(asyncapi: Record<string, unknown>) {
35+
applyAllTraits(asyncapi, v3TraitPaths);
36+
}
37+
38+
function applyAllTraits(asyncapi: Record<string, unknown>, paths: string[]) {
39+
paths.forEach(path => {
40+
JSONPath({
41+
path,
42+
json: asyncapi,
43+
resultType: 'value',
44+
callback(value) { applyTraits(value); },
45+
});
46+
});
47+
}
48+
49+
function applyTraits(value: Record<string, unknown>) {
50+
if (Array.isArray(value.traits)) {
51+
for (const trait of value.traits) {
52+
for (const key in trait) {
53+
value[String(key)] = mergePatch(value[String(key)], trait[String(key)]);
54+
}
55+
}
56+
57+
value[xParserOriginalTraits] = value.traits;
58+
delete value.traits;
59+
}
60+
}

src/custom-operations/index.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { applyTraitsV2, applyTraitsV3 } from './apply-traits';
2+
import { parseSchemasV2 } from './parse-schema';
3+
4+
import type { ParseOptions } from "../parse";
5+
import type { DetailedAsyncAPI } from "../types";
6+
7+
export async function customOperations(detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
8+
switch (detailed.semver.major) {
9+
case 2: return operationsV2(detailed, options);
10+
case 3: return operationsV3(detailed, options);
11+
}
12+
}
13+
14+
async function operationsV2(detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
15+
if (options.applyTraits) {
16+
applyTraitsV2(detailed.parsed);
17+
}
18+
if (options.parseSchemas) {
19+
await parseSchemasV2(detailed);
20+
}
21+
}
22+
23+
async function operationsV3(detailed: DetailedAsyncAPI, options: ParseOptions): Promise<void> {
24+
if (options.applyTraits) {
25+
applyTraitsV3(detailed.parsed);
26+
}
27+
}

src/custom-operations/parse-schema.ts

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { JSONPath } from 'jsonpath-plus';
2+
import { toPath } from 'lodash';
3+
4+
import { parseSchema, getDefaultSchemaFormat } from '../schema-parser';
5+
import { xParserOriginalSchemaFormat } from '../constants';
6+
7+
import type { ParseSchemaInput } from "../schema-parser";
8+
import type { DetailedAsyncAPI } from "../types";
9+
10+
interface ToParseItem {
11+
input: ParseSchemaInput;
12+
value: any;
13+
}
14+
15+
const customSchemasPathsV2 = [
16+
'$.channels.*.[publish,subscribe].message',
17+
'$.channels.*.[publish,subscribe].message.oneOf.*',
18+
'$.components.channels.*.[publish,subscribe].message',
19+
'$.components.channels.*.[publish,subscribe].message.oneOf.*',
20+
'$.components.messages.*',
21+
];
22+
23+
export async function parseSchemasV2(detailed: DetailedAsyncAPI) {
24+
const defaultSchemaFormat = getDefaultSchemaFormat(detailed.parsed.asyncapi as string);
25+
const parseItems: Array<ToParseItem> = [];
26+
27+
const visited: Set<unknown> = new Set();
28+
customSchemasPathsV2.forEach(path => {
29+
JSONPath({
30+
path,
31+
json: detailed.parsed,
32+
resultType: 'all',
33+
callback(result) {
34+
const value = result.value;
35+
if (visited.has(value)) {
36+
return;
37+
}
38+
visited.add(value);
39+
40+
const payload = value.payload;
41+
if (!payload) {
42+
return;
43+
}
44+
45+
parseItems.push({
46+
input: {
47+
asyncapi: detailed,
48+
data: payload,
49+
meta: undefined,
50+
path: [...toPath(result.path.slice(1)), 'payload'],
51+
schemaFormat: value.schemaFormat || defaultSchemaFormat,
52+
defaultSchemaFormat,
53+
},
54+
value,
55+
});
56+
},
57+
});
58+
});
59+
60+
return Promise.all(parseItems.map(parseSchemaV2));
61+
}
62+
63+
async function parseSchemaV2(item: ToParseItem) {
64+
item.value[xParserOriginalSchemaFormat] = item.input.schemaFormat;
65+
item.value.payload = await parseSchema(item.input);
66+
}

src/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ export { lint, validate } from './lint';
44
export { parse } from './parse';
55
export { stringify, unstringify } from './stringify';
66

7+
export { registerSchemaParser } from './schema-parser';
8+
export { AsyncAPISchemaParser } from './schema-parser/asyncapi-schema-parser';
9+
710
export type { LintOptions, ValidateOptions, ValidateOutput } from './lint';
811
export type { StringifyOptions } from './stringify';
912
export type { ParseOptions } from './parse';
10-
export type { ParserInput, ParserOutput, Diagnostic } from './types';
13+
export type { AsyncAPISemver, ParserInput, ParserOutput, Diagnostic, SchemaValidateResult } from './types';
14+
15+
export type { ValidateSchemaInput, ParseSchemaInput, SchemaParser } from './schema-parser'

src/parse.ts

+24-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { AsyncAPIDocument } from "./models";
2-
import { normalizeInput, toAsyncAPIDocument } from "./utils";
2+
3+
import { customOperations } from './custom-operations';
34
import { validate } from "./lint";
5+
import { stringify, unstringify } from './stringify';
6+
import { createDetailedAsyncAPI, normalizeInput, toAsyncAPIDocument } from "./utils";
7+
8+
import { xParserSpecParsed } from './constants';
49

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

813
export interface ParseOptions {
914
applyTraits?: boolean;
15+
parseSchemas?: boolean;
1016
validateOptions?: ValidateOptions;
1117
}
1218

@@ -33,15 +39,24 @@ export async function parse(asyncapi: ParserInput, options?: ParseOptions): Prom
3339
};
3440
}
3541

36-
const parsed = new AsyncAPIDocument(validated as Record<string, unknown>);
42+
const doc = {
43+
...(validated as Record<string, any>),
44+
[xParserSpecParsed]: true,
45+
}
46+
const parsed = unstringify(stringify(doc))?.json()!;
47+
48+
const detailed = createDetailedAsyncAPI(asyncapi as string | Record<string, unknown>, parsed);
49+
await customOperations(detailed, options);
50+
const parsedDoc = new AsyncAPIDocument(parsed);
51+
3752
return {
3853
source: asyncapi,
39-
parsed,
54+
parsed: parsedDoc,
4055
diagnostics,
4156
};
4257
} catch(err) {
4358
// TODO: throw proper error
44-
throw Error();
59+
throw err;
4560
}
4661
}
4762

@@ -55,10 +70,14 @@ function normalizeOptions(options?: ParseOptions): ParseOptions {
5570
// shall copy
5671
options = { ...defaultOptions, ...options };
5772

58-
// traits
73+
// applyTraits
5974
if (options.applyTraits === undefined) {
6075
options.applyTraits = true;
6176
}
77+
// parseSchemas
78+
if (options.parseSchemas === undefined) {
79+
options.parseSchemas = true;
80+
}
6281

6382
return options;
6483
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { SchemaParser } from "../schema-parser";
2+
3+
export function AsyncAPISchemaParser(): SchemaParser {
4+
return {
5+
validate,
6+
parse,
7+
getMimeTypes,
8+
}
9+
}
10+
11+
function validate() {
12+
13+
}
14+
15+
function parse() {
16+
17+
}
18+
19+
function getMimeTypes() {
20+
const mimeTypes = [
21+
'application/schema;version=draft-07',
22+
'application/schema+json;version=draft-07',
23+
'application/schema+yaml;version=draft-07',
24+
];
25+
['2.0.0', '2.1.0', '2.2.0', '2.3.0'].forEach(version => {
26+
mimeTypes.push(
27+
`application/vnd.aai.asyncapi;version=${version}`,
28+
`application/vnd.aai.asyncapi+json;version=${version}`,
29+
`application/vnd.aai.asyncapi+yaml;version=${version}`,
30+
);
31+
});
32+
return mimeTypes;
33+
}

0 commit comments

Comments
 (0)