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

feat: add a traverse schema function #198

Merged
merged 14 commits into from
Dec 10, 2020
2 changes: 1 addition & 1 deletion dist/bundle.js

Large diffs are not rendered by default.

291 changes: 186 additions & 105 deletions lib/models/asyncapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ const xParserSchemaId = 'x-parser-schema-id';
const xParserCircle = 'x-parser-circular';
const xParserCircleProps = 'x-parser-circular-props';

const SchemaIteratorCallbackType = Object.freeze({
NEW_SCHEMA: 'NEW_SCHEMA',
END_SCHEMA: 'END_SCHEMA'
});

const SchemaTypesToIterate = Object.freeze({
parameters: 'parameters',
payloads: 'payloads',
headers: 'headers',
components: 'components',
objects: 'objects',
arrays: 'arrays',
oneOfs: 'oneOfs',
allOfs: 'allOfs',
anyOfs: 'anyOfs'
});

/**
* Implements functions to deal with the AsyncAPI document.
* @class
Expand All @@ -32,6 +49,7 @@ class AsyncAPIDocument extends Base {
assignNameToAnonymousMessages(this);
assignNameToComponentMessages(this);

markCircularSchemas(this);
assignUidToComponentSchemas(this);
assignUidToParameterSchemas(this);
assignIdToAnonymousSchemas(this);
Expand Down Expand Up @@ -175,18 +193,12 @@ class AsyncAPIDocument extends Base {
*/
allSchemas() {
const schemas = new Map();
const callback = (schema) => {
const allSchemasCallback = (schema) => {
if (schema.uid()) {
schemas.set(schema.uid(), schema);
}
};
schemaDocument(this, callback);
if (this.hasComponents()) {
Object.values(this.components().schemas()).forEach(s => {
recursiveSchema(s, callback);
});
}

traverseAsyncApiDocument(this, allSchemasCallback);
return schemas;
}

Expand All @@ -196,6 +208,69 @@ class AsyncAPIDocument extends Base {
hasCircular() {
return !!this._json[String(xParserCircle)];
}

/**
* @callback TraverseSchemas
jonaslagoni marked this conversation as resolved.
Show resolved Hide resolved
* @param {Schema} schema
* @param {SchemaIteratorCallbackType} callbackType
* @param {String} propName if the schema is from a property get the name of such
* @returns {boolean} should it continue deeper
*
*/
magicmatatjahu marked this conversation as resolved.
Show resolved Hide resolved

/**
* Traverse schemas in the document and select which types of schemas to include.
* By default all schemas are iterated
*
* @param {TraverseSchemas} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
traverseSchemas(callback, schemaTypesToIterate) {
traverseAsyncApiDocument(this, callback, schemaTypesToIterate);
}
}

/**
* Marks all recursive schemas as recursive.
*
* @private
* @param {AsyncAPIDocument} doc
*/
function markCircularSchemas(doc) {
const seenObj = [];
const lastSchema = [];

//Mark the schema as recursive
const markCircular = (schema, prop) => {
if (schema.type() === 'array') return schema.json()[String(xParserCircle)] = true;
const circPropsList = schema.json()[String(xParserCircleProps)] || [];
if (prop !== undefined) {
circPropsList.push(prop);
}
schema.json()[String(xParserCircleProps)] = circPropsList;
};
magicmatatjahu marked this conversation as resolved.
Show resolved Hide resolved

//callback to use for iterating through the schemas
const circularCheckCallback = (schema, propName, type) => {
switch (type) {
case SchemaIteratorCallbackType.END_SCHEMA:
lastSchema.pop();
seenObj.pop();
break;
case SchemaIteratorCallbackType.NEW_SCHEMA:
const schemaJson = schema.json();
if (seenObj.includes(schemaJson)) {
const schemaToUse = lastSchema.length > 0 ? lastSchema[lastSchema.length-1] : schema;
markCircular(schemaToUse, propName);
return false;
}
//Save a list of seen objects and last schema which should be marked if its recursive
seenObj.push(schemaJson);
lastSchema.push(schema);
return true;
}
};
traverseAsyncApiDocument(doc, circularCheckCallback);
}

/**
Expand Down Expand Up @@ -276,88 +351,59 @@ function addNameToKey(messages, number) {
}

/**
* Function that indicates that a circular reference was detected.
* @private
* @param {Schema} schema schema that is currently accessed and need to be checked if it is a first time
* @param {Array} seenObjects list of objects that were already seen during recursion
*/
function isCircular(schema, seenObjects) {
return seenObjects.includes(schema.json());
}

/**
* Mark schema as being a circular ref
*
* @private
* @param {Schema} schema schema that should be marked as circular
*/
function markCircular(schema, prop) {
if (schema.type() === 'array') return schema.json()[String(xParserCircle)] = true;

const circPropsList = schema.json()[String(xParserCircleProps)] || [];
circPropsList.push(prop);
schema.json()[String(xParserCircleProps)] = circPropsList;
}

/**
* Callback that is called foreach schema found
* @private
* @callback FoundSchemaCallback
* @param {Schema} schema found.
*/
/**
* Recursively go through each schema and execute callback.
*
* @private
* @param {Schema} schema found.
* @param {FoundSchemaCallback} callback
*/
function recursiveSchema(schemaContent, callback) {
if (schemaContent === null) return;
const seenObj = [];

return crawl(schemaContent, seenObj, callback);
}

/**
* Schema crawler
* Traverse current schema and all nested schemas.
*
* @private
* @param {Schema} schemaContent schema.
* @param {Array} seenObj schema elements that crowler went through already.
* @param {Function} callback(schema)
* the function that is called foreach schema found.
* schema {Schema}: the found schema.
* @param {TraverseSchemas} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
function crawl(schema, seenObj, callback) {
if (isCircular(schema, seenObj)) return true;
function traverseSchema(schema, callback, prop, schemaTypesToIterate) {
if (schema === null) return;
if (!schemaTypesToIterate.includes(SchemaTypesToIterate.arrays) && schema.type() === 'array') return;
if (!schemaTypesToIterate.includes(SchemaTypesToIterate.objects) && schema.type() === 'object') return;
if (schema.isCircular()) return;
if (callback(schema, prop, SchemaIteratorCallbackType.NEW_SCHEMA) === false) return;

seenObj.push(schema.json());
callback(schema);
if (schema.type() !== undefined) {
switch (schema.type()) {
case 'object':
recursiveSchemaObject(schema, seenObj, callback);
recursiveSchemaObject(schema, callback, schemaTypesToIterate);
break;
case 'array':
recursiveSchemaArray(schema, seenObj, callback);
recursiveSchemaArray(schema, callback, schemaTypesToIterate);
break;
}
} else {
traverseCombinedSchemas(schema, callback, schemaTypesToIterate);
}
callback(schema, prop, SchemaIteratorCallbackType.END_SCHEMA);
}

/**
* Traverse combined notions
*
* @private
* @param {Schema} schemaContent schema.
* @param {TraverseSchemas} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
function traverseCombinedSchemas(schema, callback, schemaTypesToIterate) {
//check for allOf, oneOf, anyOf
const checkCombiningSchemas = (combineArray) => {
if (combineArray !== null && combineArray.length > 0) {
combineArray.forEach(combineSchema => {
if (crawl(combineSchema, seenObj, callback)) markCircular(schema);
});
}
};
const checkCombiningSchemas = (combineArray) => {
(combineArray || []).forEach(combineSchema => {
traverseSchema(combineSchema, callback, null, schemaTypesToIterate);
});
};
if (schemaTypesToIterate.includes(SchemaTypesToIterate.allOfs)) {
checkCombiningSchemas(schema.allOf());
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.anyOfs)) {
checkCombiningSchemas(schema.anyOf());
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.oneOfs)) {
checkCombiningSchemas(schema.oneOf());
}

seenObj.pop();
}

/**
Expand All @@ -366,31 +412,66 @@ function crawl(schema, seenObj, callback) {
* @private
* @param {AsyncAPIDocument} doc
* @param {FoundSchemaCallback} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
function schemaDocument(doc, callback) {
function traverseAsyncApiDocument(doc, callback, schemaTypesToIterate) {
if (!schemaTypesToIterate) {
schemaTypesToIterate = Object.values(SchemaTypesToIterate);
}
if (doc.hasChannels()) {
doc.channelNames().forEach(channelName => {
const channel = doc.channel(channelName);
traverseChannel(channel, callback, schemaTypesToIterate);
});
}
if (doc.hasComponents() && schemaTypesToIterate.includes(SchemaTypesToIterate.components)) {
Object.values(doc.components().schemas()).forEach(s => {
traverseSchema(s, callback, null, schemaTypesToIterate);
});
}
}

Object.values(channel.parameters()).forEach(p => {
recursiveSchema(p.schema(), callback);
});

if (channel.hasPublish()) {
channel.publish().messages().forEach(m => {
recursiveSchema(m.headers(), callback);
recursiveSchema(m.payload(), callback);
});
}
if (channel.hasSubscribe()) {
channel.subscribe().messages().forEach(m => {
recursiveSchema(m.headers(), callback);
recursiveSchema(m.payload(), callback);
});
}
/**
* Go through each schema in channel
*
* @private
* @param {Channel} channel
* @param {FoundSchemaCallback} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
function traverseChannel(channel, callback, schemaTypesToIterate) {
if (schemaTypesToIterate.includes(SchemaTypesToIterate.parameters)) {
Object.values(channel.parameters()).forEach(p => {
traverseSchema(p.schema(), callback, null, schemaTypesToIterate);
});
}
if (channel.hasPublish()) {
magicmatatjahu marked this conversation as resolved.
Show resolved Hide resolved
channel.publish().messages().forEach(m => {
traverseMessage(m, callback, schemaTypesToIterate);
});
}
if (channel.hasSubscribe()) {
channel.subscribe().messages().forEach(m => {
traverseMessage(m, callback, schemaTypesToIterate);
});
}
}
/**
* Go through each schema in a message
*
* @private
* @param {Message} message
* @param {FoundSchemaCallback} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
function traverseMessage(message, callback, schemaTypesToIterate) {
if (schemaTypesToIterate.includes(SchemaTypesToIterate.headers)) {
traverseSchema(message.headers(), callback, null, schemaTypesToIterate);
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.payloads)) {
traverseSchema(message.payload(), callback, null, schemaTypesToIterate);
}
}

/**
* Gives schemas id to all anonymous schemas.
Expand All @@ -405,28 +486,28 @@ function assignIdToAnonymousSchemas(doc) {
schema.json()[String(xParserSchemaId)] = `<anonymous-schema-${++anonymousSchemaCounter}>`;
}
};
schemaDocument(doc, callback);
traverseAsyncApiDocument(doc, callback);
}

/**
* Recursively go through schema of object type and execute callback.
*
* @private
* @param {Schema} schema Object type.
* @param {Array} seenObj schema elements that crawler went through already.
* @param {Function} callback(schema)
* the function that is called foreach schema found.
* schema {Schema}: the found schema.
* @param {TraverseSchemas} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
function recursiveSchemaObject(schema, seenObj, callback) {
function recursiveSchemaObject(schema, callback, schemaTypesToIterate) {
if (schema.additionalProperties() !== undefined && typeof schema.additionalProperties() !== 'boolean') {
const additionalSchema = schema.additionalProperties();
if (crawl(additionalSchema, seenObj, callback)) markCircular(schema);
traverseSchema(additionalSchema, callback, null, schemaTypesToIterate);
}
if (schema.properties() !== null) {
const props = schema.properties();
for (const [prop, propertySchema] of Object.entries(props)) {
if (crawl(propertySchema, seenObj, callback)) markCircular(schema, prop);
const circularProps = schema.circularProps();
if (circularProps !== undefined && circularProps.includes(prop)) return;
traverseSchema(propertySchema, callback, prop, schemaTypesToIterate);
}
}
}
Expand All @@ -436,23 +517,23 @@ function recursiveSchemaObject(schema, seenObj, callback) {
*
* @private
* @param {Schema} schema Array type.
* @param {Array} seenObj schema elements that crowler went through already.
* @param {Function} callback(schema)
* the function that is called foreach schema found.
* schema {Schema}: the found schema.
* @param {TraverseSchemas} callback
* @param {SchemaTypesToIterate[]} schemaTypesToIterate
*/
function recursiveSchemaArray(schema, seenObj, callback) {
function recursiveSchemaArray(schema, callback, schemaTypesToIterate) {
if (schema.additionalItems() !== undefined) {
const additionalArrayItems = schema.additionalItems();
if (crawl(additionalArrayItems, seenObj, callback)) markCircular(schema);
traverseSchema(additionalArrayItems, callback, null, schemaTypesToIterate);
}

if (schema.items() !== null) {
if (Array.isArray(schema.items())) {
schema.items().forEach(arraySchema => {
if (crawl(arraySchema, seenObj, callback)) markCircular(schema);
traverseSchema(arraySchema, callback, null, schemaTypesToIterate);
});
} else if (crawl(schema.items(), seenObj, callback)) markCircular(schema);
} else {
traverseSchema(schema.items(), callback, null, schemaTypesToIterate);
}
}
}

Expand Down
Loading