Skip to content

Commit

Permalink
fix: add missing anonymous schema id's (#43)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukasz Gornicki <[email protected]>
  • Loading branch information
jonaslagoni and derberg authored Apr 9, 2020
1 parent 2056d4f commit 5328fb3
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 48 deletions.
143 changes: 106 additions & 37 deletions lib/models/asyncapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ class AsyncAPIDocument extends Base {
super(...args);

assignNameToAnonymousMessages(this);
assignIdToAnonymousSchemas(this);

assignNameToComponentMessages(this);

assignUidToComponentSchemas(this);
assignUidToParameterSchemas(this);
assignIdToAnonymousSchemas(this);
}

/**
Expand All @@ -37,7 +38,7 @@ class AsyncAPIDocument extends Base {
info() {
return new Info(this._json.info);
}

/**
* @returns {string}
*/
Expand All @@ -51,7 +52,7 @@ class AsyncAPIDocument extends Base {
hasServers() {
return !!this._json.servers;
}

/**
* @returns {Object<string, Server>}
*/
Expand All @@ -73,14 +74,14 @@ class AsyncAPIDocument extends Base {
hasChannels() {
return !!this._json.channels;
}

/**
* @returns {Object<string, Channel>}
*/
channels() {
return createMapOfType(this._json.channels, Channel, this);
}

/**
* @returns {string[]}
*/
Expand Down Expand Up @@ -139,7 +140,6 @@ class AsyncAPIDocument extends Base {
*/
allMessages() {
const messages = new Map();

if (this.hasChannels()) {
this.channelNames().forEach(channelName => {
const channel = this.channel(channelName);
Expand All @@ -155,13 +155,11 @@ class AsyncAPIDocument extends Base {
}
});
}

if (this.hasComponents()) {
Object.values(this.components().messages()).forEach(m => {
messages.set(m.uid(), m);
messages.set(m.uid(), m);
});
}

return messages;
}

Expand All @@ -170,7 +168,6 @@ class AsyncAPIDocument extends Base {
*/
allSchemas() {
const schemas = new Map();

if (this.hasChannels()) {
this.channelNames().forEach(channelName => {
const channel = this.channel(channelName);
Expand Down Expand Up @@ -205,29 +202,48 @@ class AsyncAPIDocument extends Base {
}
});
}

if (this.hasComponents()) {
Object.values(this.components().schemas()).forEach(s => {
schemas.set(s.uid(), s);
schemas.set(s.uid(), s);
});
}

return schemas;
}
}

function assignNameToComponentMessages(doc){

function assignNameToComponentMessages(doc) {
if (doc.hasComponents()) {
for(const [key, m] of Object.entries(doc.components().messages())){
for (const [key, m] of Object.entries(doc.components().messages())) {
if (m.name() === undefined) {
m.json()['x-parser-message-name'] = key;
}
}
}
}
function assignUidToComponentSchemas(doc){

/**
* Assign parameter keys as uid for the parameter schema.
*
* @param {AsyncAPIDocument} doc
*/
function assignUidToParameterSchemas(doc) {
doc.channelNames().forEach(channelName => {
const channel = doc.channel(channelName);
for (const [parameterKey, parameterSchema] of Object.entries(channel.parameters())) {
parameterSchema.json()['x-parser-schema-id'] = parameterKey;
}
});
}

/**
* Assign uid to component schemas.
*
* @param {AsyncAPIDocument} doc
*/
function assignUidToComponentSchemas(doc) {
if (doc.hasComponents()) {
for(const [key, s] of Object.entries(doc.components().schemas())){
for (const [key, s] of Object.entries(doc.components().schemas())) {
s.json()['x-parser-schema-id'] = key;
}
}
Expand Down Expand Up @@ -256,43 +272,96 @@ function assignNameToAnonymousMessages(doc) {
}
}

function assignIdToAnonymousSchemas(doc) {
let anonymousSchemaCounter = 0;
/**
* Recursively go through each schema and execute callback.
*
* @param {Schema} schema found.
* @param {Function} callback(schema)
* the function that is called foreach schema found.
* schema {Schema}: the found schema.
*/
function recursiveSchema(schema, callback) {
if (schema === null) return;
callback(schema);
if (schema.type() !== null) {
switch (schema.type()) {
case 'object':
const props = schema.properties();
for (const [, propertySchema] of Object.entries(props)) {
recursiveSchema(propertySchema, callback);
}
break;
case 'array':
if (Array.isArray(schema.items())) {
schema.items().forEach(arraySchema => {
recursiveSchema(arraySchema, callback);
});
} else {
recursiveSchema(schema.items(), callback);
}
break;
}
} else {
//check for allOf, oneOf, anyOf
const checkCombiningSchemas = (combineArray) => {
if (combineArray !== null && combineArray.length > 0) {
combineArray.forEach(combineSchema => {
recursiveSchema(combineSchema, callback);;
});
}
}
checkCombiningSchemas(schema.allOf());
checkCombiningSchemas(schema.anyOf());
checkCombiningSchemas(schema.oneOf());
}
}

/**
* Go through each channel and for each parameter, and message payload and headers recursively call the callback for each schema.
*
* @param {AsyncAPIDocument} doc
* @param {Function} callback(schema)
* the function that is called foreach schema found.
* schema {Schema}: the found schema.
*/
function schemaDocument(doc, callback) {
if (doc.hasChannels()) {
doc.channelNames().forEach(channelName => {
const channel = doc.channel(channelName);

Object.values(channel.parameters()).forEach(p => {
if (p.schema() && !p.schema().$id()) {
p.schema().json()['x-parser-schema-id'] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}
recursiveSchema(p.schema(), callback);
});

if (channel.hasPublish()) {
channel.publish().messages().forEach(m => {
if (m.headers() && !m.headers().$id()) {
m.headers().json()['x-parser-schema-id'] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}

if (m.payload() && !m.payload().$id()) {
m.payload().json()['x-parser-schema-id'] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}
recursiveSchema(m.headers(), callback);
recursiveSchema(m.payload(), callback);
});
}
if (channel.hasSubscribe()) {
channel.subscribe().messages().forEach(m => {
if (m.headers() && !m.headers().$id()) {
m.headers().json()['x-parser-schema-id'] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}

if (m.payload() && !m.payload().$id()) {
m.payload().json()['x-parser-schema-id'] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}
recursiveSchema(m.headers(), callback);
recursiveSchema(m.payload(), callback);
});
}
});
}
}

/**
* Gives schemas id to all anonymous schemas.
*
* @param {AsyncAPIDocument} doc
*/
function assignIdToAnonymousSchemas(doc) {
let anonymousSchemaCounter = 0;
const callback = (schema) => {
if (!schema.uid()) {
schema.json()['x-parser-schema-id'] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}
};
schemaDocument(doc, callback);
}

module.exports = addExtensions(AsyncAPIDocument);
8 changes: 8 additions & 0 deletions test/models/asyncapi_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ const { expect } = require('chai');
const AsyncAPIDocument = require('../../lib/models/asyncapi');

describe('AsyncAPIDocument', () => {
describe('assignUidToParameterSchemas()', () => {
it('should assign uids to parameters', () => {
const inputDoc = { "channels": { "smartylighting/{streetlightId}": { "parameters": { "streetlightId": { "schema": { "type": "string" } } } } } };
const expectedDoc = { "channels": { "smartylighting/{streetlightId}": { "parameters": { "streetlightId": { "schema": { "type": "string", "x-parser-schema-id": "<anonymous-schema-1>" }, "x-parser-schema-id": "streetlightId" } } } } }
const d = new AsyncAPIDocument(inputDoc);
expect(d.json()).to.be.deep.equal(expectedDoc);
});
});
describe('#ext()', () => {
it('should support extensions', () => {
const doc = { 'x-test': 'testing' };
Expand Down
22 changes: 11 additions & 11 deletions test/parse_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,30 @@ const expect = chai.expect;

const invalidYAML = fs.readFileSync(path.resolve(__dirname, "./malformed-asyncapi.yaml"), 'utf8');
const inputYAML = fs.readFileSync(path.resolve(__dirname, "./asyncapi.yaml"), 'utf8');
const outputJSON = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"externalDocs":{"x-extension":true,"url":"https://company.com/docs"},"message":{"payload":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}},"x-parser-schema-id":"testSchema"},"x-some-extension":"some extension","x-parser-original-traits":[{"x-some-extension":"some extension"}],"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"},"x-parser-original-traits":[{"externalDocs":{"url":"https://company.com/docs"}}]}}},"components":{"messages":{"testMessage":{"payload":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}},"x-parser-schema-id":"testSchema"},"x-some-extension":"some extension","x-parser-original-traits":[{"x-some-extension":"some extension"}],"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}},"x-parser-schema-id":"testSchema"}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}';
const outputJsonNoApplyTraits = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"traits":[{"externalDocs":{"url":"https://company.com/docs"}}],"externalDocs":{"x-extension":true,"url":"https://irrelevant.com"},"message":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}},"x-parser-schema-id":"testSchema"},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}}}},"components":{"messages":{"testMessage":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}},"x-parser-schema-id":"testSchema"},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string"},"test":{"type":"object","properties":{"testing":{"type":"string"}}}},"x-parser-schema-id":"testSchema"}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}';
const outputJSON = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"externalDocs":{"x-extension":true,"url":"https://company.com/docs"},"message":{"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"x-some-extension":"some extension","x-parser-original-traits":[{"x-some-extension":"some extension"}],"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"},"x-parser-original-traits":[{"externalDocs":{"url":"https://company.com/docs"}}]}}},"components":{"messages":{"testMessage":{"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"x-some-extension":"some extension","x-parser-original-traits":[{"x-some-extension":"some extension"}],"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}';
const outputJsonNoApplyTraits = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"mychannel":{"publish":{"traits":[{"externalDocs":{"url":"https://company.com/docs"}}],"externalDocs":{"x-extension":true,"url":"https://irrelevant.com"},"message":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}}}},"components":{"messages":{"testMessage":{"traits":[{"x-some-extension":"some extension"}],"payload":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"},"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-name":"testMessage"}},"schemas":{"testSchema":{"type":"object","properties":{"name":{"type":"string","x-parser-schema-id":"<anonymous-schema-1>"},"test":{"type":"object","properties":{"testing":{"type":"string","x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-2>"}},"x-parser-schema-id":"testSchema"}},"messageTraits":{"extension":{"x-some-extension":"some extension"}},"operationTraits":{"docs":{"externalDocs":{"url":"https://company.com/docs"}}}}}';
const invalidAsyncAPI = { "asyncapi": "2.0.0", "info": {} };
const errorsOfInvalidAsyncAPI = [{keyword: 'required',dataPath: '.info',schemaPath: '#/required',params: { missingProperty: 'title' },message: 'should have required property \'title\''},{keyword: 'required',dataPath: '.info',schemaPath: '#/required',params: { missingProperty: 'version' },message: 'should have required property \'version\''},{keyword: 'required',dataPath: '',schemaPath: '#/required',params: { missingProperty: 'channels' },message: 'should have required property \'channels\''}];
const errorsOfInvalidAsyncAPI = [{ keyword: 'required', dataPath: '.info', schemaPath: '#/required', params: { missingProperty: 'title' }, message: 'should have required property \'title\'' }, { keyword: 'required', dataPath: '.info', schemaPath: '#/required', params: { missingProperty: 'version' }, message: 'should have required property \'version\'' }, { keyword: 'required', dataPath: '', schemaPath: '#/required', params: { missingProperty: 'channels' }, message: 'should have required property \'channels\'' }];

describe('parse()', function () {
it('should parse YAML', async function () {
const result = await parser.parse(inputYAML, { path: __filename });
await expect(JSON.stringify(result.json())).to.equal(outputJSON);
});

it('should forward ajv errors and AsyncAPI json', async function () {
try {
await parser.parse(invalidAsyncAPI);
} catch(e) {
} catch (e) {
await expect(e.errors).to.deep.equal(errorsOfInvalidAsyncAPI);
await expect(e.parsedJSON).to.deep.equal(invalidAsyncAPI);
}
});

it('should not forward AsyncAPI json when it is not possible to convert it', async function () {
try {
await parser.parse('bad');
} catch(e) {
} catch (e) {
await expect(e.constructor.name).to.equal('ParserErrorNoJS');
await expect(e.parsedJSON).to.equal(undefined);
}
Expand All @@ -42,23 +42,23 @@ describe('parse()', function () {
it('should forward AsyncAPI json when version is not supported', async function () {
try {
await parser.parse('bad: true');
} catch(e) {
} catch (e) {
await expect(e.constructor.name).to.equal('ParserErrorUnsupportedVersion');
await expect(e.parsedJSON).to.deep.equal({ bad: true });
}
});

it('should not apply traits', async function () {
const result = await parser.parse(inputYAML, { path: __filename, applyTraits: false });
await expect(JSON.stringify(result.json())).to.equal(outputJsonNoApplyTraits);
});

it('should fail to resolve relative files when options.path is not provided', async function () {
const testFn = async () => await parser.parse(inputYAML);
await expect(testFn())
.to.be.rejectedWith(ParserError)
});

it('should throw error if document is invalid YAML', async function () {
const testFn = async () => await parser.parse(invalidYAML, { path: __filename });
await expect(testFn())
Expand Down

0 comments on commit 5328fb3

Please sign in to comment.