Skip to content

Commit

Permalink
feat: add x-origin property
Browse files Browse the repository at this point in the history
  • Loading branch information
aeworxet committed Mar 11, 2024
1 parent e2e8c84 commit a9d96fb
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 123 deletions.
4 changes: 3 additions & 1 deletion src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export class Document {
* @return {string}
*/
string() {
return JSON.stringify(this._doc);
if (Object.keys(this._doc).length) {
return JSON.stringify(this._doc);
}
}
}
134 changes: 47 additions & 87 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,58 @@
import $RefParser from '@apidevtools/json-schema-ref-parser';
import { JSONPath } from 'jsonpath-plus';
import { merge } from 'lodash';
import { Parser } from '@asyncapi/parser';
import { addXOrigins } from './util';

import type { $Refs } from '@apidevtools/json-schema-ref-parser';
import type { AsyncAPIObject, ComponentsObject, MessageObject } from './spec-types';
import { AsyncAPIObject } from 'spec-types';

/**
* @class
* @private
*/
class ExternalComponents {
ref;
resolvedJSON;
constructor(ref: string, resolvedJSON: string) {
this.ref = ref;
this.resolvedJSON = resolvedJSON;
}

getKey() {
const keys = this.ref.split('/');
return keys[keys.length - 1];
}

getValue() {
return this.resolvedJSON;
}
}

/**
* @private
*/
function crawlChannelPropertiesForRefs(JSONSchema: AsyncAPIObject) {
// eslint-disable-next-line
return JSONPath({ json: JSONSchema, path: `$.channels.*.*.message['$ref']` });
}

/**
* Checks if `ref` is an external reference.
* @param {string} ref
* @returns {boolean}
* @private
*/
export function isExternalReference(ref: string): boolean {
return typeof ref === 'string' && !ref.startsWith('#');
}

/**
*
* @param {Object[]} parsedJSON
* @param {$RefParser} $refs
* @returns {ExternalComponents}
* @private
*/
async function resolveExternalRefs(parsedJSON: AsyncAPIObject, $refs: $Refs) {
const componentObj: ComponentsObject = { messages: {} };
JSONPath({
json: parsedJSON,
resultType: 'all',
path: '$.channels.*.*.message',
}).forEach(
({ parent, parentProperty }: { parent: any; parentProperty: string }) => {
const ref = parent[String(parentProperty)]['$ref'];
if (isExternalReference(ref)) {
const value: any = $refs.get(ref);
const component = new ExternalComponents(ref, value);
if (componentObj.messages) {
componentObj.messages[String(component.getKey())] =
component.getValue() as unknown as MessageObject;
}
parent[String(parentProperty)][
'$ref'
] = `#/components/messages/${component.getKey()}`;
}
}
);
return componentObj;
}
const parser = new Parser();

