diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 00000000..4cc6c5a3 --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1 @@ +sonar.exclusions=tools/**/* \ No newline at end of file diff --git a/README.md b/README.md index 0eb6ef1e..da079908 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,8 @@ This is the current project structure explained: - [./examples](./examples) - contain most individual definition examples that will automatically be bundled together to provide example for each definition in the schemas in [./schemas](./schemas). - [./tools/bundler](./tools/bundler) - is the tool that bundles all the individual schemas together. - [./schemas](./schemas) - contain all automatically bundled and complete schemas for each AsyncAPI version. These schemas should **NOT** be manually changed as they are automatically generated. Any changes should be done in [./definitions](./definitions). +- [./extensions](./extensions) - contains all the schemas of the extensions that will automatically be bundled to provide informations about extensions. + ## Schema Bundling @@ -210,7 +212,16 @@ Whenever you make changes in AsyncAPI JSON Schema, you should always manually ve ```yaml # yaml-language-server: $schema=YOUR-PROJECTS-DIRECTORY/spec-json-schemas/schemas/2.6.0-without-$id.json ``` + +## Extensions +Extensions are a way to [extend AsyncAPI specification](https://www.asyncapi.com/docs/concepts/asyncapi-document/extending-specification) with fields that are not yet defined inside the specification. To add JSON schema of the extension in this repository, you need to first make sure it is added to the [extension-catalog](https://github.com/asyncapi/extensions-catalog) repository. +### How to add schema of the extension +1. All the extensions must be present in [./extensions](./extensions) folder. +2. A proper folder structure must be followed to add the extensions. +3. A new folder just as [x extension](./extensions/x) must be added with proper `versioning` and `schema file`. +4. All the schemas must be added in a file named `schema.json` just as one is defined for [x extension](./extensions/x/0.1.0/schema.json). +5. Extension schema should not be referenced directly in the definition of the object it extends. For example if you add an extension for `info`, your extension's schema should not be referenced from `info.json` but [infoExtensions.json](./definitions/3.0.0/infoExtensions.json). If the object that you extend doesn't have a corresponding `*Extensions.json` file, you need to create one. diff --git a/definitions/3.0.0/info.json b/definitions/3.0.0/info.json index 6c9b70fa..034eb5ab 100644 --- a/definitions/3.0.0/info.json +++ b/definitions/3.0.0/info.json @@ -1,66 +1,70 @@ { - "type": "object", "description": "The object provides metadata about the API. The metadata can be used by the clients if needed.", - "required": [ - "version", - "title" - ], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d\\.\\x2d_]+$": { - "$ref": "http://asyncapi.com/definitions/3.0.0/specificationExtension.json" - } - }, - "properties": { - "title": { - "type": "string", - "description": "A unique and precise title of the API." - }, - "version": { - "type": "string", - "description": "A semantic version number of the API." - }, - "description": { - "type": "string", - "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." - }, - "termsOfService": { - "type": "string", - "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", - "format": "uri" - }, - "contact": { - "$ref": "http://asyncapi.com/definitions/3.0.0/contact.json" - }, - "license": { - "$ref": "http://asyncapi.com/definitions/3.0.0/license.json" - }, - "tags": { - "type": "array", - "description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.", - "items": { - "oneOf": [ - { - "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" - }, - { - "$ref": "http://asyncapi.com/definitions/3.0.0/tag.json" - } - ] + "allOf": [ + { + "type": "object", + "required": ["version", "title"], + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\x2d_]+$": { + "$ref": "http://asyncapi.com/definitions/3.0.0/specificationExtension.json" + } }, - "uniqueItems": true - }, - "externalDocs": { - "oneOf": [ - { - "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." }, - { - "$ref": "http://asyncapi.com/definitions/3.0.0/externalDocs.json" + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." + }, + "termsOfService": { + "type": "string", + "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", + "format": "uri" + }, + "contact": { + "$ref": "http://asyncapi.com/definitions/3.0.0/contact.json" + }, + "license": { + "$ref": "http://asyncapi.com/definitions/3.0.0/license.json" + }, + "tags": { + "type": "array", + "description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.", + "items": { + "oneOf": [ + { + "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" + }, + { + "$ref": "http://asyncapi.com/definitions/3.0.0/tag.json" + } + ] + }, + "uniqueItems": true + }, + "externalDocs": { + "oneOf": [ + { + "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" + }, + { + "$ref": "http://asyncapi.com/definitions/3.0.0/externalDocs.json" + } + ] } - ] + } + }, + { + "$ref": "http://asyncapi.com/definitions/3.0.0/infoExtensions.json" } - }, + ], "example": { "$ref": "http://asyncapi.com/examples/3.0.0/info.json" }, diff --git a/definitions/3.0.0/infoExtensions.json b/definitions/3.0.0/infoExtensions.json new file mode 100644 index 00000000..47e71402 --- /dev/null +++ b/definitions/3.0.0/infoExtensions.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "description": "The object that lists all the extensions of Info", + "properties": { + "x-x":{ + "$ref": "http://asyncapi.com/extensions/x/0.1.0/schema.json" + } + }, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://asyncapi.com/definitions/3.0.0/infoExtensions.json" +} diff --git a/extensions/x/0.1.0/schema.json b/extensions/x/0.1.0/schema.json new file mode 100644 index 00000000..6bfdc146 --- /dev/null +++ b/extensions/x/0.1.0/schema.json @@ -0,0 +1,10 @@ +{ + "type": "string", + "description": "This extension allows you to provide the Twitter username of the account representing the team/company of the API.", + "example": [ + "sambhavgupta75", + "AsyncAPISpec" + ], + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://asyncapi.com/extensions/x/0.1.0/schema.json" +} diff --git a/schemas/3.0.0-without-$id.json b/schemas/3.0.0-without-$id.json index 297899e2..2be60cbc 100644 --- a/schemas/3.0.0-without-$id.json +++ b/schemas/3.0.0-without-$id.json @@ -50,68 +50,75 @@ "additionalItems": true }, "info": { - "type": "object", "description": "The object provides metadata about the API. The metadata can be used by the clients if needed.", - "required": [ - "version", - "title" - ], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d\\.\\x2d_]+$": { - "$ref": "#/definitions/specificationExtension" - } - }, - "properties": { - "title": { - "type": "string", - "description": "A unique and precise title of the API." - }, - "version": { - "type": "string", - "description": "A semantic version number of the API." - }, - "description": { - "type": "string", - "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." - }, - "termsOfService": { - "type": "string", - "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", - "format": "uri" - }, - "contact": { - "$ref": "#/definitions/contact" - }, - "license": { - "$ref": "#/definitions/license" - }, - "tags": { - "type": "array", - "description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.", - "items": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" - }, - { - "$ref": "#/definitions/tag" - } - ] + "allOf": [ + { + "type": "object", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\x2d_]+$": { + "$ref": "#/definitions/specificationExtension" + } }, - "uniqueItems": true - }, - "externalDocs": { - "oneOf": [ - { - "$ref": "#/definitions/Reference" + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." }, - { - "$ref": "#/definitions/externalDocs" + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." + }, + "termsOfService": { + "type": "string", + "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", + "format": "uri" + }, + "contact": { + "$ref": "#/definitions/contact" + }, + "license": { + "$ref": "#/definitions/license" + }, + "tags": { + "type": "array", + "description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/tag" + } + ] + }, + "uniqueItems": true + }, + "externalDocs": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/externalDocs" + } + ] } - ] + } + }, + { + "$ref": "#/definitions/infoExtensions" } - }, + ], "examples": [ { "title": "AsyncAPI Sample App", @@ -292,6 +299,23 @@ } ] }, + "infoExtensions": { + "type": "object", + "description": "The object that lists all the extensions of Info", + "properties": { + "x-x": { + "$ref": "#/definitions/extensions-x-0.1.0-schema" + } + } + }, + "extensions-x-0.1.0-schema": { + "type": "string", + "description": "This extension allows you to provide the Twitter username of the account representing the team/company of the API.", + "example": [ + "sambhavgupta75", + "AsyncAPISpec" + ] + }, "servers": { "description": "An object representing multiple servers.", "type": "object", diff --git a/schemas/3.0.0.json b/schemas/3.0.0.json index 25bbc016..b100bf28 100644 --- a/schemas/3.0.0.json +++ b/schemas/3.0.0.json @@ -53,68 +53,75 @@ }, "http://asyncapi.com/definitions/3.0.0/info.json": { "$id": "http://asyncapi.com/definitions/3.0.0/info.json", - "type": "object", "description": "The object provides metadata about the API. The metadata can be used by the clients if needed.", - "required": [ - "version", - "title" - ], - "additionalProperties": false, - "patternProperties": { - "^x-[\\w\\d\\.\\x2d_]+$": { - "$ref": "http://asyncapi.com/definitions/3.0.0/specificationExtension.json" - } - }, - "properties": { - "title": { - "type": "string", - "description": "A unique and precise title of the API." - }, - "version": { - "type": "string", - "description": "A semantic version number of the API." - }, - "description": { - "type": "string", - "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." - }, - "termsOfService": { - "type": "string", - "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", - "format": "uri" - }, - "contact": { - "$ref": "http://asyncapi.com/definitions/3.0.0/contact.json" - }, - "license": { - "$ref": "http://asyncapi.com/definitions/3.0.0/license.json" - }, - "tags": { - "type": "array", - "description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.", - "items": { - "oneOf": [ - { - "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" - }, - { - "$ref": "http://asyncapi.com/definitions/3.0.0/tag.json" - } - ] + "allOf": [ + { + "type": "object", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\x2d_]+$": { + "$ref": "http://asyncapi.com/definitions/3.0.0/specificationExtension.json" + } }, - "uniqueItems": true - }, - "externalDocs": { - "oneOf": [ - { - "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" + "properties": { + "title": { + "type": "string", + "description": "A unique and precise title of the API." }, - { - "$ref": "http://asyncapi.com/definitions/3.0.0/externalDocs.json" + "version": { + "type": "string", + "description": "A semantic version number of the API." + }, + "description": { + "type": "string", + "description": "A longer description of the API. Should be different from the title. CommonMark is allowed." + }, + "termsOfService": { + "type": "string", + "description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.", + "format": "uri" + }, + "contact": { + "$ref": "http://asyncapi.com/definitions/3.0.0/contact.json" + }, + "license": { + "$ref": "http://asyncapi.com/definitions/3.0.0/license.json" + }, + "tags": { + "type": "array", + "description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.", + "items": { + "oneOf": [ + { + "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" + }, + { + "$ref": "http://asyncapi.com/definitions/3.0.0/tag.json" + } + ] + }, + "uniqueItems": true + }, + "externalDocs": { + "oneOf": [ + { + "$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json" + }, + { + "$ref": "http://asyncapi.com/definitions/3.0.0/externalDocs.json" + } + ] } - ] + } + }, + { + "$ref": "http://asyncapi.com/definitions/3.0.0/infoExtensions.json" } - }, + ], "examples": [ { "title": "AsyncAPI Sample App", @@ -301,6 +308,25 @@ } ] }, + "http://asyncapi.com/definitions/3.0.0/infoExtensions.json": { + "$id": "http://asyncapi.com/definitions/3.0.0/infoExtensions.json", + "type": "object", + "description": "The object that lists all the extensions of Info", + "properties": { + "x-x": { + "$ref": "http://asyncapi.com/extensions/x/0.1.0/schema.json" + } + } + }, + "http://asyncapi.com/extensions/x/0.1.0/schema.json": { + "$id": "http://asyncapi.com/extensions/x/0.1.0/schema.json", + "type": "string", + "description": "This extension allows you to provide the Twitter username of the account representing the team/company of the API.", + "example": [ + "sambhavgupta75", + "AsyncAPISpec" + ] + }, "http://asyncapi.com/definitions/3.0.0/servers.json": { "$id": "http://asyncapi.com/definitions/3.0.0/servers.json", "description": "An object representing multiple servers.", diff --git a/test/fixtures/asyncapi.yml b/test/fixtures/asyncapi.yml index cbf0dc4d..3c72f2e6 100644 --- a/test/fixtures/asyncapi.yml +++ b/test/fixtures/asyncapi.yml @@ -1,42 +1,201 @@ -# yaml-language-server: $schema=YOUR-PROJECTS-DIRECTORY/spec-json-schemas/schemas/2.6.0-without-$id.json -asyncapi: 2.6.0 - +# yaml-language-server: $schema=YOUR-PROJECTS-DIRECTORY/spec-json-schemas/schemas/3.0.0-without-$id.json +asyncapi: 3.0.0 info: - - title: test.mosquitto.org - version: This service is in charge of processing all the events related to comments. - + title: Streetlights Kafka API + version: 1.0.0 + dupa: test + description: "The Smartylighting Streetlights API allows you to remotely manage the city lights.\n\n### Check out its awesome features:\n\n* Turn a specific streetlight on/off \U0001F303\n* Dim a specific streetlight \U0001F60E\n* Receive real-time information about environmental lighting conditions \U0001F4C8\n" + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +defaultContentType: application/json servers: - dev: - url: test.mosquitto.org - protocol: mqtt - + scram-connections: + host: 'test.mykafkacluster.org:18092' + protocol: kafka-secure + description: Test broker secured with scramSha256 + security: + - $ref: '#/components/securitySchemes/saslScram' + tags: + - name: 'env:test-scram' + description: >- + This environment is meant for running internal tests through + scramSha256 + - name: 'kind:remote' + description: This server is a remote server. Not exposed by the application + - name: 'visibility:private' + description: This resource is private and only available to certain users + mtls-connections: + host: 'test.mykafkacluster.org:28092' + protocol: kafka-secure + description: Test broker secured with X509 + security: + - $ref: '#/components/securitySchemes/certs' + tags: + - name: 'env:test-mtls' + description: This environment is meant for running internal tests through mtls + - name: 'kind:remote' + description: This server is a remote server. Not exposed by the application + - name: 'visibility:private' + description: This resource is private and only available to certain users channels: - comment/liked: - - description: Updates the likes count in the database when new like is noticed. - publish: - operationId: commentLiked - message: - description: Message that is being sent when a comment has been liked by someone. - payload: - $ref: '#/components/schemas/commentId' - - comment/unliked: - description: Updates the likes count in the database when comment is unliked. - publish: - operationId: commentUnliked - message: - description: Message that is being sent when a comment has been unliked by someone. - messageId: ddd - payload: - $ref: '#/components/schemas/commentId' - + lightingMeasured: + address: 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured' + messages: + lightMeasured: + $ref: '#/components/messages/lightMeasured' + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOn: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.on' + messages: + turnOn: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightTurnOff: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.off' + messages: + turnOff: + $ref: '#/components/messages/turnOnOff' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + lightsDim: + address: 'smartylighting.streetlights.1.0.action.{streetlightId}.dim' + messages: + dimLight: + $ref: '#/components/messages/dimLight' + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' +operations: + receiveLightMeasurement: + action: receive + channel: + $ref: '#/channels/lightingMeasured' + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightingMeasured/messages/lightMeasured' + turnOn: + action: send + channel: + $ref: '#/channels/lightTurnOn' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOn/messages/turnOn' + turnOff: + action: send + channel: + $ref: '#/channels/lightTurnOff' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightTurnOff/messages/turnOff' + dimLight: + action: send + channel: + $ref: '#/channels/lightsDim' + traits: + - $ref: '#/components/operationTraits/kafka' + messages: + - $ref: '#/channels/lightsDim/messages/dimLight' components: + messages: + lightMeasured: + name: lightMeasured + title: Light measured + summary: >- + Inform about environmental lighting conditions of a particular + streetlight. + contentType: application/json + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/lightMeasuredPayload' + turnOnOff: + name: turnOnOff + title: Turn on/off + summary: Command a particular streetlight to turn the lights on or off. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/turnOnOffPayload' + dimLight: + name: dimLight + title: Dim light + summary: Command a particular streetlight to dim the lights. + traits: + - $ref: '#/components/messageTraits/commonHeaders' + payload: + $ref: '#/components/schemas/dimLightPayload' schemas: - commentId: + lightMeasuredPayload: type: object - additionalProperties: false properties: - commentId: + lumens: + type: integer + minimum: 0 + description: Light intensity measured in lumens. + sentAt: + $ref: '#/components/schemas/sentAt' + turnOnOffPayload: + type: object + properties: + command: type: string + enum: + - 'on' + - 'off' + description: Whether to turn on or off the light. + sentAt: + $ref: '#/components/schemas/sentAt' + dimLightPayload: + type: object + properties: + percentage: + type: integer + description: Percentage to which the light should be dimmed to. + minimum: 0 + maximum: 100 + sentAt: + $ref: '#/components/schemas/sentAt' + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. + securitySchemes: + saslScram: + type: scramSha256 + description: Provide your username and password for SASL/SCRAM authentication + certs: + type: X509 + description: Download the certificate files from service provider + parameters: + streetlightId: + description: The ID of the streetlight. + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + operationTraits: + kafka: + bindings: + kafka: + clientId: + type: string + enum: + - my-app-id \ No newline at end of file diff --git a/tools/bundler/index.js b/tools/bundler/index.js index 3596c8b3..44bf731a 100644 --- a/tools/bundler/index.js +++ b/tools/bundler/index.js @@ -4,10 +4,12 @@ const traverse = require('json-schema-traverse'); const definitionsDirectory = path.resolve(__dirname, '../../definitions'); const commonSchemasDirectory = path.resolve(__dirname, '../../common'); const bindingsDirectory = path.resolve(__dirname, '../../bindings'); +const extensionsDirectory = path.resolve(__dirname, '../../extensions'); const outputDirectory = path.resolve(__dirname, '../../schemas'); const JSON_SCHEMA_PROP_NAME = 'json-schema-draft-07-schema'; console.log(`Looking for separate definitions in the following directory: ${definitionsDirectory}`); console.log(`Looking for binding version schemas in the following directory: ${bindingsDirectory}`); +console.log(`Looking for extension version schemas in the following directory: ${extensionsDirectory}`); console.log(`Using the following output directory: ${outputDirectory}`); // definitionsRegex is used to transform the name of a definition into a valid one to be used in the -without-$id.json files. @@ -16,58 +18,8 @@ const definitionsRegex = /http:\/\/asyncapi\.com\/definitions\/[^/]*\/(.+)\.json // definitionsRegex is used to transform the name of a binding into a valid one to be used in the -without-$id.json files. const bindingsRegex = /http:\/\/asyncapi\.com\/(bindings\/[^/]+)\/([^/]+)\/(.+)\.json(.*)/i; -/** - * Function to load all the core AsyncAPI spec definition (except the root asyncapi schema, as that will be loaded later) into the bundler. - */ -async function loadDefinitions(bundler, versionDir) { - const definitions = await fs.promises.readdir(versionDir); - const definitionFiles = definitions.filter((value) => {return !value.includes('asyncapi');}).map((file) => fs.readFileSync(path.resolve(versionDir, file))); - const definitionJson = definitionFiles.map((file) => JSON.parse(file)); - - for (const jsonFile of definitionJson) { - if (jsonFile.example) { - // Replaced the example property with the referenced example property - const examples = await loadRefProperties(jsonFile.example); - // Replacing example property with examples is because using example - // to pass an array of example properties is not valid in JSON Schema. - // So replacing it when bundling is the goto solution. - jsonFile.examples = examples; - delete jsonFile.example; - bundler.add(jsonFile); - } else { - bundler.add(jsonFile); - } - } -} - -/** - * Function to load all the binding version schemas into the bundler - */ -async function loadBindings(bundler) { - const bindingDirectories = await fs.promises.readdir(bindingsDirectory); - for (const bindingDirectory of bindingDirectories) { - const bindingVersionDirectories = await fs.promises.readdir(path.resolve(bindingsDirectory, bindingDirectory)); - const bindingVersionDirectoriesFiltered = bindingVersionDirectories.filter((file) => fs.lstatSync(path.resolve(bindingsDirectory, bindingDirectory, file)).isDirectory()); - for (const bindingVersionDirectory of bindingVersionDirectoriesFiltered) { - const bindingFiles = await fs.promises.readdir(path.resolve(bindingsDirectory, bindingDirectory, bindingVersionDirectory)); - const bindingFilesFiltered = bindingFiles.filter((bindingFile) => path.extname(bindingFile) === '.json').map((bindingFile) => path.resolve(bindingsDirectory, bindingDirectory, bindingVersionDirectory, bindingFile)); - for (const bindingFile of bindingFilesFiltered) { - const bindingFileContent = require(bindingFile); - bundler.add(bindingFileContent); - } - } - } -} - -async function loadCommonSchemas(bundler) { - // Add common schemas to all versions - const commonSchemas = await fs.promises.readdir(commonSchemasDirectory); - const commonSchemaFiles = commonSchemas.map((file) => path.resolve(commonSchemasDirectory, file)); - for (const commonSchemaFile of commonSchemaFiles) { - const commonSchemaFileContent = require(commonSchemaFile); - bundler.add(commonSchemaFileContent); - } -} +// definitionsRegex is used to transform the name of a binding into a valid one to be used in the -without-$id.json files. +const extensionsRegex = /http:\/\/asyncapi\.com\/(extensions\/[^/]+)\/([^/]+)\/(.+)\.json(.*)/i; /** * When run, go through all versions that have split definitions and bundles them together. @@ -88,7 +40,8 @@ async function loadCommonSchemas(bundler) { const versionDir = path.resolve(definitionsDirectory, version); await loadDefinitions(Bundler, versionDir); await loadCommonSchemas(Bundler); - await loadBindings(Bundler); + await loadSchemas(Bundler, 'bindings'); + await loadSchemas(Bundler, 'extensions'); const filePathToBundle = `file://${versionDir}/asyncapi.json`; const fileToBundle = await Bundler.get(filePathToBundle); @@ -97,7 +50,12 @@ async function loadCommonSchemas(bundler) { * bundling schemas into one file with $id */ const bundledSchemaWithId = await Bundler.bundle(fileToBundle); - bundledSchemaWithId.description = `!!Auto generated!! \n Do not manually edit. ${bundledSchemaWithId.description !== undefined && bundledSchemaWithId.description !== null ? bundledSchemaWithId.description : ''}`; + bundledSchemaWithId.description = `!!Auto generated!! \n Do not manually edit. ${ + bundledSchemaWithId.description !== undefined && + bundledSchemaWithId.description !== null + ? bundledSchemaWithId.description + : '' + }`; console.log(`Writing the bundled file WITH $ids to: ${outputFileWithId}`); await fs.promises.writeFile(outputFileWithId, JSON.stringify(bundledSchemaWithId, null, 4)); @@ -115,9 +73,92 @@ async function loadCommonSchemas(bundler) { console.log('done'); })(); +/** + * Function to load all the core AsyncAPI spec definition (except the root asyncapi schema, as that will be loaded later) into the bundler. + */ +async function loadDefinitions(bundler, versionDir) { + const definitions = await fs.promises.readdir(versionDir); + const definitionFiles = definitions + .filter((value) => { + return !value.includes('asyncapi'); + }) + .map((file) => fs.readFileSync(path.resolve(versionDir, file))); + const definitionJson = definitionFiles.map((file) => JSON.parse(file)); + + for (const jsonFile of definitionJson) { + if (jsonFile.example) { + // Replaced the example property with the referenced example property + const examples = await loadRefProperties(jsonFile.example); + // Replacing example property with examples is because using example + // to pass an array of example properties is not valid in JSON Schema. + // So replacing it when bundling is the goto solution. + jsonFile.examples = examples; + delete jsonFile.example; + bundler.add(jsonFile); + } else { + bundler.add(jsonFile); + } + } +} + +/** + * Function to load all schemas into bundler, by "type" you specify if these are "bindings" or "extensions" + */ +async function loadSchemas(bundler, type) { + let directory; + + switch (type) { + case 'bindings': + directory = bindingsDirectory; + break; + case 'extensions': + directory = extensionsDirectory; + break; + default: + console.error( + 'Invalid input. I\'m not going to assume if you want bindings or extensions - these are different beasts.' + ); + } + + const directories = await fs.promises.readdir(directory); + for (const nestedDir of directories) { + const versionDirectories = await fs.promises.readdir( + path.resolve(directory, nestedDir) + ); + const versionDirectoriesFiltered = versionDirectories.filter((file) => + fs.lstatSync(path.resolve(directory, nestedDir, file)).isDirectory() + ); + for (const versionDir of versionDirectoriesFiltered) { + const files = await fs.promises.readdir( + path.resolve(directory, nestedDir, versionDir) + ); + const filesFiltered = files + .filter((file) => path.extname(file) === '.json') + .map((file) => path.resolve(directory, nestedDir, versionDir, file)); + for (const filteredFile of filesFiltered) { + const fileContent = require(filteredFile); + bundler.add(fileContent); + } + } + } +} + +async function loadCommonSchemas(bundler) { + // Add common schemas to all versions + const commonSchemas = await fs.promises.readdir(commonSchemasDirectory); + const commonSchemaFiles = commonSchemas.map((file) => + path.resolve(commonSchemasDirectory, file) + ); + for (const commonSchemaFile of commonSchemaFiles) { + const commonSchemaFileContent = require(commonSchemaFile); + bundler.add(commonSchemaFileContent); + } +} + /** * Extract file data from reference file path */ + async function loadRefProperties(filePath) { const schemaPath = filePath.$ref; // first we need to turn the path to an absolute file path instead of a generic url @@ -139,7 +180,7 @@ function modifyRefsAndDefinitions(bundledSchema) { //first we need to improve names of the definitions from URL to their names for (const def of Object.keys(bundledSchema.definitions)) { const newDefName = getDefinitionName(def); - + //creating copy of definition under new name so later definition stored under URL name can be removed bundledSchema.definitions[newDefName] = bundledSchema.definitions[def]; delete bundledSchema.definitions[def]; @@ -183,7 +224,12 @@ function getDefinitionName(def) { return `${result[1].replace('/', '-')}-${result[2]}-${result[3]}/${result[4].replace('#/', '')}`; } } - + if (def.startsWith('http://asyncapi.com/extensions')) { + const result = extensionsRegex.exec(def); + if (result) + return `${result[1].replace('/', '-')}-${result[2]}-${result[3]}`; + } + return path.basename(def, '.json'); } @@ -197,7 +243,6 @@ function replaceRef(schema) { //traversing should take place only in case of schemas with refs if (schema.$ref === undefined) return; - // updating refs that are related to remote URL refs that need to be update and point to inlined versions if (!schema.$ref.startsWith('#')) schema.$ref = `#/definitions/${getDefinitionName(schema.$ref)}`; } @@ -227,6 +272,7 @@ function updateOpenApi(schema) { const openApiPropName = 'openapiSchema_3_0'; schema.$ref = schema.$ref.replace( + /* eslint-disable sonarjs/no-duplicate-string */ '#/definitions/', `#/definitions/${openApiPropName}/definitions/` ); @@ -252,4 +298,4 @@ function updateJsonSchema(schema) { if (schema.$ref === '#') { schema.$ref = `#/definitions/${JSON_SCHEMA_PROP_NAME}`; } -} \ No newline at end of file +}