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 Jul 8, 2022
1 parent 0348674 commit c804fd5
Show file tree
Hide file tree
Showing 13 changed files with 1,236 additions and 57 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 `genererateIdInSchema` option is passed to the parser. It indicates the traversal level of a schema. This is used to prepare the 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
166 changes: 150 additions & 16 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,42 +49,170 @@ 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;
}
}
}
}

/**
* Assign uid to component parameters schemas
* Assign uid to component schemas. By walking recursive through schema.
*
* @private
* @param {AsyncAPIDocument} doc
* @param {Schema} schema which is going to be processed.
* @param {String} key The name of the schema to be processed.
*/
function assignUidToComponentSchemasRecursive(schema, key) {
traverseSchema(
schema,
key,
{
parentId: '',
level: 0,
propOrIndexModifier: (propertyName, subSchema, options) => {
if (typeof propertyName === 'number') {
// It is not save to generate ids with arrays containing multiple types
// This is the case when be called from iterators.js:recursiveSchemaArray

// This example is working because `Car` and `Airplane` are root level schemas and gots its own ids:
// properties:
// vehicle:
// type: array
// x-parser-schema-id: mySchemaVehicleArray
// items:
// - $ref: "#/components/schemas/Car"
// - $ref: "#/components/schemas/Airplane"

// but if the items are anonymous
// properties:
// vehicle:
// type: array
// x-parser-schema-id: mySchemaVehicleArray <-- collision
// items:
// - type: object
// x-parser-schema-id: mySchemaVehicle
// properties:
// maxSpeed:
// type: string
// - type: object
// x-parser-schema-id: mySchemaVehicle <-- collision
// properties:
// maxAltitude:
// type: string
//
// would not work so we can not give code generator firendly id when `items`is an array.
return null;
}

return options.parentId + upperFirst(propertyName);
},
callback: (subSchema, 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 (!(subSchema.type() === 'object' ||
subSchema.type() === 'array' ||
(subSchema.type() === 'string' && subSchema.enum()) ||
!subSchema.type() // oneOf, anyOf, allOf dont have a type.
)) {
// Simple types dont need generated ids.
return false;
}

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

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

// The usage of $ref will lead to having the same schema multiple times in the tree caused by deref.
// By walking through the tree, each level deper in the tree gets a higher xParserSchemaIdLevel
// So if we find a genereated id with a higher xParserSchemaIdLevel the the current, this id was generated because the current schema was used via $ref as subelement in another schema.
// and we prefer the generated id that is clother to a rood node in tree.
return false;
}

if (subSchema.type() === 'array') {
// You want the shorter on the child, this is used by the code generator to define the class name.
// Example:
// properties:
// pet:
// x-parser-schema-id: mySchemaPetArray <-- this is just a field of class "MySchema", so no need to get a great name
// x-parser-schema-id-level: 1
// type: array
// items:
// x-parser-schema-id: mySchemaPet <-- This will be generate as a class "MySchemaPet"
// x-parser-schema-id-level: 2
// type: object
// properties:
// name:
// type: string
// kind:
// type: string
subSchema.json()[String(xParserSchemaId)] = propOrIndex + 'Array';
} else {
subSchema.json()[String(xParserSchemaId)] = propOrIndex;
}
subSchema.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
* @param {AsyncAPIDocument} doc
*/
function assignUidToComponentParameterSchemas(doc) {
if (doc.hasComponents()) {
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 +221,7 @@ function assignNameToAnonymousMessages(doc) {
});
}
}

/**
* Add anonymous name to key if no name provided.
*
Expand All @@ -101,7 +235,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
};
};
Loading

0 comments on commit c804fd5

Please sign in to comment.