From 276ecfd4d61e2f4b73440b683c2fe874b50c02b4 Mon Sep 17 00:00:00 2001 From: nulltoken Date: Wed, 19 Feb 2020 07:51:43 +0100 Subject: [PATCH] WIP --- package.json | 8 +- rollup.config.js | 29 +- scripts/generate-assets.ts | 22 +- scripts/generate-karma-fixtures.js | 6 +- setupKarma.ts | 18 +- src/__tests__/generate-assets.jest.test.ts | 7 + src/__tests__/spectral.test.ts | 20 +- src/assets.ts | 1 + src/cli/services/linter/linter.ts | 2 + src/cli/services/linter/utils/getRuleset.ts | 2 +- src/formats/__tests__/asyncapi.test.ts | 28 + src/formats/asyncapi.ts | 10 + src/formats/index.ts | 1 + .../__tests__/__fixtures__/streetlights.yaml | 211 +++ .../aas/__tests__/streetlights.jest.test.ts | 30 + src/rulesets/aas/index.json | 337 ++++ src/rulesets/aas/schemas/schema.aas2.json | 1597 +++++++++++++++++ src/rulesets/reader.ts | 16 +- .../scenarios/aas2-streetlights.scenario | 221 +++ .../rules-matching-multiple-places.scenario | 2 +- .../scenarios/unrecognized-format.scenario | 2 +- 21 files changed, 2523 insertions(+), 47 deletions(-) create mode 100644 src/formats/__tests__/asyncapi.test.ts create mode 100644 src/formats/asyncapi.ts create mode 100644 src/rulesets/aas/__tests__/__fixtures__/streetlights.yaml create mode 100644 src/rulesets/aas/__tests__/streetlights.jest.test.ts create mode 100644 src/rulesets/aas/index.json create mode 100644 src/rulesets/aas/schemas/schema.aas2.json create mode 100644 test-harness/scenarios/aas2-streetlights.scenario diff --git a/package.json b/package.json index f21ee047ba..c2e1bf16ee 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "scripts": { "build.binary": "pkg . --output ./binaries/spectral", "build.clean": "rimraf ./coverage && rimraf ./dist && rimraf ./rulesets && rimraf ./__karma__/__fixtures__", - "build.oas-functions": "rollup -c", + "build.functions": "rollup -c", "build": "tsc -p ./tsconfig.build.json", "cli": "node -r ts-node/register -r tsconfig-paths/register src/cli/index.ts", "cli:debug": "node -r ts-node/register -r tsconfig-paths/register --inspect-brk src/cli/index.ts", @@ -44,9 +44,9 @@ "lint.fix": "yarn lint --fix", "lint": "tsc --noEmit && tslint 'src/**/*.ts'", "copy.html-templates": "copyfiles -u 1 \"./src/formatters/html/*.html\" \"./dist/\"", - "postbuild.oas-functions": "copyfiles -u 1 \"dist/rulesets/oas*/functions/*.js\" ./", - "postbuild": "yarn build.oas-functions && yarn generate-assets", - "prebuild": "yarn build.clean && copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" dist && copyfiles -u 1 \"src/rulesets/oas*/**/*.json\" ./ && yarn copy.html-templates", + "postbuild.functions": "copyfiles -u 1 \"dist/rulesets/{o,a}as/functions/*.js\" ./", + "postbuild": "yarn build.functions && yarn generate-assets", + "prebuild": "yarn build.clean && copyfiles -u 1 \"src/rulesets/{o,a}as/**/*.json\" dist && copyfiles -u 1 \"src/rulesets/{o,a}as/**/*.json\" ./ && yarn copy.html-templates", "prebuild.binary": "yarn build", "pretest.karma": "node ./scripts/generate-karma-fixtures.js && yarn pretest", "pretest": "yarn generate-assets", diff --git a/rollup.config.js b/rollup.config.js index d74dac46bb..dc1ba2e55e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,18 +6,27 @@ import commonjs from 'rollup-plugin-commonjs'; import { terser } from 'rollup-plugin-terser'; const BASE_PATH = process.cwd(); -const directory = 'dist/rulesets/oas/functions'; -const targetDir = path.join(BASE_PATH, directory); const functions = []; -for (const file of fs.readdirSync(targetDir)) { - const targetFile = path.join(targetDir, file); - const stat = fs.statSync(targetFile); - if (!stat.isFile()) continue; - const ext = path.extname(targetFile); - if (ext !== '.js') continue; - - functions.push(targetFile); + +const builtIns = ['oas', 'aas'] + +for (const rulesetName of builtIns) { + const targetDir = path.join(BASE_PATH, `dist/rulesets/${rulesetName}/functions/`); + + if (!fs.existsSync(targetDir)) { + continue; + } + + for (const file of fs.readdirSync(targetDir)) { + const targetFile = path.join(targetDir, file); + const stat = fs.statSync(targetFile); + if (!stat.isFile()) continue; + const ext = path.extname(targetFile); + if (ext !== '.js') continue; + + functions.push(targetFile); + } } module.exports = functions.map(fn => ({ diff --git a/scripts/generate-assets.ts b/scripts/generate-assets.ts index b68995a761..5acc52d6c7 100644 --- a/scripts/generate-assets.ts +++ b/scripts/generate-assets.ts @@ -1,10 +1,10 @@ /** - * This script generates a list of assets that are needed to load spectral:oas ruleset. - * It contains all OAS custom functions and *resolved* rulesets. - * The assets are stores in a single filed call assets.json in the following format: + * This script generates a list of assets that are needed to load spectral:oas and spectral:aas rulesets. + * It contains all custom functions and *resolved* rulesets. + * The assets are stored in a single file named `assets.json` in the following format: * `: ` * where the `require-call-path` is the path you'd normally pass to require(), i.e. `@stoplight/spectral/rulesets/oas/index.js` and `content` is the text data. - * Assets can be loaded using Spectral#registerStaticAssets statc method, i.e. `Spectral.registerStaticAssets(require('@stoplight/spectral/rulesets/assets/assets.json'))`; + * Assets can be loaded using Spectral#registerStaticAssets static method, i.e. `Spectral.registerStaticAssets(require('@stoplight/spectral/rulesets/assets/assets.json'))`; * If you execute the code above, ruleset will be loaded fully offline, without a need to make any request. */ @@ -30,14 +30,16 @@ const assetsPath = path.join(baseDir, `assets.json`); const generatedAssets = {}; (async () => { - await processDirectory(generatedAssets, path.join(__dirname, '../rulesets/oas')); - await writeFileAsync(assetsPath, JSON.stringify(generatedAssets, null, 2)); + for (const kind of ['oas', 'aas']) { + await processDirectory(generatedAssets, path.join(__dirname, `../rulesets/${kind}`)); + await writeFileAsync(assetsPath, JSON.stringify(generatedAssets, null, 2)); + } })(); async function processDirectory(assets: Record, dir: string) { await Promise.all( (await readdirAsync(dir)).map(async (name: string) => { - if (name === 'schemas') return; + if (['schemas', '__tests__'].includes(name)) return; const target = path.join(dir, name); const stats = await statAsync(target); if (stats.isDirectory()) { @@ -53,7 +55,11 @@ async function processDirectory(assets: Record, dir: string) { baseUri: target, parseResolveResult(opts) { return new Promise(resolve => { - resolve({ result: parse(opts.result) }); + try { + resolve({ result: parse(opts.result) }); + } catch (e) { + resolve({ error: e }); + } }); }, }) diff --git a/scripts/generate-karma-fixtures.js b/scripts/generate-karma-fixtures.js index 203c412a21..4f329f9870 100755 --- a/scripts/generate-karma-fixtures.js +++ b/scripts/generate-karma-fixtures.js @@ -8,9 +8,9 @@ if (!fs.existsSync(baseDir)) { fs.mkdirSync(baseDir); } -for (const spec of ['']) { - const target = path.join(baseDir, `oas${spec}-functions.json`); - const fnsPath = path.join(__dirname, `../rulesets/oas${spec}/functions`); +for (const rulesetName of ['oas', 'aas']) { + const target = path.join(baseDir, `${rulesetName}-functions.json`); + const fnsPath = path.join(__dirname, `../rulesets/${rulesetName}/functions`); const bundledFns = {}; if (fs.existsSync(fnsPath)) { diff --git a/setupKarma.ts b/setupKarma.ts index 9313240824..142b45cbb7 100644 --- a/setupKarma.ts +++ b/setupKarma.ts @@ -3,6 +3,7 @@ import { FetchMockSandbox } from 'fetch-mock'; const oasRuleset = JSON.parse(JSON.stringify(require('./rulesets/oas/index.json'))); const oasFunctions = JSON.parse(JSON.stringify(require('./__karma__/__fixtures__/oas-functions.json'))); +const aasFunctions = JSON.parse(JSON.stringify(require('./__karma__/__fixtures__/aas-functions.json'))); const oas2Schema = JSON.parse(JSON.stringify(require('./rulesets/oas/schemas/schema.oas2.json'))); const oas3Schema = JSON.parse(JSON.stringify(require('./rulesets/oas/schemas/schema.oas3.json'))); @@ -28,12 +29,17 @@ beforeEach(() => { body: JSON.parse(JSON.stringify(oas3Schema)), }); - for (const [name, fn] of Object.entries(oasFunctions)) { - fetchMock.get(`https://unpkg.com/@stoplight/spectral/rulesets/oas/functions/${name}`, { - status: 200, - body: fn, - }); - } + [ + ['oas', oasFunctions], + ['aas', aasFunctions], + ].forEach(([rulesetName, funcs]) => { + for (const [name, fn] of Object.entries(funcs)) { + fetchMock.get(`https://unpkg.com/@stoplight/spectral/rulesets/${rulesetName}/functions/${name}`, { + status: 200, + body: fn, + }); + } + }); fetchMock.get('http://json-schema.org/draft-04/schema', { status: 200, diff --git a/src/__tests__/generate-assets.jest.test.ts b/src/__tests__/generate-assets.jest.test.ts index f9609b16ec..b5517df5b1 100644 --- a/src/__tests__/generate-assets.jest.test.ts +++ b/src/__tests__/generate-assets.jest.test.ts @@ -19,6 +19,7 @@ describe('generate-assets', () => { const testCases = [ ['oas', 'oas2-schema', 'title', 'A JSON Schema for Swagger 2.0 API.'], ['oas', 'oas3-schema', 'description', 'Validation schema for OpenAPI Specification 3.0.X.'], + ['aas', 'asyncapi-schema', 'title', 'AsyncAPI 2.0.0 schema.'], ]; it.each(testCases)( @@ -35,4 +36,10 @@ describe('generate-assets', () => { }, ); }); + + it('Does not contain test files', () => { + Object.keys(assets).forEach(key => { + expect(key).not.toMatch('__tests__'); + }); + }); }); diff --git a/src/__tests__/spectral.test.ts b/src/__tests__/spectral.test.ts index c8c851c1b0..8d2a0912fb 100644 --- a/src/__tests__/spectral.test.ts +++ b/src/__tests__/spectral.test.ts @@ -12,17 +12,22 @@ import { RulesetExceptionCollection } from '../types/ruleset'; import { buildRulesetExceptionCollectionFrom } from '../../setupTests'; const oasRuleset = JSON.parse(JSON.stringify(require('../rulesets/oas/index.json'))); +const aasRuleset = JSON.parse(JSON.stringify(require('../rulesets/aas/index.json'))); const oasRulesetRules: Dictionary = oasRuleset.rules; +const aasRulesetRules: Dictionary = aasRuleset.rules; describe('spectral', () => { describe('loadRuleset', () => { - test('should support loading built-in rulesets', async () => { + test.each([ + ['spectral:oas', oasRulesetRules], + ['spectral:aas', aasRulesetRules], + ])('should support loading "%s" built-in ruleset', async (rulesetName, rules) => { const s = new Spectral(); - await s.loadRuleset('spectral:oas'); + await s.loadRuleset(rulesetName); expect(s.rules).toEqual( expect.objectContaining( - Object.entries(oasRulesetRules).reduce>((oasRules, [name, rule]) => { + Object.entries(rules).reduce>((oasRules, [name, rule]) => { oasRules[name] = { name, ...rule, @@ -37,12 +42,15 @@ describe('spectral', () => { ); }); - test('should support loading multiple times the built-in ruleset', async () => { + test.each([ + ['spectral:oas', oasRulesetRules], + ['spectral:aas', aasRulesetRules], + ])('should support loading multiple times the built-in ruleset "%s"', async (rulesetName, expectedRules) => { const s = new Spectral(); - await s.loadRuleset(['spectral:oas', 'spectral:oas']); + await s.loadRuleset([rulesetName, rulesetName]); expect(s.rules).toEqual( - Object.entries(oasRulesetRules).reduce>((oasRules, [name, rule]) => { + Object.entries(expectedRules).reduce>((oasRules, [name, rule]) => { oasRules[name] = { name, ...rule, diff --git a/src/assets.ts b/src/assets.ts index 7e9d8543f3..4ad5486e3d 100644 --- a/src/assets.ts +++ b/src/assets.ts @@ -6,6 +6,7 @@ function resolveSpectralRuleset(ruleset: string) { export const RESOLVE_ALIASES: Dictionary = { 'spectral:oas': resolveSpectralRuleset('oas'), + 'spectral:aas': resolveSpectralRuleset('aas'), }; export const STATIC_ASSETS: Dictionary = {}; diff --git a/src/cli/services/linter/linter.ts b/src/cli/services/linter/linter.ts index 949d079b61..6586b22adb 100644 --- a/src/cli/services/linter/linter.ts +++ b/src/cli/services/linter/linter.ts @@ -1,5 +1,6 @@ import { Document, STDIN } from '../../../document'; import { + isAsyncApiv2, isJSONSchema, isJSONSchemaDraft2019_09, isJSONSchemaDraft4, @@ -21,6 +22,7 @@ import { getResolver } from './utils/getResolver'; const KNOWN_FORMATS: Array<[string, FormatLookup, string]> = [ ['oas2', isOpenApiv2, 'OpenAPI 2.0 (Swagger) detected'], ['oas3', isOpenApiv3, 'OpenAPI 3.x detected'], + ['aas2', isAsyncApiv2, 'AsyncAPI 2.x detected'], ['json-schema', isJSONSchema, 'JSON Schema detected'], ['json-schema-loose', isJSONSchemaLoose, 'JSON Schema (loose) detected'], ['json-schema-draft4', isJSONSchemaDraft4, 'JSON Schema Draft 4 detected'], diff --git a/src/cli/services/linter/utils/getRuleset.ts b/src/cli/services/linter/utils/getRuleset.ts index cdee8b3220..cfd466111b 100644 --- a/src/cli/services/linter/utils/getRuleset.ts +++ b/src/cli/services/linter/utils/getRuleset.ts @@ -21,5 +21,5 @@ export async function getRuleset(rulesetFile: Optional) { return await (rulesetFiles ? loadRulesets(process.cwd(), Array.isArray(rulesetFiles) ? rulesetFiles : [rulesetFiles]) - : readRuleset('spectral:oas')); + : readRuleset(['spectral:oas', 'spectral:aas'])); // TODO: Rule name clashes } diff --git a/src/formats/__tests__/asyncapi.test.ts b/src/formats/__tests__/asyncapi.test.ts new file mode 100644 index 0000000000..e83c209538 --- /dev/null +++ b/src/formats/__tests__/asyncapi.test.ts @@ -0,0 +1,28 @@ +import { isAsyncApiv2 } from '../asyncapi'; + +describe('AsyncApi format', () => { + describe('AsyncApi 2.0', () => { + it.each(['2.0.0'])('recognizes %s version correctly', version => { + expect(isAsyncApiv2({ asyncapi: version })).toBe(true); + }); + + const testCases = [ + { asyncapi: '3.0' }, + { asyncapi: '2' }, + { asyncapi: '2.0' }, + { asyncapi: '1.0' }, + { asyncapi: 2 }, + { openapi: '4.0' }, + { openapi: '2.0' }, + { openapi: null }, + { swagger: null }, + { swagger: '3.0' }, + {}, + null, + ]; + + it.each(testCases)('does not recognize invalid document %o', document => { + expect(isAsyncApiv2(document)).toBe(false); + }); + }); +}); diff --git a/src/formats/asyncapi.ts b/src/formats/asyncapi.ts new file mode 100644 index 0000000000..64d4e28a4b --- /dev/null +++ b/src/formats/asyncapi.ts @@ -0,0 +1,10 @@ +import { isObject } from 'lodash'; + +type MaybeAsyncApi2 = Partial<{ asyncapi: unknown }>; + +export const bearsAStringPropertyNamed = (document: unknown, propertyName: string) => { + return isObject(document) && propertyName in document && typeof document[propertyName] === 'string'; +}; + +export const isAsyncApiv2 = (document: unknown) => + bearsAStringPropertyNamed(document, 'asyncapi') && String((document as MaybeAsyncApi2).asyncapi) === '2.0.0'; diff --git a/src/formats/index.ts b/src/formats/index.ts index 54f1929ebb..78a2a4f1c0 100644 --- a/src/formats/index.ts +++ b/src/formats/index.ts @@ -1,2 +1,3 @@ export * from './openapi'; +export * from './asyncapi'; export * from './json-schema'; diff --git a/src/rulesets/aas/__tests__/__fixtures__/streetlights.yaml b/src/rulesets/aas/__tests__/__fixtures__/streetlights.yaml new file mode 100644 index 0000000000..51f75039f2 --- /dev/null +++ b/src/rulesets/aas/__tests__/__fixtures__/streetlights.yaml @@ -0,0 +1,211 @@ +%YAML 1.2 +--- +asyncapi: '2.0.0' +info: + title: Streetlights API + version: '1.0.0' + description: | + The Smartylighting Streetlights API allows you to remotely manage the city lights. + + ### Check out its awesome features: + + * Turn a specific streetlight on/off 🌃 + * Dim a specific streetlight 😎 + * Receive real-time information about environmental lighting conditions 📈 + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + +servers: + production: + url: test.mosquitto.org:{port} + protocol: mqtt + description: Test broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' + security: + - apiKey: [] + - supportedOauthFlows: + - streetlights:on + - streetlights:off + - streetlights:dim + - openIdConnectWellKnown: [] + +defaultContentType: application/json + +channels: + smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured: + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + publish: + summary: Inform about environmental lighting conditions of a particular streetlight. + operationId: receiveLightMeasurement + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/lightMeasured' + + smartylighting/streetlights/1/0/action/{streetlightId}/turn/on: + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + subscribe: + operationId: turnOn + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/turnOnOff' + + smartylighting/streetlights/1/0/action/{streetlightId}/turn/off: + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + subscribe: + operationId: turnOff + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/turnOnOff' + + smartylighting/streetlights/1/0/action/{streetlightId}/dim: + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + subscribe: + operationId: dimLight + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/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: + lightMeasuredPayload: + type: object + properties: + 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: + apiKey: + type: apiKey + in: user + description: Provide your API key as the user and leave the password empty. + supportedOauthFlows: + type: oauth2 + description: Flows to support OAuth 2.0 + flows: + implicit: + authorizationUrl: 'https://authserver.example/auth' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + password: + tokenUrl: 'https://authserver.example/token' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + clientCredentials: + tokenUrl: 'https://authserver.example/token' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + authorizationCode: + authorizationUrl: 'https://authserver.example/auth' + tokenUrl: 'https://authserver.example/token' + refreshUrl: 'https://authserver.example/refresh' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + openIdConnectWellKnown: + type: openIdConnect + openIdConnectUrl: 'https://authserver.example/.well-known' + + parameters: + streetlightId: + description: The ID of the streetlight. + schema: + type: string + + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + + operationTraits: + kafka: + bindings: + kafka: + clientId: my-app-id diff --git a/src/rulesets/aas/__tests__/streetlights.jest.test.ts b/src/rulesets/aas/__tests__/streetlights.jest.test.ts new file mode 100644 index 0000000000..3cf88991f5 --- /dev/null +++ b/src/rulesets/aas/__tests__/streetlights.jest.test.ts @@ -0,0 +1,30 @@ +import * as path from '@stoplight/path'; + +import { Document } from '../../../document'; +import { isAsyncApiv2 } from '../../../formats'; +import { readParsable } from '../../../fs/reader'; +import { Spectral } from '../../../index'; +import * as Parsers from '../../../parsers'; +import { httpAndFileResolver } from '../../../resolvers/http-and-file'; + +describe('streetlights', () => { + const streetlights = path.join(__dirname, './__fixtures__/streetlights.yaml'); + + const s = new Spectral({ resolver: httpAndFileResolver }); + s.registerFormat('aas2', isAsyncApiv2); + + test('can be linted', async () => { + await s.loadRuleset('spectral:aas'); + + const document = new Document(await readParsable(streetlights, { encoding: 'utf8' }), Parsers.Yaml, streetlights); + + const results = await s.run(document); + + expect(results).toEqual([ + expect.objectContaining({ + code: 'asyncapi-info-contact', + path: ['info'], + }), + ]); + }); +}); diff --git a/src/rulesets/aas/index.json b/src/rulesets/aas/index.json new file mode 100644 index 0000000000..e8ace92c89 --- /dev/null +++ b/src/rulesets/aas/index.json @@ -0,0 +1,337 @@ +{ + "rules": { + "asyncapi-contact-properties": { + "description": "Contact object should have `name`, `url` and `email`.", + "recommended": true, + "formats": [ + "aas2" + ], + "severity": 2, + "type": "style", + "given": "$.info.contact", + "then": [ + { + "field": "name", + "function": "truthy" + }, + { + "field": "url", + "function": "truthy" + }, + { + "field": "email", + "function": "truthy" + } + ] + }, + "asyncapi-example-value-or-externalValue": { + "description": "Example should have either a `value` or `externalValue` field.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$..example", + "then": { + "function": "xor", + "functionOptions": { + "properties": [ + "externalValue", + "value" + ] + } + } + }, + "asyncapi-info-contact": { + "description": "Info object should contain `contact` object.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$", + "then": { + "field": "info.contact", + "function": "truthy" + } + }, + "asyncapi-info-description": { + "description": "AsyncAPI object info `description` must be present and non-empty string.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$", + "then": { + "field": "info.description", + "function": "truthy" + }, + "tags": [ + "api" + ] + }, + "asyncapi-info-license": { + "description": "AsyncAPI object info `license` must be present and non-empty string.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$", + "then": { + "field": "info.license", + "function": "truthy" + }, + "tags": [ + "api" + ] + }, + "asyncapi-license-url": { + "description": "License object should include `url`.", + "recommended": false, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$", + "then": { + "field": "info.license.url", + "function": "truthy" + } + }, + "asyncapi-tags-alphabetical": { + "description": "AsyncAPI object should have alphabetical `tags`.", + "recommended": false, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$", + "then": { + "field": "tags", + "function": "alphabetical", + "functionOptions": { + "keyedBy": "name" + } + } + }, + "asyncapi-operation-description": { + "description": "Operation `description` must be present and non-empty string.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]", + "then": { + "field": "description", + "function": "truthy" + }, + "tags": [ + "operation" + ] + }, + "asyncapi-operation-operationId": { + "description": "Operation should have an `operationId`.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$.paths.*[?( @property === 'get' || @property === 'put' || @property === 'post' || @property === 'delete' || @property === 'options' || @property === 'head' || @property === 'patch' || @property === 'trace' )]", + "then": { + "field": "operationId", + "function": "truthy" + }, + "tags": [ + "operation" + ] + }, + "asyncapi-path-declarations-must-exist": { + "description": "Path parameter declarations cannot be empty, ex.`/companies/{}` is invalid.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$.paths", + "then": { + "field": "@key", + "function": "pattern", + "functionOptions": { + "notMatch": "{}" + } + }, + "tags": [ + "given" + ] + }, + "asyncapi-path-keys-no-trailing-slash": { + "description": "paths should not end with a slash.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$.paths", + "then": { + "field": "@key", + "function": "pattern", + "functionOptions": { + "notMatch": ".+\\/$" + } + }, + "tags": [ + "given" + ] + }, + "asyncapi-path-not-include-query": { + "description": "given keys should not include a query string.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$.paths", + "then": { + "field": "@key", + "function": "pattern", + "functionOptions": { + "notMatch": "\\?" + } + }, + "tags": [ + "given" + ] + }, + "asyncapi-tag-description": { + "description": "Tag object should have a `description`.", + "recommended": false, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$.tags[*]", + "then": { + "field": "description", + "function": "truthy" + } + }, + "asyncapi-servers": { + "description": "Define at least one server.", + "recommended": true, + "formats": [ + "aas2" + ], + "given": "$", + "then": { + "field": "servers", + "function": "schema", + "functionOptions": { + "schema": { + "type": "object", + "minProperties": 1 + } + } + } + }, + "asyncapi-parameter-description": { + "description": "Parameter objects should have a `description`.", + "recommended": false, + "formats": [ + "aas2" + ], + "given": "$..['parameters']", + "then": { + "field": "description", + "function": "truthy" + }, + "tags": [ + "parameters" + ] + }, + "asyncapi-server-not-example.com": { + "description": "Server URL should not point at example.com.", + "recommended": false, + "formats": [ + "aas2" + ], + "given": "$.servers[*].url", + "then": { + "function": "pattern", + "functionOptions": { + "notMatch": "example\\.com" + } + } + }, + "asyncapi-server-trailing-slash": { + "description": "Server URL should not have a trailing slash.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "given": "$.servers[*].url", + "then": { + "function": "pattern", + "functionOptions": { + "notMatch": "/$" + } + } + }, + "asyncapi-valid-schema-example": { + "description": "Examples must be valid against their defined schema.", + "message": "{{error}}", + "severity": 0, + "recommended": false, + "formats": [ + "aas2" + ], + "type": "validation", + "given": "$.components.schemas..[?(@property !== 'properties' && @.example && (@.type || @.format || @.$ref))]", + "then": { + "function": "schemaPath", + "functionOptions": { + "field": "example", + "schemaPath": "$" + } + } + }, + "asyncapi-schema": { + "description": "Validate structure of AsyncAPI v2.0.0 Specification.", + "message": "{{error}}", + "severity": 0, + "recommended": true, + "formats": [ + "aas2" + ], + "type": "validation", + "given": "$", + "then": { + "function": "schema", + "functionOptions": { + "schema": { + "$ref": "./schemas/schema.aas2.json" + } + } + } + }, + "asyncapi-unused-components-schema": { + "description": "Potentially unused components schema has been detected.", + "recommended": true, + "formats": [ + "aas2" + ], + "type": "style", + "resolved": false, + "given": "$.components.schemas", + "then": { + "function": "unreferencedReusableObject", + "functionOptions": { + "reusableObjectsLocation": "#/components/schemas" + } + } + } + } +} diff --git a/src/rulesets/aas/schemas/schema.aas2.json b/src/rulesets/aas/schemas/schema.aas2.json new file mode 100644 index 0000000000..7020a63a10 --- /dev/null +++ b/src/rulesets/aas/schemas/schema.aas2.json @@ -0,0 +1,1597 @@ +{ + "title": "AsyncAPI 2.0.0 schema.", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "asyncapi", + "info", + "channels" + ], + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "asyncapi": { + "type": "string", + "enum": [ + "2.0.0" + ], + "description": "The AsyncAPI specification version of this document." + }, + "id": { + "type": "string", + "description": "A unique id representing the application.", + "format": "uri" + }, + "info": { + "$ref": "#/definitions/info" + }, + "servers": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/server" + } + }, + "defaultContentType": { + "type": "string" + }, + "channels": { + "$ref": "#/definitions/channels" + }, + "components": { + "$ref": "#/definitions/components" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "definitions": { + "Reference": { + "type": "object", + "required": [ + "$ref" + ], + "properties": { + "$ref": { + "$ref": "#/definitions/ReferenceObject" + } + } + }, + "ReferenceObject": { + "type": "string", + "format": "uri-reference" + }, + "info": { + "type": "object", + "description": "General information about the API.", + "required": [ + "version", + "title" + ], + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$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" + } + } + }, + "contact": { + "type": "object", + "description": "Contact information for the owners of the API.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The identifying name of the contact person/organization." + }, + "url": { + "type": "string", + "description": "The URL pointing to the contact information.", + "format": "uri" + }, + "email": { + "type": "string", + "description": "The email address of the contact person/organization.", + "format": "email" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + } + }, + "license": { + "type": "object", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the license type. It's encouraged to use an OSI compatible license." + }, + "url": { + "type": "string", + "description": "The URL pointing to the license.", + "format": "uri" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + } + }, + "server": { + "type": "object", + "description": "An object representing a Server.", + "required": [ + "url", + "protocol" + ], + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "protocol": { + "type": "string", + "description": "The transfer protocol." + }, + "protocolVersion": { + "type": "string" + }, + "variables": { + "$ref": "#/definitions/serverVariables" + }, + "security": { + "type": "array", + "items": { + "$ref": "#/definitions/SecurityRequirement" + } + }, + "bindings": { + "$ref": "#/definitions/bindingsObject" + } + } + }, + "serverVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/serverVariable" + } + }, + "serverVariable": { + "type": "object", + "description": "An object representing a Server Variable for server URL template substitution.", + "minProperties": 1, + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "enum": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "default": { + "type": "string" + }, + "description": { + "type": "string" + }, + "examples": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "channels": { + "type": "object", + "propertyNames": { + "type": "string", + "format": "uri-template", + "minLength": 1 + }, + "additionalProperties": { + "$ref": "#/definitions/channelItem" + } + }, + "components": { + "type": "object", + "description": "An object to hold a set of reusable objects for different aspects of the AsyncAPI Specification.", + "additionalProperties": false, + "properties": { + "schemas": { + "$ref": "#/definitions/schemas" + }, + "messages": { + "$ref": "#/definitions/messages" + }, + "securitySchemes": { + "type": "object", + "patternProperties": { + "^[\\w\\d\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/SecurityScheme" + } + ] + } + } + }, + "parameters": { + "$ref": "#/definitions/parameters" + }, + "correlationIds": { + "type": "object", + "patternProperties": { + "^[\\w\\d\\.\\-_]+$": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/correlationId" + } + ] + } + } + }, + "operationTraits": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/operationTrait" + } + }, + "messageTraits": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/messageTrait" + } + }, + "serverBindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/bindingsObject" + } + }, + "channelBindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/bindingsObject" + } + }, + "operationBindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/bindingsObject" + } + }, + "messageBindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/bindingsObject" + } + } + } + }, + "schemas": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "description": "JSON objects describing schemas the API uses." + }, + "messages": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/message" + }, + "description": "JSON objects describing the messages being consumed and produced by the API." + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + }, + "description": "JSON objects describing re-usable channel parameters." + }, + "schema": { + "allOf": [ + { + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema/allOf/0" + } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" + }, + { + "default": 0 + } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "default": [] + } + }, + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { + "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" + }, + "minLength": { + "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "$ref": "#/definitions/schema/allOf/0" + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema/allOf/0" + }, + { + "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" + } + ], + "default": true + }, + "maxItems": { + "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" + }, + "minItems": { + "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { + "$ref": "#/definitions/schema/allOf/0" + }, + "maxProperties": { + "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeInteger" + }, + "minProperties": { + "$ref": "#/definitions/schema/allOf/0/definitions/nonNegativeIntegerDefault0" + }, + "required": { + "$ref": "#/definitions/schema/allOf/0/definitions/stringArray" + }, + "additionalProperties": { + "$ref": "#/definitions/schema/allOf/0" + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema/allOf/0" + }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema/allOf/0" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema/allOf/0" + }, + "propertyNames": { + "format": "regex" + }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema/allOf/0" + }, + { + "$ref": "#/definitions/schema/allOf/0/definitions/stringArray" + } + ] + } + }, + "propertyNames": { + "$ref": "#/definitions/schema/allOf/0" + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/schema/allOf/0/definitions/simpleTypes" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/schema/allOf/0/definitions/simpleTypes" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { + "type": "string" + }, + "contentMediaType": { + "type": "string" + }, + "contentEncoding": { + "type": "string" + }, + "if": { + "$ref": "#/definitions/schema/allOf/0" + }, + "then": { + "$ref": "#/definitions/schema/allOf/0" + }, + "else": { + "$ref": "#/definitions/schema/allOf/0" + }, + "allOf": { + "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schema/allOf/0/definitions/schemaArray" + }, + "not": { + "$ref": "#/definitions/schema/allOf/0" + } + }, + "default": true + }, + { + "type": "object", + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "boolean" + } + ], + "default": {} + }, + "items": { + "anyOf": [ + { + "$ref": "#/definitions/schema" + }, + { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + } + ], + "default": {} + }, + "allOf": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/schema" + } + }, + "oneOf": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/definitions/schema" + } + }, + "anyOf": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/definitions/schema" + } + }, + "not": { + "$ref": "#/definitions/schema" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/schema" + }, + "default": {} + }, + "propertyNames": { + "$ref": "#/definitions/schema" + }, + "contains": { + "$ref": "#/definitions/schema" + }, + "discriminator": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "deprecated": { + "type": "boolean", + "default": false + } + } + } + ] + }, + "externalDocs": { + "type": "object", + "additionalProperties": false, + "description": "information about external documentation", + "required": [ + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + } + }, + "channelItem": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "minProperties": 1, + "properties": { + "$ref": { + "$ref": "#/definitions/ReferenceObject" + }, + "parameters": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/parameter" + } + }, + "description": { + "type": "string", + "description": "A description of the channel." + }, + "publish": { + "$ref": "#/definitions/operation" + }, + "subscribe": { + "$ref": "#/definitions/operation" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "bindings": { + "$ref": "#/definitions/bindingsObject" + } + } + }, + "parameter": { + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A brief description of the parameter. This could contain examples of use. GitHub Flavored Markdown is allowed." + }, + "schema": { + "$ref": "#/definitions/schema" + }, + "location": { + "type": "string", + "description": "A runtime expression that specifies the location of the parameter value", + "pattern": "^\\$message\\.(header|payload)\\#(\\/(([^\\/~])|(~[01]))*)*" + }, + "$ref": { + "$ref": "#/definitions/ReferenceObject" + } + } + }, + "operation": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "traits": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/operationTrait" + }, + { + "type": "array", + "items": [ + { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/operationTrait" + } + ] + }, + { + "type": "object", + "additionalItems": true + } + ] + } + ] + } + }, + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string" + }, + "bindings": { + "$ref": "#/definitions/bindingsObject" + }, + "message": { + "$ref": "#/definitions/message" + } + } + }, + "message": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "oneOf": [ + { + "type": "object", + "required": [ + "oneOf" + ], + "additionalProperties": false, + "properties": { + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/message" + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "schemaFormat": { + "type": "string" + }, + "contentType": { + "type": "string" + }, + "headers": { + "$ref": "#/definitions/schema" + }, + "payload": {}, + "correlationId": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/correlationId" + } + ] + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the message." + }, + "name": { + "type": "string", + "description": "Name of the message." + }, + "title": { + "type": "string", + "description": "A human-friendly title for the message." + }, + "description": { + "type": "string", + "description": "A longer description of the message. CommonMark is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": { + "type": "object" + } + }, + "bindings": { + "$ref": "#/definitions/bindingsObject" + }, + "traits": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/messageTrait" + }, + { + "type": "array", + "items": [ + { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/messageTrait" + } + ] + }, + { + "type": "object", + "additionalItems": true + } + ] + } + ] + } + } + } + } + ] + } + ] + }, + "bindingsObject": { + "type": "object", + "additionalProperties": true, + "properties": { + "http": {}, + "ws": {}, + "amqp": {}, + "amqp1": {}, + "mqtt": {}, + "mqtt5": {}, + "kafka": {}, + "nats": {}, + "jms": {}, + "sns": {}, + "sqs": {}, + "stomp": {}, + "redis": {} + } + }, + "correlationId": { + "type": "object", + "required": [ + "location" + ], + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "description": { + "type": "string", + "description": "A optional description of the correlation ID. GitHub Flavored Markdown is allowed." + }, + "location": { + "type": "string", + "description": "A runtime expression that specifies the location of the correlation ID", + "pattern": "^\\$message\\.(header|payload)\\#(\\/(([^\\/~])|(~[01]))*)*" + } + } + }, + "specificationExtension": { + "description": "Any property starting with x- is valid.", + "additionalProperties": true, + "additionalItems": true + }, + "tag": { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + } + }, + "operationTrait": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "summary": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "operationId": { + "type": "string" + }, + "bindings": { + "$ref": "#/definitions/bindingsObject" + } + } + }, + "messageTrait": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "properties": { + "schemaFormat": { + "type": "string" + }, + "contentType": { + "type": "string" + }, + "headers": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/schema" + } + ] + }, + "correlationId": { + "oneOf": [ + { + "$ref": "#/definitions/Reference" + }, + { + "$ref": "#/definitions/correlationId" + } + ] + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/tag" + }, + "uniqueItems": true + }, + "summary": { + "type": "string", + "description": "A brief summary of the message." + }, + "name": { + "type": "string", + "description": "Name of the message." + }, + "title": { + "type": "string", + "description": "A human-friendly title for the message." + }, + "description": { + "type": "string", + "description": "A longer description of the message. CommonMark is allowed." + }, + "externalDocs": { + "$ref": "#/definitions/externalDocs" + }, + "deprecated": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": { + "type": "object" + } + }, + "bindings": { + "$ref": "#/definitions/bindingsObject" + } + } + }, + "SecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/userPassword" + }, + { + "$ref": "#/definitions/apiKey" + }, + { + "$ref": "#/definitions/X509" + }, + { + "$ref": "#/definitions/symmetricEncryption" + }, + { + "$ref": "#/definitions/asymmetricEncryption" + }, + { + "$ref": "#/definitions/HTTPSecurityScheme" + }, + { + "$ref": "#/definitions/oauth2Flows" + }, + { + "$ref": "#/definitions/openIdConnect" + } + ] + }, + "userPassword": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "userPassword" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "apiKey": { + "type": "object", + "required": [ + "type", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey" + ] + }, + "in": { + "type": "string", + "enum": [ + "user", + "password" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "X509": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "X509" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "symmetricEncryption": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "symmetricEncryption" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "asymmetricEncryption": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "asymmetricEncryption" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "HTTPSecurityScheme": { + "oneOf": [ + { + "$ref": "#/definitions/NonBearerHTTPSecurityScheme" + }, + { + "$ref": "#/definitions/BearerHTTPSecurityScheme" + }, + { + "$ref": "#/definitions/APIKeyHTTPSecurityScheme" + } + ] + }, + "NonBearerHTTPSecurityScheme": { + "not": { + "type": "object", + "properties": { + "scheme": { + "type": "string", + "enum": [ + "bearer" + ] + } + } + }, + "type": "object", + "required": [ + "scheme", + "type" + ], + "properties": { + "scheme": { + "type": "string" + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "BearerHTTPSecurityScheme": { + "type": "object", + "required": [ + "type", + "scheme" + ], + "properties": { + "scheme": { + "type": "string", + "enum": [ + "bearer" + ] + }, + "bearerFormat": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "http" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "APIKeyHTTPSecurityScheme": { + "type": "object", + "required": [ + "type", + "name", + "in" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "httpApiKey" + ] + }, + "name": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "cookie" + ] + }, + "description": { + "type": "string" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "oauth2Flows": { + "type": "object", + "required": [ + "type", + "flows" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "description": { + "type": "string" + }, + "flows": { + "type": "object", + "properties": { + "implicit": { + "allOf": [ + { + "$ref": "#/definitions/oauth2Flow" + }, + { + "required": [ + "authorizationUrl", + "scopes" + ] + }, + { + "not": { + "required": [ + "tokenUrl" + ] + } + } + ] + }, + "password": { + "allOf": [ + { + "$ref": "#/definitions/oauth2Flow" + }, + { + "required": [ + "tokenUrl", + "scopes" + ] + }, + { + "not": { + "required": [ + "authorizationUrl" + ] + } + } + ] + }, + "clientCredentials": { + "allOf": [ + { + "$ref": "#/definitions/oauth2Flow" + }, + { + "required": [ + "tokenUrl", + "scopes" + ] + }, + { + "not": { + "required": [ + "authorizationUrl" + ] + } + } + ] + }, + "authorizationCode": { + "allOf": [ + { + "$ref": "#/definitions/oauth2Flow" + }, + { + "required": [ + "authorizationUrl", + "tokenUrl", + "scopes" + ] + } + ] + } + }, + "additionalProperties": false, + "minProperties": 1 + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + } + }, + "oauth2Flow": { + "type": "object", + "properties": { + "authorizationUrl": { + "type": "string", + "format": "uri" + }, + "tokenUrl": { + "type": "string", + "format": "uri" + }, + "refreshUrl": { + "type": "string", + "format": "uri" + }, + "scopes": { + "$ref": "#/definitions/oauth2Scopes" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "oauth2Scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "openIdConnect": { + "type": "object", + "required": [ + "type", + "openIdConnectUrl" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "openIdConnect" + ] + }, + "description": { + "type": "string" + }, + "openIdConnectUrl": { + "type": "string", + "format": "uri" + } + }, + "patternProperties": { + "^x-[\\w\\d\\.\\-\\_]+$": { + "$ref": "#/definitions/specificationExtension" + } + }, + "additionalProperties": false + }, + "SecurityRequirement": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + } + } +} diff --git a/src/rulesets/reader.ts b/src/rulesets/reader.ts index 166a58526d..67201f7ee5 100644 --- a/src/rulesets/reader.ts +++ b/src/rulesets/reader.ts @@ -1,5 +1,5 @@ import { Cache } from '@stoplight/json-ref-resolver'; -import { ICache } from '@stoplight/json-ref-resolver/types'; +import { ICache, IUriParserResult } from '@stoplight/json-ref-resolver/types'; import { join } from '@stoplight/path'; import { Optional } from '@stoplight/types'; import { parse } from '@stoplight/yaml'; @@ -65,15 +65,17 @@ const createRulesetProcessor = ( dereferenceInline: false, uriCache, async parseResolveResult(opts) { - try { - opts.result = parse(opts.result); - } catch { - // happens - } - return opts; + return new Promise(resolve => { + try { + resolve({ result: parse(opts.result) }); + } catch (e) { + resolve({ error: e }); + } + }); }, }, ); + const ruleset = assertValidRuleset(JSON.parse(JSON.stringify(result))); const rules = {}; const functions = {}; diff --git a/test-harness/scenarios/aas2-streetlights.scenario b/test-harness/scenarios/aas2-streetlights.scenario new file mode 100644 index 0000000000..b8c7df402d --- /dev/null +++ b/test-harness/scenarios/aas2-streetlights.scenario @@ -0,0 +1,221 @@ +====test==== +Validate streetlights.yaml AsyncAPI 2.0 sample +====document==== +asyncapi: '2.0.0' +info: + title: Streetlights API + version: '1.0.0' + description: | + The Smartylighting Streetlights API allows you to remotely manage the city lights. + + ### Check out its awesome features: + + * Turn a specific streetlight on/off 🌃 + * Dim a specific streetlight 😎 + * Receive real-time information about environmental lighting conditions 📈 + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + +servers: + production: + url: test.mosquitto.org:{port} + protocol: mqtt + description: Test broker + variables: + port: + description: Secure connection (TLS) is available through port 8883. + default: '1883' + enum: + - '1883' + - '8883' + security: + - apiKey: [] + - supportedOauthFlows: + - streetlights:on + - streetlights:off + - streetlights:dim + - openIdConnectWellKnown: [] + +defaultContentType: application/json + +channels: + smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured: + description: The topic on which measured values may be produced and consumed. + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + publish: + summary: Inform about environmental lighting conditions of a particular streetlight. + operationId: receiveLightMeasurement + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/lightMeasured' + + smartylighting/streetlights/1/0/action/{streetlightId}/turn/on: + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + subscribe: + operationId: turnOn + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/turnOnOff' + + smartylighting/streetlights/1/0/action/{streetlightId}/turn/off: + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + subscribe: + operationId: turnOff + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/turnOnOff' + + smartylighting/streetlights/1/0/action/{streetlightId}/dim: + parameters: + streetlightId: + $ref: '#/components/parameters/streetlightId' + subscribe: + operationId: dimLight + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/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: + lightMeasuredPayload: + type: object + properties: + 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: + apiKey: + type: apiKey + in: user + description: Provide your API key as the user and leave the password empty. + supportedOauthFlows: + type: oauth2 + description: Flows to support OAuth 2.0 + flows: + implicit: + authorizationUrl: 'https://authserver.example/auth' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + password: + tokenUrl: 'https://authserver.example/token' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + clientCredentials: + tokenUrl: 'https://authserver.example/token' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + authorizationCode: + authorizationUrl: 'https://authserver.example/auth' + tokenUrl: 'https://authserver.example/token' + refreshUrl: 'https://authserver.example/refresh' + scopes: + 'streetlights:on': Ability to switch lights on + 'streetlights:off': Ability to switch lights off + 'streetlights:dim': Ability to dim the lights + openIdConnectWellKnown: + type: openIdConnect + openIdConnectUrl: 'https://authserver.example/.well-known' + + parameters: + streetlightId: + description: The ID of the streetlight. + schema: + type: string + + messageTraits: + commonHeaders: + headers: + type: object + properties: + my-app-header: + type: integer + minimum: 0 + maximum: 100 + + operationTraits: + kafka: + bindings: + kafka: + clientId: my-app-id +====command==== +{bin} lint {document} +====stdout==== +AsyncAPI 2.x detected + +{document} + 2:6 warning asyncapi-info-contact Info object should contain `contact` object. + +✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints) diff --git a/test-harness/scenarios/rules-matching-multiple-places.scenario b/test-harness/scenarios/rules-matching-multiple-places.scenario index 6d26fe3c04..9d699125a1 100644 --- a/test-harness/scenarios/rules-matching-multiple-places.scenario +++ b/test-harness/scenarios/rules-matching-multiple-places.scenario @@ -33,7 +33,7 @@ rules: 1 ====stdout==== {document} - 1:1 warning unrecognized-format The provided document does not match any of the registered formats [oas2, oas3, json-schema, json-schema-loose, json-schema-draft4, json-schema-draft6, json-schema-draft7, json-schema-2019-09] + 1:1 warning unrecognized-format The provided document does not match any of the registered formats [oas2, oas3, aas2, json-schema, json-schema-loose, json-schema-draft4, json-schema-draft6, json-schema-draft7, json-schema-2019-09] 6:15 error valid-user-properties must match the pattern '/^string$/' 10:15 error valid-user-properties must match the pattern '/^string$/' 11:11 error require-user-and-address `address` property is not truthy diff --git a/test-harness/scenarios/unrecognized-format.scenario b/test-harness/scenarios/unrecognized-format.scenario index 9c97b0ff76..99b7a28b36 100644 --- a/test-harness/scenarios/unrecognized-format.scenario +++ b/test-harness/scenarios/unrecognized-format.scenario @@ -7,6 +7,6 @@ info: {} {bin} lint {document} ====stdout==== {document} - 1:1 warning unrecognized-format The provided document does not match any of the registered formats [oas2, oas3, json-schema, json-schema-loose, json-schema-draft4, json-schema-draft6, json-schema-draft7, json-schema-2019-09] + 1:1 warning unrecognized-format The provided document does not match any of the registered formats [oas2, oas3, aas2, json-schema, json-schema-loose, json-schema-draft4, json-schema-draft6, json-schema-draft7, json-schema-2019-09] ✖ 1 problem (0 errors, 1 warning, 0 infos, 0 hints)