forked from asyncapi/parser-js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: add iterator for traversing an AsyncAPI doc (asyncapi#604)
- Loading branch information
1 parent
565d8ea
commit a56ea22
Showing
3 changed files
with
379 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
import { AsyncAPIDocumentInterface } from 'models'; | ||
import { ChannelInterface } from 'models/channel'; | ||
import { ChannelParameterInterface } from 'models/channel-parameter'; | ||
import { MessageInterface } from 'models/message'; | ||
import { MessageTraitInterface } from 'models/message-trait'; | ||
import { SchemaInterface } from 'models/schema'; | ||
|
||
/** | ||
* The different kind of stages when crawling a schema. | ||
*/ | ||
export enum SchemaIteratorCallbackType { | ||
NEW_SCHEMA = 'NEW_SCHEMA', // The crawler just started crawling a schema. | ||
END_SCHEMA = 'END_SCHEMA', // The crawler just finished crawling a schema. | ||
} | ||
|
||
/** | ||
* The different types of schemas you can iterate | ||
*/ | ||
export enum SchemaTypesToIterate { | ||
Parameters = 'parameters', // Crawl all schemas in payloads | ||
Payloads = 'payloads', // Crawl all schemas in payloads | ||
Headers = 'headers', // Crawl all schemas in headers | ||
Components = 'components', // Crawl all schemas in components | ||
Objects = 'objects', // Crawl all schemas of type object | ||
Arrays = 'arrays', // Crawl all schemas of type array | ||
OneOfs = 'oneOfs', // Crawl all schemas in oneOf's | ||
AllOfs = 'allOfs', // Crawl all schemas in allOf's | ||
AnyOfs = 'anyOfs', // Crawl all schemas in anyOf's | ||
Nots = 'nots', // Crawl all schemas in not field | ||
PropertyNames = 'propertyNames', // Crawl all schemas in propertyNames field | ||
PatternProperties = 'patternProperties', // Crawl all schemas in patternProperties field | ||
Contains = 'contains', // Crawl all schemas in contains field | ||
Ifs = 'ifs', // Crawl all schemas in if field | ||
Thenes = 'thenes', // Crawl all schemas in then field | ||
Elses = 'elses', // Crawl all schemas in else field | ||
Dependencies = 'dependencies', // Crawl all schemas in dependencies field | ||
Definitions = 'definitions', // Crawl all schemas in definitions field | ||
} | ||
|
||
export type TraverseOptions = { | ||
callback: Function | ||
schemaTypesToIterate: SchemaTypesToIterate[] | ||
seenSchemas: Set<any> | ||
} | ||
|
||
export type TraverseCallback = (schema: SchemaInterface, propOrIndex: string | number | null, callbackType: SchemaIteratorCallbackType) => void | ||
|
||
/** | ||
* Go through each channel and for each parameter, and message payload and headers recursively call the callback for each schema. | ||
*/ | ||
export function traverseAsyncApiDocument(doc: AsyncAPIDocumentInterface, callback: TraverseCallback, schemaTypesToIterate: SchemaTypesToIterate[]) { | ||
if (schemaTypesToIterate.length === 0) { | ||
schemaTypesToIterate = Object.values(SchemaTypesToIterate); | ||
} | ||
const options: TraverseOptions = { callback, schemaTypesToIterate, seenSchemas: new Set() }; | ||
|
||
if (!doc.channels().isEmpty()) { | ||
doc.channels().all().forEach(channel => { | ||
traverseChannel(channel, options); | ||
}); | ||
} | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Components) && !doc.components().isEmpty()) { | ||
const components = doc.components(); | ||
Object.values(components.messages().all() || {}).forEach(message => { | ||
traverseMessage(message, options); | ||
}); | ||
Object.values(components.schemas().all() || {}).forEach(schema => { | ||
traverseSchema(schema, null, options); | ||
}); | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Parameters)) { | ||
Object.values(components.channelParameters().filterBy((param: ChannelParameterInterface) => { return param.hasSchema(); })).forEach(parameter => { | ||
traverseSchema(parameter.schema() as SchemaInterface, null, options); | ||
}); | ||
} | ||
Object.values(components.messageTraits().all() || {}).forEach(messageTrait => { | ||
traverseMessageTrait(messageTrait, options); | ||
}); | ||
} | ||
} | ||
|
||
/* eslint-disable sonarjs/cognitive-complexity */ | ||
/** | ||
* Traverse current schema and all nested schemas. | ||
*/ | ||
function traverseSchema(schema: SchemaInterface, propOrIndex: string | number | null, options: TraverseOptions) { // NOSONAR | ||
if (!schema) return; | ||
|
||
const { schemaTypesToIterate, callback, 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() as SchemaInterface, null, options); | ||
} | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Ifs) && schema.if()) { | ||
traverseSchema(schema.if() as SchemaInterface, null, options); | ||
} | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Thenes) && schema.then()) { | ||
traverseSchema(schema.then() as SchemaInterface, null, options); | ||
} | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Elses) && schema.else()) { | ||
traverseSchema(schema.else() as SchemaInterface, 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. | ||
*/ | ||
function recursiveSchemaObject(schema: SchemaInterface, options: TraverseOptions) { | ||
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() as SchemaInterface, 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. | ||
*/ | ||
function recursiveSchemaArray(schema: SchemaInterface, options: any) { | ||
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('contains') && schema.contains()) { | ||
traverseSchema(schema.contains() as SchemaInterface, null, options); | ||
} | ||
} | ||
|
||
/** | ||
* Go through each schema in channel | ||
*/ | ||
function traverseChannel(channel: ChannelInterface, options: TraverseOptions) { | ||
if (!channel) return; | ||
const { schemaTypesToIterate } = options; | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Parameters)) { | ||
Object.values(channel.parameters().filterBy((param: ChannelParameterInterface) => { return param.hasSchema(); }) || {}).forEach(parameter => { | ||
traverseSchema(parameter.schema() as SchemaInterface, null, options); | ||
}); | ||
} | ||
|
||
channel.messages().all().forEach(message => { | ||
traverseMessage(message, options); | ||
}); | ||
} | ||
|
||
/** | ||
* Go through each schema in a message | ||
*/ | ||
function traverseMessage(message: MessageInterface, options: TraverseOptions) { | ||
if (!message) return; | ||
const { schemaTypesToIterate } = options; | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Headers) && message.hasHeaders()) { | ||
traverseSchema(message.headers() as SchemaInterface, null, options); | ||
} | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Payloads) && message.hasPayload()) { | ||
traverseSchema(message.payload() as SchemaInterface, null, options); | ||
} | ||
} | ||
|
||
/** | ||
* Go through each schema in a messageTrait | ||
*/ | ||
function traverseMessageTrait(messageTrait: MessageTraitInterface, options: TraverseOptions) { | ||
if (!messageTrait) return; | ||
const { schemaTypesToIterate } = options; | ||
if (schemaTypesToIterate.includes(SchemaTypesToIterate.Headers) && messageTrait.hasHeaders()) { | ||
traverseSchema(messageTrait.headers() as SchemaInterface, null, options); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { SchemaIteratorCallbackType, SchemaTypesToIterate, traverseAsyncApiDocument, TraverseCallback } from '../src/iterator'; | ||
import { Parser } from '../src/parser'; | ||
import { parse } from '../src/parse'; | ||
import { AsyncAPIDocumentInterface, AsyncAPIDocumentV2, SchemaV2 } from '../src/models'; | ||
import { SchemaInterface } from '../src/models/schema'; | ||
|
||
const document = { | ||
asyncapi: '2.0.0', | ||
info: { | ||
title: 'Valid AsyncApi document', | ||
version: '1.0', | ||
}, | ||
channels: { | ||
myChannel: { | ||
publish: { | ||
operationId: 'MyOperation', | ||
message: { | ||
payload: { | ||
type: 'object', | ||
properties: { | ||
exampleField: { | ||
type: 'string' | ||
}, | ||
exampleNumber: { | ||
type: 'number' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
components: { | ||
schemas: { | ||
anotherSchema: { | ||
type: 'object', | ||
properties: { | ||
anotherExampleField: { | ||
type: 'string' | ||
}, | ||
anotherExampleNumber: { | ||
type: 'number' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
|
||
type expectedCallback = { | ||
schema: SchemaInterface, | ||
propOrIndex: string | number | null, | ||
callbackType: SchemaIteratorCallbackType, | ||
} | ||
|
||
describe('Traverse AsyncAPI document', function() { | ||
const parser = new Parser(); | ||
describe('traverseAsyncApiDocument()', function() { | ||
it('should traverse all possible schemas from a valid document', async function() { | ||
const { parsed } = await parse(parser, document); | ||
expect(parsed).toBeInstanceOf(AsyncAPIDocumentV2); | ||
|
||
const payload = parsed?.messages().all()[0].payload() as SchemaV2; | ||
const componentsSchema = parsed?.components().schemas().all()[0] as SchemaV2; | ||
const expectedCalls = [ | ||
// Schema from channels | ||
call(payload, SchemaIteratorCallbackType.NEW_SCHEMA), | ||
call(payload, SchemaIteratorCallbackType.NEW_SCHEMA, 'exampleField'), | ||
call(payload, SchemaIteratorCallbackType.END_SCHEMA, 'exampleField'), | ||
call(payload, SchemaIteratorCallbackType.NEW_SCHEMA, 'exampleNumber'), | ||
call(payload, SchemaIteratorCallbackType.END_SCHEMA, 'exampleNumber'), | ||
call(payload, SchemaIteratorCallbackType.END_SCHEMA), | ||
// Schema from components | ||
call(componentsSchema, SchemaIteratorCallbackType.NEW_SCHEMA), | ||
call(componentsSchema, SchemaIteratorCallbackType.NEW_SCHEMA, 'anotherExampleField'), | ||
call(componentsSchema, SchemaIteratorCallbackType.END_SCHEMA, 'anotherExampleField'), | ||
call(componentsSchema, SchemaIteratorCallbackType.NEW_SCHEMA, 'anotherExampleNumber'), | ||
call(componentsSchema, SchemaIteratorCallbackType.END_SCHEMA, 'anotherExampleNumber'), | ||
call(componentsSchema, SchemaIteratorCallbackType.END_SCHEMA) | ||
]; | ||
|
||
testCallback(expectedCalls, parsed as AsyncAPIDocumentInterface, []); | ||
}); | ||
|
||
it('should traverse few schemas from a valid document', async function() { | ||
const { parsed } = await parse(parser, document); | ||
expect(parsed).toBeInstanceOf(AsyncAPIDocumentV2); | ||
|
||
const componentsSchema = parsed?.components().schemas().all()[0] as SchemaV2; | ||
const expectedCalls = [ | ||
// Schema from components | ||
call(componentsSchema, SchemaIteratorCallbackType.NEW_SCHEMA), | ||
call(componentsSchema, SchemaIteratorCallbackType.NEW_SCHEMA, 'anotherExampleField'), | ||
call(componentsSchema, SchemaIteratorCallbackType.END_SCHEMA, 'anotherExampleField'), | ||
call(componentsSchema, SchemaIteratorCallbackType.NEW_SCHEMA, 'anotherExampleNumber'), | ||
call(componentsSchema, SchemaIteratorCallbackType.END_SCHEMA, 'anotherExampleNumber'), | ||
call(componentsSchema, SchemaIteratorCallbackType.END_SCHEMA) | ||
]; | ||
|
||
testCallback(expectedCalls, parsed as AsyncAPIDocumentInterface, [SchemaTypesToIterate.Components, SchemaTypesToIterate.Objects]); | ||
}); | ||
}); | ||
}); | ||
|
||
function testCallback(expectedCalls: expectedCallback[], document: AsyncAPIDocumentInterface, schemaTypesToIterate: SchemaTypesToIterate[]) { | ||
let callsLeft = expectedCalls.length; | ||
const callback = function(schema: SchemaInterface, propOrIndex: string | number | null, callbackType: SchemaIteratorCallbackType): void { | ||
callsLeft--; | ||
const expected = expectedCalls.shift(); | ||
expect(schema).toEqual(expected?.schema); | ||
expect(propOrIndex).toEqual(expected?.propOrIndex); | ||
expect(callbackType).toEqual(expected?.callbackType); | ||
}; | ||
|
||
traverseAsyncApiDocument(document, callback, schemaTypesToIterate); | ||
expect(callsLeft).toEqual(0); | ||
} | ||
|
||
function call(schema: SchemaInterface, callbackType: SchemaIteratorCallbackType, propOrIndex: string | number | null = null): expectedCallback { | ||
let schemaProperties = schema; | ||
if (propOrIndex) { | ||
schemaProperties = (schema.properties() as Record<string, SchemaInterface>)[propOrIndex as string]; | ||
} | ||
|
||
return { | ||
schema: schemaProperties, | ||
propOrIndex, | ||
callbackType, | ||
}; | ||
} |