/**
* Resolves external references and updates $refs.
* @param {Object[]} JSONSchema
* @private
*/
export async function parse(JSONSchema: AsyncAPIObject) {
const $ref: any = await $RefParser.resolve(JSONSchema);
const refs = crawlChannelPropertiesForRefs(JSONSchema);
for (const ref of refs) {
if (isExternalReference(ref)) {
const componentObject = await resolveExternalRefs(JSONSchema, $ref);
if (JSONSchema.components) {
merge(JSONSchema.components, componentObject);
} else {
JSONSchema.components = componentObject;
}
}
}
addXOrigins(JSONSchema);

const dereferencedJSONSchema = await $RefParser.dereference(JSONSchema, {
dereference: {
circular: false,
// excludedPathMatcher: (path: string): boolean => {
// return (
// // prettier-ignore
// !!(/#\/channels\/[a-zA-Z0-9]*\/servers/).exec(path) ||
// !!(/#\/operations\/[a-zA-Z0-9]*\/channel/).exec(path) ||
// !!(/#\/operations\/[a-zA-Z0-9]*\/messages/).exec(path) ||
// !!(/#\/operations\/[a-zA-Z0-9]*\/reply\/channel/).exec(path) ||
// !!(/#\/operations\/[a-zA-Z0-9]*\/reply\/messages/).exec(path) ||
// !!(/#\/components\/channels\/[a-zA-Z0-9]*\/servers/).exec(path) ||
// !!(/#\/components\/operations\/[a-zA-Z0-9]*\/channel/).exec(path) ||
// !!(/#\/components\/operations\/[a-zA-Z0-9]*\/messages/).exec(path) ||
// !!(/#\/components\/operations\/[a-zA-Z0-9]*\/reply\/channel/).exec(
// path
// ) ||
// !!(/#\/components\/operations\/[a-zA-Z0-9]*\/reply\/messages/).exec(
// path
// )
// );
// },
},
});

const result = await parser.validate(

Check failure on line 42 in src/parser.ts

View workflow job for this annotation

GitHub Actions / Test NodeJS PR - ubuntu-latest

Immediately return this expression instead of assigning it to the temporary variable "result"
JSON.parse(JSON.stringify(dereferencedJSONSchema))
);

// If Parser's `validate()` function returns a non-empty array, that means
// there were errors during validation. Thus, the array is outputted as a list
// of remarks, and the program exits without doing anything further.
// if (result.length !== 0) {
// console.log(
// 'Validation of the resulting AsyncAPI Document failed.\nList of remarks:\n',
// result
// );
// throw new Error();
// }

return result;
}
12 changes: 0 additions & 12 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,6 @@ export const toJS = (asyncapiYAMLorJSON: string | object) => {
return yaml.load(asyncapiYAMLorJSON);
};

/**
* @private
*/
export const validate = async (
parsedJSONs: AsyncAPIObject[],
parser: { parse(asyncapi: string | any): Promise<any> }
) => {
for (const parsedJSON of parsedJSONs) {
await parser.parse(cloneDeep(parsedJSON));
}
};

/**
*
* @param {Object} asyncapiDocuments
Expand Down
27 changes: 4 additions & 23 deletions tests/lib/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { describe, expect, test } from '@jest/globals';
import bundle from '../../src';
import { isExternalReference } from '../../src/parser';
import { isExternalReference } from '../../src/util';
import fs from 'fs';
import path from 'path';

import type { ReferenceObject } from '../../src/spec-types';

describe('[integration testing] bundler should ', () => {
test('should return bundled doc', async () => {
const files = ['./tests/camera.yml', './tests/audio.yml'];
Expand All @@ -25,23 +23,6 @@ describe('[integration testing] bundler should ', () => {
expect(response).toBeDefined();
});

test('should bundle references into components', async () => {
const files = ['./tests/asyncapi.yaml'];
const doc = await bundle(
files.map(file =>
fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8')
),
{
referenceIntoComponents: true,
}
);

const asyncapiObject = doc.json();
const message = asyncapiObject?.channels?.['user/signedup']?.subscribe?.message as ReferenceObject;

expect(message.$ref).toMatch('#/components/messages/UserSignedUp');
});

test('should not throw if value of `$ref` is not a string', async () => {
const files = ['./tests/wrong-ref-not-string.yaml'];

Expand All @@ -54,7 +35,7 @@ describe('[integration testing] bundler should ', () => {
fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8')
),
{
referenceIntoComponents: true,
referenceIntoComponents: false,
}
)
).resolves;
Expand All @@ -72,7 +53,7 @@ describe('[integration testing] bundler should ', () => {
fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8')
),
{
referenceIntoComponents: true,
referenceIntoComponents: false,
}
)
).resolves;
Expand All @@ -84,7 +65,7 @@ describe('[integration testing] bundler should ', () => {
expect(
await bundle(
files.map(file => fs.readFileSync(path.resolve(process.cwd(), file), 'utf-8')),
{ referenceIntoComponents: true, base: fs.readFileSync(path.resolve(process.cwd(), './tests/base-option/base.yaml'), 'utf-8') }
{ referenceIntoComponents: false, base: fs.readFileSync(path.resolve(process.cwd(), './tests/base-option/base.yaml'), 'utf-8') }
)
).resolves;

Expand Down

0 comments on commit a9d96fb

Please sign in to comment.