Skip to content

Commit c7c623e

Browse files
feat: add extensions from extensions catalog to schema (#443)
Co-authored-by: derberg <[email protected]>
1 parent 476a16c commit c7c623e

File tree

6 files changed

+136
-70
lines changed

6 files changed

+136
-70
lines changed

.sonarcloud.properties

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sonar.exclusions=tools/**/*

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ This is the current project structure explained:
134134
- [./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).
135135
- [./tools/bundler](./tools/bundler) - is the tool that bundles all the individual schemas together.
136136
- [./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).
137+
- [./extensions](./extensions) - contains all the schemas of the extensions that will automatically be bundled to provide informations about extensions.
138+
137139

138140
## Schema Bundling
139141

@@ -210,7 +212,18 @@ Whenever you make changes in AsyncAPI JSON Schema, you should always manually ve
210212
```yaml
211213
# yaml-language-server: $schema=YOUR-PROJECTS-DIRECTORY/spec-json-schemas/schemas/2.6.0-without-$id.json
212214
```
215+
216+
## Extensions
217+
218+
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.
219+
### How to add schema of the extension
220+
213221

222+
1. All the extensions must be present in [./extensions](./extensions) folder.
223+
2. A proper folder structure must be followed to add the extensions.
224+
3. A new folder just as [x extension](./extensions/x) must be added with proper `versioning` and `schema file`.
225+
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).
214226

227+
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.
215228

216229

definitions/3.0.0/info.json

