Skip to content

Commit

Permalink
feat: new options to generate nice $id (asyncapi#516)
Browse files Browse the repository at this point in the history
  • Loading branch information
GreenRover committed Jun 8, 2022
1 parent 0348674 commit af9b7cd
Show file tree
Hide file tree
Showing 13 changed files with 1,179 additions and 55 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ node_modules
coverage
.DS_Store
test/sample_browser/bundle.js
dist/bundle.js
dist/bundle.js
.idea
*.iml
20 changes: 20 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
</dd>
</dl>

## Functions

<dl>
<dt><a href="#upperFirst">upperFirst(word)</a></dt>
<dd><p>returns a string with the first character of string capitalized, if that character is alphabetic.</p>
</dd>
</dl>

## Typedefs

<dl>
Expand Down Expand Up @@ -3783,6 +3791,7 @@ The complete list of parse configuration options used to parse the given data.
| [parse] | <code>Object</code> | Options object to pass to [json-schema-ref-parser](https://apitools.dev/json-schema-ref-parser/docs/options.html). |
| [resolve] | <code>Object</code> | Options object to pass to [json-schema-ref-parser](https://apitools.dev/json-schema-ref-parser/docs/options.html). |
| [applyTraits] | <code>Boolean</code> | Whether to resolve and apply traits or not. Defaults to true. |
| [genererateIdInSchema] | <code>Boolean</code> | Genereate speaking $id where everver not given by schema. |

<a name="MixinBindings"></a>

Expand Down Expand Up @@ -3979,6 +3988,17 @@ Implements functions to deal with the Tags object.
| --- | --- | --- |
| name | <code>string</code> | Name of the tag. |

<a name="upperFirst"></a>

## upperFirst(word)
returns a string with the first character of string capitalized, if that character is alphabetic.

**Kind**: global function

| Param | Type |
| --- | --- |
| word | <code>String</code> |

<a name="SchemaIteratorCallbackType"></a>

## SchemaIteratorCallbackType
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ The parser uses custom extensions to define additional information about the spe
- `x-parser-original-schema-format` holds information about the original schema format of the payload. You can use different schema formats with the AsyncAPI documents and the parser converts them to AsyncAPI schema. This is why different schema format is set, and the original one is preserved in the extension.
- `x-parser-original-payload` holds the original payload of the message. You can use different formats for payloads with the AsyncAPI documents and the parser converts them to. For example, it converts payload described with Avro schema to AsyncAPI schema. The original payload is preserved in the extension.
- [`x-parser-circular`](#circular-references)
- `x-parser-schema-id-level` Is internally used to generate use friendly `x-parser-schema-id` if option `genererateIdInSchema` is set. It indicates the traversal level of a schema. This is used to prefer name of well-defined schemas.
> **NOTE**: All extensions added by the parser (including all properties) should be retrieved using special functions. Names of extensions and their location may change, and their eventual changes will not be announced.
Expand Down
107 changes: 93 additions & 14 deletions lib/anonymousNaming.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
const {xParserMessageName, xParserSchemaId} = require('./constants');
const {traverseAsyncApiDocument} = require('./iterators');
const { xParserMessageName, xParserSchemaId, xParserSchemaIdLevel} = require('./constants');
const { traverseAsyncApiDocument, traverseSchema, SchemaIteratorCallbackType } = require('./iterators');
const { upperFirst } = require('./utils');

/**
* Assign message keys as message name to all the component messages.
*
* @private
* @param {AsyncAPIDocument} doc
* @param {AsyncAPIDocument} doc
* @param {boolean} traversIds Walk recursive through schema and try to find reasonable uid where no uids was given.
*/
function assignNameToComponentMessages(doc) {
function assignNameToComponentMessages(doc, traversIds = false) {
if (doc.hasComponents()) {
for (const [key, m] of Object.entries(doc.components().messages())) {
if (m.name() === undefined) {
m.json()[String(xParserMessageName)] = key;

if (traversIds) {
assignUidToComponentSchemasRecursive(m.payload(), key);
}
}
}
}
Expand Down Expand Up @@ -43,22 +49,95 @@ function assignUidToParameterSchemas(doc) {
assignIdToParameters(channel.parameters());
});
}

/**
* Assign uid to component schemas.
*
* @private
* @param {AsyncAPIDocument} doc
* @param {boolean} traversIds Walk recursive through schema and try to find reasonamble uid where no uids was given.
*/
function assignUidToComponentSchemas(doc) {
function assignUidToComponentSchemas(doc, traversIds = false) {
if (doc.hasComponents()) {
for (const [key, s] of Object.entries(doc.components().schemas())) {
s.json()[String(xParserSchemaId)] = key;
for (const [key, schema] of Object.entries(doc.components().schemas())) {
if (traversIds) {
assignUidToComponentSchemasRecursive(schema, key);
} else {
schema.json()[String(xParserSchemaId)] = key;
}
}
}
}

/**
function assignUidToComponentSchemasRecursive(schema, key) {
traverseSchema(
schema,
key,
{
parentId: '',
level: 0,
propOrIndexModifier: (propertyName, schema, options) => {
if (typeof propertyName === 'number') {
// It is not save to generate ids with arrays containing multiple types
return null;
}

return options.parentId + upperFirst(propertyName);
},
callback: (schema, propOrIndex, callbackType, options) => {
if (callbackType === SchemaIteratorCallbackType.END_SCHEMA) {
return;
}

if (propOrIndex === null &&
options.parentType === 'array' // Special edge case, here we need to inheritance the id to the childs.
) {
propOrIndex = options.parentId;
}

if (propOrIndex === null) {
return false;
}

if (!(schema.type() === 'object' ||
schema.type() === 'array' ||
(schema.type() === 'string' && schema.enum()) ||
!schema.type() // oneOf, anyOf, allOf dont have a type.
)) {
// Simple types dont need generated ids.
return false;
}

options.parentId = propOrIndex;
options.parentType = schema.type();
options.level++;

if (!schema.$id()) {
if (schema.json()[String(xParserSchemaIdLevel)] && schema.json()[String(xParserSchemaIdLevel)] < options.level) {
// After deref we get a tree, always prefer names of parent root schema definitions.
return false;
}

if (schema.type() === 'array') {
// You want the shorter on the child, this is used by the code generator to define the class name.
schema.json()[String(xParserSchemaId)] = propOrIndex + 'Array';
} else {
schema.json()[String(xParserSchemaId)] = propOrIndex;
}
schema.json()[String(xParserSchemaIdLevel)] = options.level;
}
},
schemaTypesToIterate: [
'objects',
'arrays',
'allOf' // Will inheritate the parent id, because it is a joined object.
],
seenSchemas: new Set()
}
);
}

/**
* Assign uid to component parameters schemas
*
* @private
Expand All @@ -69,16 +148,16 @@ function assignUidToComponentParameterSchemas(doc) {
assignIdToParameters(doc.components().parameters());
}
}

/**
* Assign anonymous names to nameless messages.
*
* @private
* @param {AsyncAPIDocument} doc
* @param {AsyncAPIDocument} doc
*/
function assignNameToAnonymousMessages(doc) {
let anonymousMessageCounter = 0;

if (doc.hasChannels()) {
doc.channelNames().forEach(channelName => {
const channel = doc.channel(channelName);
Expand All @@ -87,7 +166,7 @@ function assignNameToAnonymousMessages(doc) {
});
}
}

/**
* Add anonymous name to key if no name provided.
*
Expand All @@ -101,7 +180,7 @@ function addNameToKey(messages, number) {
}
});
}

/**
* Gives schemas id to all anonymous schemas.
*
Expand Down
4 changes: 3 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const xParserSpecParsed = 'x-parser-spec-parsed';
const xParserSpecStringified = 'x-parser-spec-stringified';
const xParserMessageName = 'x-parser-message-name';
const xParserSchemaId = 'x-parser-schema-id';
const xParserSchemaIdLevel = 'x-parser-schema-id-level';
const xParserCircle = 'x-parser-circular';
const xParserCircleProps = 'x-parser-circular-props';

Expand All @@ -10,6 +11,7 @@ module.exports = {
xParserSpecStringified,
xParserMessageName,
xParserSchemaId,
xParserSchemaIdLevel,
xParserCircle,
xParserCircleProps
};
};
39 changes: 24 additions & 15 deletions lib/iterators.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function traverseSchema(schema, propOrIndex, options) { // NOSONAR
if (!schemaTypesToIterate.includes(SchemaTypesToIterate.arrays) && types.includes('array')) return;

// check callback `NEW_SCHEMA` case
if (callback(schema, propOrIndex, SchemaIteratorCallbackType.NEW_SCHEMA) === false) return;
if (callback(schema, propOrIndex, SchemaIteratorCallbackType.NEW_SCHEMA, options) === false) return;

if (schemaTypesToIterate.includes(SchemaTypesToIterate.objects) && types.includes('object')) {
recursiveSchemaObject(schema, options);
Expand All @@ -106,46 +106,46 @@ function traverseSchema(schema, propOrIndex, options) { // NOSONAR
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.oneOfs)) {
(schema.oneOf() || []).forEach((combineSchema, idx) => {
traverseSchema(combineSchema, idx, options);
traverseSchema(combineSchema, idx, {...options});
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.anyOfs)) {
(schema.anyOf() || []).forEach((combineSchema, idx) => {
traverseSchema(combineSchema, idx, options);
traverseSchema(combineSchema, idx, {...options});
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.allOfs)) {
(schema.allOf() || []).forEach((combineSchema, idx) => {
traverseSchema(combineSchema, idx, options);
traverseSchema(combineSchema, idx, {...options});
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.nots) && schema.not()) {
traverseSchema(schema.not(), null, options);
traverseSchema(schema.not(), null, {...options});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.ifs) && schema.if()) {
traverseSchema(schema.if(), null, options);
traverseSchema(schema.if(), null, {...options});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.thenes) && schema.then()) {
traverseSchema(schema.then(), null, options);
traverseSchema(schema.then(), null, {...options});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.elses) && schema.else()) {
traverseSchema(schema.else(), null, options);
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 (dep && !Array.isArray(dep)) {
traverseSchema(dep, depName, {...options});
}
});
}
if (schemaTypesToIterate.includes(SchemaTypesToIterate.definitions)) {
Object.entries(schema.definitions() || {}).forEach(([defName, def]) => {
traverseSchema(def, defName, options);
traverseSchema(def, defName, {...options});
});
}

callback(schema, propOrIndex, SchemaIteratorCallbackType.END_SCHEMA);
callback(schema, propOrIndex, SchemaIteratorCallbackType.END_SCHEMA, options);
seenSchemas.delete(jsonSchema);
}
/* eslint-enable sonarjs/cognitive-complexity */
Expand All @@ -156,12 +156,16 @@ function traverseSchema(schema, propOrIndex, options) { // NOSONAR
* @private
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaIteratorCallbackType} [options.propOrIndexModifier] callback used modify, propertyName.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [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);
if (options.propOrIndexModifier) {
propertyName = options.propOrIndexModifier(propertyName, schema, options);
}
traverseSchema(property, propertyName, {...options});
});

const additionalProperties = schema.additionalProperties();
Expand All @@ -186,6 +190,7 @@ function recursiveSchemaObject(schema, options) {
* @private
* @param {Object} options
* @param {SchemaIteratorCallbackType} [options.callback] callback used when crawling a schema.
* @param {SchemaIteratorCallbackType} [options.propOrIndexModifier] callback used modify, propertyName.
* @param {SchemaTypesToIterate[]} [options.schemaTypesToIterate] list of schema types to crawl.
* @param {Set<Object>} [options.seenSchemas] Set which holds all defined schemas in the tree - it is mainly used to check circular references
*/
Expand All @@ -194,7 +199,10 @@ function recursiveSchemaArray(schema, options) {
if (items) {
if (Array.isArray(items)) {
items.forEach((item, idx) => {
traverseSchema(item, idx, options);
if (options.propOrIndexModifier) {
idx = options.propOrIndexModifier(idx, schema, options);
}
traverseSchema(item, idx, {...options});
});
} else {
traverseSchema(items, null, options);
Expand Down Expand Up @@ -322,4 +330,5 @@ module.exports = {
SchemaIteratorCallbackType,
SchemaTypesToIterate,
traverseAsyncApiDocument,
};
traverseSchema
};
Loading

0 comments on commit af9b7cd

Please sign in to comment.