/** * @readonly * @enum {SchemaIteratorCallbackType} */ /** * The different kind of stages when crawling a schema. * * @typedef SchemaIteratorCallbackType * @property {string} NEW_SCHEMA The crawler just started crawling a schema. * @property {string} END_SCHEMA The crawler just finished crawling a schema. */ const SchemaIteratorCallbackType = Object.freeze({ NEW_SCHEMA: 'NEW_SCHEMA', END_SCHEMA: 'END_SCHEMA' }); /** * * @readonly * @enum {SchemaTypesToIterate} */ /** * The different types of schemas you can iterate * * @typedef SchemaTypesToIterate * @property {string} parameters Crawl all schemas in parameters * @property {string} payloads Crawl all schemas in payloads * @property {string} headers Crawl all schemas in headers * @property {string} components Crawl all schemas in components * @property {string} objects Crawl all schemas of type object * @property {string} arrays Crawl all schemas of type array * @property {string} oneOfs Crawl all schemas in oneOf's * @property {string} allOfs Crawl all schemas in allOf's * @property {string} anyOfs Crawl all schemas in anyOf's * @property {string} nots Crawl all schemas in not field * @property {string} propertyNames Crawl all schemas in propertyNames field * @property {string} patternProperties Crawl all schemas in patternProperties field * @property {string} contains Crawl all schemas in contains field * @property {string} ifs Crawl all schemas in if field * @property {string} thenes Crawl all schemas in then field * @property {string} elses Crawl all schemas in else field * @property {string} dependencies Crawl all schemas in dependencies field * @property {string} definitions Crawl all schemas in definitions field */ const SchemaTypesToIterate = Object.freeze({ parameters: 'parameters', payloads: 'payloads', headers: 'headers', components: 'components', objects: 'objects', arrays: 'arrays', oneOfs: 'oneOfs', allOfs: 'allOfs', anyOfs: 'anyOfs', nots: 'nots', propertyNames: 'propertyNames', patternProperties: 'patternProperties', contains: 'contains', ifs: 'ifs', thenes: 'thenes', elses: 'elses', dependencies: 'dependencies', definitions: 'definitions', }); /* eslint-disable sonarjs/cognitive-complexity */ /** * Traverse current schema and all nested schemas. * * @private * @param {Schema} schema which is being crawled. * @param {(String | Number)} propOrIndex if the schema is from a property/index get the name/number of such. * @param {Object} options * @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema. * @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl. * @param {Set} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references */ function traverseSchema(schema, propOrIndex, options) { // NOSONAR if (!schema) return; const { callback, schemaTypesToIterate, seenSchemas } = options; // handle circular references const jsonSchema = schema.json(); if (seenSchemas.has(jsonSchema)) return; seenSchemas.add(jsonSchema); // `type` isn't required so save type as array in the fallback let types = schema.type() || []; // change primitive type to array of types for easier handling if (!Array.isArray(types)) { types = [types]; } if (!schemaTypesToIterate.includes(SchemaTypesToIterate.objects) && types.includes('object')) return; if (!schemaTypesToIterate.includes(SchemaTypesToIterate.arrays) && types.includes('array')) return; // check callback `NEW_SCHEMA` case if (callback(schema, propOrIndex, SchemaIteratorCallbackType.NEW_SCHEMA) === false) return; if (schemaTypesToIterate.includes(SchemaTypesToIterate.objects) && types.includes('object')) { recursiveSchemaObject(schema, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.arrays) && types.includes('array')) { recursiveSchemaArray(schema, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.oneOfs)) { (schema.oneOf() || []).forEach((combineSchema, idx) => { traverseSchema(combineSchema, idx, options); }); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.anyOfs)) { (schema.anyOf() || []).forEach((combineSchema, idx) => { traverseSchema(combineSchema, idx, options); }); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.allOfs)) { (schema.allOf() || []).forEach((combineSchema, idx) => { traverseSchema(combineSchema, idx, options); }); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.nots) && schema.not()) { traverseSchema(schema.not(), null, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.ifs) && schema.if()) { traverseSchema(schema.if(), null, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.thenes) && schema.then()) { traverseSchema(schema.then(), null, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.elses) && schema.else()) { traverseSchema(schema.else(), null, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.dependencies)) { Object.entries(schema.dependencies() || {}).forEach(([depName, dep]) => { // do not iterate dependent required if (dep && !Array.isArray(dep)) { traverseSchema(dep, depName, options); } }); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.definitions)) { Object.entries(schema.definitions() || {}).forEach(([defName, def]) => { traverseSchema(def, defName, options); }); } callback(schema, propOrIndex, SchemaIteratorCallbackType.END_SCHEMA); seenSchemas.delete(jsonSchema); } /* eslint-enable sonarjs/cognitive-complexity */ /** * Recursively go through schema of object type and execute callback. * * @private * @param {Object} options * @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema. * @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl. * @param {Set} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references */ function recursiveSchemaObject(schema, options) { Object.entries(schema.properties() || {}).forEach(([propertyName, property]) => { traverseSchema(property, propertyName, options); }); const additionalProperties = schema.additionalProperties(); if (typeof additionalProperties === 'object') { traverseSchema(additionalProperties, null, options); } const schemaTypesToIterate = options.schemaTypesToIterate; if (schemaTypesToIterate.includes(SchemaTypesToIterate.propertyNames) && schema.propertyNames()) { traverseSchema(schema.propertyNames(), null, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.patternProperties)) { Object.entries(schema.patternProperties() || {}).forEach(([propertyName, property]) => { traverseSchema(property, propertyName, options); }); } } /** * Recursively go through schema of array type and execute callback. * * @private * @param {Object} options * @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema. * @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl. * @param {Set} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references */ function recursiveSchemaArray(schema, options) { const items = schema.items(); if (items) { if (Array.isArray(items)) { items.forEach((item, idx) => { traverseSchema(item, idx, options); }); } else { traverseSchema(items, null, options); } } const additionalItems = schema.additionalItems(); if (typeof additionalItems === 'object') { traverseSchema(additionalItems, null, options); } if (options.schemaTypesToIterate.includes(SchemaTypesToIterate.contains) && schema.contains()) { traverseSchema(schema.contains(), null, options); } } /** * Go through each channel and for each parameter, and message payload and headers recursively call the callback for each schema. * * @private * @param {AsyncAPIDocument} doc parsed AsyncAPI Document * @param {FoundSchemaCallback} callback callback used when crawling a schema. * @param {SchemaTypesToIterate[]} schemaTypesToIterate list of schema types to crawl. */ function traverseAsyncApiDocument(doc, callback, schemaTypesToIterate) { if (!schemaTypesToIterate) { schemaTypesToIterate = Object.values(SchemaTypesToIterate); } const options = { callback, schemaTypesToIterate, seenSchemas: new Set() }; if (doc.hasChannels()) { Object.values(doc.channels()).forEach(channel => { traverseChannel(channel, options); }); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.components) && doc.hasComponents()) { const components = doc.components(); Object.values(components.messages() || {}).forEach(message => { traverseMessage(message, options); }); Object.values(components.schemas() || {}).forEach(schema => { traverseSchema(schema, null, options); }); if (schemaTypesToIterate.includes(SchemaTypesToIterate.parameters)) { Object.values(components.parameters() || {}).forEach(parameter => { traverseSchema(parameter.schema(), null, options); }); } Object.values(components.messageTraits() || {}).forEach(messageTrait => { traverseMessageTrait(messageTrait, options); }); } } /** * Go through each schema in channel * * @private * @param {Channel} channel * @param {Object} options * @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema. * @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl. * @param {Set} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references */ function traverseChannel(channel, options) { if (!channel) return; const { schemaTypesToIterate } = options; if (schemaTypesToIterate.includes(SchemaTypesToIterate.parameters)) { Object.values(channel.parameters() || {}).forEach(parameter => { traverseSchema(parameter.schema(), null, options); }); } if (channel.hasPublish()) { channel.publish().messages().forEach(message => { traverseMessage(message, options); }); } if (channel.hasSubscribe()) { channel.subscribe().messages().forEach(message => { traverseMessage(message, options); }); } } /** * Go through each schema in a message * * @private * @param {Message} message * @param {Object} options * @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema. * @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl. * @param {Set} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references */ function traverseMessage(message, options) { if (!message) return; const { schemaTypesToIterate } = options; if (schemaTypesToIterate.includes(SchemaTypesToIterate.headers)) { traverseSchema(message.headers(), null, options); } if (schemaTypesToIterate.includes(SchemaTypesToIterate.payloads)) { traverseSchema(message.payload(), null, options); } } /** * Go through each schema in a messageTrait * * @private * @param {MessageTrait} messageTrait * @param {Object} options * @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema. * @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl. * @param {Set} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references */ function traverseMessageTrait(messageTrait, options) { if (!messageTrait) return; const { schemaTypesToIterate } = options; if (schemaTypesToIterate.includes(SchemaTypesToIterate.headers)) { traverseSchema(messageTrait.headers(), null, options); } } module.exports = { SchemaIteratorCallbackType, SchemaTypesToIterate, traverseAsyncApiDocument, };