+61-57
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,70 @@
11
{
2-
"type": "object",
32
"description": "The object provides metadata about the API. The metadata can be used by the clients if needed.",
4-
"required": [
5-
"version",
6-
"title"
7-
],
8-
"additionalProperties": false,
9-
"patternProperties": {
10-
"^x-[\\w\\d\\.\\x2d_]+$": {
11-
"$ref": "http://asyncapi.com/definitions/3.0.0/specificationExtension.json"
12-
}
13-
},
14-
"properties": {
15-
"title": {
16-
"type": "string",
17-
"description": "A unique and precise title of the API."
18-
},
19-
"version": {
20-
"type": "string",
21-
"description": "A semantic version number of the API."
22-
},
23-
"description": {
24-
"type": "string",
25-
"description": "A longer description of the API. Should be different from the title. CommonMark is allowed."
26-
},
27-
"termsOfService": {
28-
"type": "string",
29-
"description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.",
30-
"format": "uri"
31-
},
32-
"contact": {
33-
"$ref": "http://asyncapi.com/definitions/3.0.0/contact.json"
34-
},
35-
"license": {
36-
"$ref": "http://asyncapi.com/definitions/3.0.0/license.json"
37-
},
38-
"tags": {
39-
"type": "array",
40-
"description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.",
41-
"items": {
42-
"oneOf": [
43-
{
44-
"$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json"
45-
},
46-
{
47-
"$ref": "http://asyncapi.com/definitions/3.0.0/tag.json"
48-
}
49-
]
3+
"allOf": [
4+
{
5+
"type": "object",
6+
"required": ["version", "title"],
7+
"additionalProperties": false,
8+
"patternProperties": {
9+
"^x-[\\w\\d\\.\\x2d_]+$": {
10+
"$ref": "http://asyncapi.com/definitions/3.0.0/specificationExtension.json"
11+
}
5012
},
51-
"uniqueItems": true
52-
},
53-
"externalDocs": {
54-
"oneOf": [
55-
{
56-
"$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json"
13+
"properties": {
14+
"title": {
15+
"type": "string",
16+
"description": "A unique and precise title of the API."
5717
},
58-
{
59-
"$ref": "http://asyncapi.com/definitions/3.0.0/externalDocs.json"
18+
"version": {
19+
"type": "string",
20+
"description": "A semantic version number of the API."
21+
},
22+
"description": {
23+
"type": "string",
24+
"description": "A longer description of the API. Should be different from the title. CommonMark is allowed."
25+
},
26+
"termsOfService": {
27+
"type": "string",
28+
"description": "A URL to the Terms of Service for the API. MUST be in the format of a URL.",
29+
"format": "uri"
30+
},
31+
"contact": {
32+
"$ref": "http://asyncapi.com/definitions/3.0.0/contact.json"
33+
},
34+
"license": {
35+
"$ref": "http://asyncapi.com/definitions/3.0.0/license.json"
36+
},
37+
"tags": {
38+
"type": "array",
39+
"description": "A list of tags for application API documentation control. Tags can be used for logical grouping of applications.",
40+
"items": {
41+
"oneOf": [
42+
{
43+
"$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json"
44+
},
45+
{
46+
"$ref": "http://asyncapi.com/definitions/3.0.0/tag.json"
47+
}
48+
]
49+
},
50+
"uniqueItems": true
51+
},
52+
"externalDocs": {
53+
"oneOf": [
54+
{
55+
"$ref": "http://asyncapi.com/definitions/3.0.0/Reference.json"
56+
},
57+
{
58+
"$ref": "http://asyncapi.com/definitions/3.0.0/externalDocs.json"
59+
}
60+
]
6061
}
61-
]
62+
}
63+
},
64+
{
65+
"$ref": "http://asyncapi.com/definitions/3.0.0/infoExtensions.json"
6266
}
63-
},
67+
],
6468
"example": {
6569
"$ref": "http://asyncapi.com/examples/3.0.0/info.json"
6670
},

definitions/3.0.0/infoExtensions.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "object",
3+
"description": "The object that lists all the extensions of Info",
4+
"properties": {
5+
"x-x":{
6+
"$ref": "http://asyncapi.com/extensions/x/0.1.0/schema.json"
7+
}
8+
},
9+
"$schema": "http://json-schema.org/draft-07/schema#",
10+
"$id": "http://asyncapi.com/definitions/3.0.0/infoExtensions.json"
11+
}

extensions/x/0.1.0/schema.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"type": "string",
3+
"description": "This extension allows you to provide the Twitter username of the account representing the team/company of the API.",
4+
"example": [
5+
"sambhavgupta75",
6+
"AsyncAPISpec"
7+
],
8+
"$schema": "http://json-schema.org/draft-07/schema#",
9+
"$id": "http://asyncapi.com/extensions/x/0.1.0/schema.json"
10+
}

tools/bundler/index.js

+40-13
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ const traverse = require('json-schema-traverse');
44
const { url } = require('inspector');
55
const definitionsDirectory = path.resolve(__dirname, '../../definitions');
66
const bindingsDirectory = path.resolve(__dirname, '../../bindings');
7+
const extensionsDirectory = path.resolve(__dirname, '../../extensions');
78
const outputDirectory = path.resolve(__dirname, '../../schemas');
89
const JSON_SCHEMA_PROP_NAME = 'json-schema-draft-07-schema';
910
console.log(`Looking for separate definitions in the following directory: ${definitionsDirectory}`);
1011
console.log(`Looking for binding version schemas in the following directory: ${bindingsDirectory}`);
12+
console.log(`Looking for extension version schemas in the following directory: ${extensionsDirectory}`);
1113
console.log(`Using the following output directory: ${outputDirectory}`);
1214

1315
// definitionsRegex is used to transform the name of a definition into a valid one to be used in the -without-$id.json files.
@@ -16,13 +18,17 @@ const definitionsRegex = /http:\/\/asyncapi\.com\/definitions\/[^/]*\/(.+)\.json
1618
// definitionsRegex is used to transform the name of a binding into a valid one to be used in the -without-$id.json files.
1719
const bindingsRegex = /http:\/\/asyncapi\.com\/(bindings\/[^/]+)\/([^/]+)\/(.+)\.json(.*)/i
1820

21+
// definitionsRegex is used to transform the name of a binding into a valid one to be used in the -without-$id.json files.
22+
const extensionsRegex = /http:\/\/asyncapi\.com\/(extensions\/[^/]+)\/([^/]+)\/(.+)\.json(.*)/i
23+
1924
/**
2025
* Function to load all the core AsyncAPI spec definition (except the root asyncapi schema, as that will be loaded later) into the bundler.
2126
*/
2227
async function loadDefinitions(bundler, versionDir) {
2328
const definitions = await fs.promises.readdir(versionDir);
2429
const definitionFiles = definitions.filter((value) => {return !value.includes('asyncapi')}).map((file) => fs.readFileSync(path.resolve(versionDir, file)));
2530
const definitionJson = definitionFiles.map((file) => JSON.parse(file));
31+
2632
for (const jsonFile of definitionJson) {
2733
if (jsonFile.example) {
2834
// Replaced the example property with the referenced example property
@@ -38,20 +44,36 @@ async function loadDefinitions(bundler, versionDir) {
3844
}
3945
}
4046
}
47+
48+
4149
/**
42-
* Function to load all the binding version schemas into the bundler
50+
* Function to load all schemas into bundler, by "type" you specify if these are "bindings" or "extensions"
4351
*/
44-
async function loadBindings(bundler) {
45-
const bindingDirectories = await fs.promises.readdir(bindingsDirectory);
46-
for (const bindingDirectory of bindingDirectories) {
47-
const bindingVersionDirectories = await fs.promises.readdir(path.resolve(bindingsDirectory, bindingDirectory));
48-
const bindingVersionDirectoriesFiltered = bindingVersionDirectories.filter((file) => fs.lstatSync(path.resolve(bindingsDirectory, bindingDirectory, file)).isDirectory());
49-
for (const bindingVersionDirectory of bindingVersionDirectoriesFiltered) {
50-
const bindingFiles = await fs.promises.readdir(path.resolve(bindingsDirectory, bindingDirectory, bindingVersionDirectory));
51-
const bindingFilesFiltered = bindingFiles.filter((bindingFile) => path.extname(bindingFile) === '.json').map((bindingFile) => path.resolve(bindingsDirectory, bindingDirectory, bindingVersionDirectory, bindingFile));
52-
for (const bindingFile of bindingFilesFiltered) {
53-
const bindingFileContent = require(bindingFile);
54-
bundler.add(bindingFileContent);
52+
async function loadSchemas(bundler, type) {
53+
54+
let directory;
55+
56+
switch (type) {
57+
case "bindings":
58+
directory = bindingsDirectory;
59+
break;
60+
case "extensions":
61+
directory = extensionsDirectory;
62+
break;
63+
default:
64+
console.error("Invalid input. I'm not going to assume if you want bindings or extensions - these are different beasts.");
65+
}
66+
67+
const directories = await fs.promises.readdir(directory);
68+
for (const nestedDir of directories) {
69+
const versionDirectories = await fs.promises.readdir(path.resolve(directory, nestedDir));
70+
const versionDirectoriesFiltered = versionDirectories.filter((file) => fs.lstatSync(path.resolve(directory, nestedDir, file)).isDirectory());
71+
for (const versionDir of versionDirectoriesFiltered) {
72+
const files = await fs.promises.readdir(path.resolve(directory, nestedDir, versionDir));
73+
const filesFiltered = files.filter((file) => path.extname(file) === '.json').map((file) => path.resolve(directory, nestedDir, versionDir, file));
74+
for (const filteredFile of filesFiltered) {
75+
const fileContent = require(filteredFile);
76+
bundler.add(fileContent);
5577
}
5678
}
5779
}
@@ -74,7 +96,8 @@ async function loadBindings(bundler) {
7496
const outputFileWithoutId = path.resolve(outputDirectory, `${version}-without-$id.json`);
7597
const versionDir = path.resolve(definitionsDirectory, version);
7698
await loadDefinitions(Bundler, versionDir);
77-
await loadBindings(Bundler);
99+
await loadSchemas(Bundler, 'bindings');
100+
await loadSchemas(Bundler, 'extensions');
78101

79102
const filePathToBundle = `file://${versionDir}/asyncapi.json`;
80103
const fileToBundle = await Bundler.get(filePathToBundle);
@@ -155,6 +178,10 @@ function getDefinitionName(def) {
155178
const result = bindingsRegex.exec(def);
156179
if (result) return `${result[1].replace('/', '-')}-${result[2]}-${result[3]}`;
157180
}
181+
if (def.startsWith('http://asyncapi.com/extensions')) {
182+
const result = extensionsRegex.exec(def);
183+
if (result) return `${result[1].replace('/', '-')}-${result[2]}-${result[3]}`;
184+
}
158185

159186
return path.basename(def, '.json')
160187
}

0 commit comments

Comments
 (0)