diff --git a/src/ruleset/v2/functions/messageExamples-spectral-rule-v2.ts b/src/ruleset/v2/functions/messageExamples-spectral-rule-v2.ts index aae9c1ce6..f6d47fce5 100644 --- a/src/ruleset/v2/functions/messageExamples-spectral-rule-v2.ts +++ b/src/ruleset/v2/functions/messageExamples-spectral-rule-v2.ts @@ -12,24 +12,24 @@ import { createDetailedAsyncAPI } from '../../../utils'; export function asyncApi2MessageExamplesParserRule(parser: Parser): RuleDefinition { return { - description: 'Examples of message object should validate againt the "payload" and "headers" schemas.', + description: 'Examples of message object should validate against the "payload" and "headers" schemas.', message: '{{error}}', severity: 'error', recommended: true, given: [ // messages - '$.channels.*.[publish,subscribe].message', - '$.channels.*.[publish,subscribe].message.oneOf.*', - '$.components.channels.*.[publish,subscribe].message', - '$.components.channels.*.[publish,subscribe].message.oneOf.*', - '$.components.messages.*', + '$.channels.*.[publish,subscribe][?(@property === \'message\' && @.schemaFormat !== void 0)]', + '$.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat !== void 0)]', + '$.components.channels.*.[publish,subscribe].message[?(@property === \'message\' && @.schemaFormat !== void 0)]', + '$.components.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat !== void 0)]', + '$.components.messages[?(!@null && @.schemaFormat !== void 0)]', // message traits - '$.channels.*.[publish,subscribe].message.traits.*', - '$.channels.*.[publish,subscribe].message.oneOf.*.traits.*', - '$.components.channels.*.[publish,subscribe].message.traits.*', - '$.components.channels.*.[publish,subscribe].message.oneOf.*.traits.*', - '$.components.messages.*.traits.*', - '$.components.messageTraits.*', + '$.channels.*.[publish,subscribe].message.traits[?(!@null && @.schemaFormat !== void 0)]', + '$.channels.*.[publish,subscribe].message.oneOf.*.traits[?(!@null && @.schemaFormat !== void 0)]', + '$.components.channels.*.[publish,subscribe].message.traits[?(!@null && @.schemaFormat !== void 0)]', + '$.components.channels.*.[publish,subscribe].message.oneOf.*.traits[?(!@null && @.schemaFormat !== void 0)]', + '$.components.messages.*.traits[?(!@null && @.schemaFormat !== void 0)]', + '$.components.messageTraits[?(!@null && @.schemaFormat !== void 0)]', ], then: { function: rulesetFunction(parser), @@ -121,7 +121,7 @@ async function parseExampleSchema(parser: Parser, schema: unknown, type: 'payloa try { const parseSchemaInput: ParseSchemaInput = { asyncapi: input.asyncapi, - data: serializeSchema(schema, type), + data: schema, meta: {}, path, schemaFormat: input.schemaFormat, @@ -138,23 +138,6 @@ async function parseExampleSchema(parser: Parser, schema: unknown, type: 'payloa } } -function serializeSchema(schema: unknown, type: 'payload' | 'headers'): any { - if (!schema && typeof schema !== 'boolean') { // if schema is falsy then - if (type === 'headers') { // object for headers - schema = { type: 'object' }; - } else { // anything for payload - schema = {}; - } - } else if (typeof schema === 'boolean') { // spectral cannot handle boolean schemas - if (schema === true) { - schema = {}; // everything - } else { - schema = { not: {} }; // nothing - } - } - return schema; -} - function getMessageExamples(message: v2.MessageObject): Array<{ path: JsonPath; value: v2.MessageExampleObject }> { if (!Array.isArray(message.examples)) { return []; diff --git a/src/ruleset/v2/ruleset.ts b/src/ruleset/v2/ruleset.ts index 5b53e2ea7..f125f55fa 100644 --- a/src/ruleset/v2/ruleset.ts +++ b/src/ruleset/v2/ruleset.ts @@ -123,9 +123,9 @@ export const v2CoreRuleset = { recommended: true, given: [ // messages - '$.channels.*.[publish,subscribe].[?(@property === \'message\' && @.schemaFormat === void 0)]', + '$.channels.*.[publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)]', '$.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat === void 0)]', - '$.components.channels.*.[publish,subscribe].[?(@property === \'message\' && @.schemaFormat === void 0)]', + '$.components.channels.*.[publish,subscribe][?(@property === \'message\' && @.schemaFormat === void 0)]', '$.components.channels.*.[publish,subscribe].message.oneOf[?(!@null && @.schemaFormat === void 0)]', '$.components.messages[?(!@null && @.schemaFormat === void 0)]', // message traits diff --git a/test/ruleset/rules/v2/asyncapi2-message-examples-spectral-v2.spec.ts b/test/ruleset/rules/v2/asyncapi2-message-examples-spectral-v2.spec.ts index 0be029ae0..5e615903e 100644 --- a/test/ruleset/rules/v2/asyncapi2-message-examples-spectral-v2.spec.ts +++ b/test/ruleset/rules/v2/asyncapi2-message-examples-spectral-v2.spec.ts @@ -1,146 +1,7 @@ import { testRule, DiagnosticSeverity } from '../../tester'; testRule('asyncapi2-message-examples-custom-format', [ - { - name: 'valid case', - document: { - asyncapi: '2.0.0', - channels: { - someChannel: { - publish: { - message: { - payload: { - type: 'string', - }, - headers: { - type: 'object', - }, - examples: [ - { - payload: 'foobar', - headers: { - someKey: 'someValue', - }, - }, - ], - }, - }, - }, - }, - }, - errors: [], - }, - - { - name: 'invalid case', - document: { - asyncapi: '2.0.0', - channels: { - someChannel: { - publish: { - message: { - payload: { - type: 'string', - }, - headers: { - type: 'object', - }, - examples: [ - { - payload: 2137, - headers: { - someKey: 'someValue', - }, - }, - ], - }, - }, - }, - }, - }, - errors: [ - { - message: '"payload" property type must be string', - path: ['channels', 'someChannel', 'publish', 'message', 'examples', '0', 'payload'], - severity: DiagnosticSeverity.Error, - }, - ], - }, - - { - name: 'invalid case (oneOf case)', - document: { - asyncapi: '2.0.0', - channels: { - someChannel: { - publish: { - message: { - oneOf: [ - { - payload: { - type: 'string', - }, - headers: { - type: 'object', - }, - examples: [ - { - payload: 2137, - headers: { - someKey: 'someValue', - }, - }, - ], - }, - ], - }, - }, - }, - }, - }, - errors: [ - { - message: '"payload" property type must be string', - path: ['channels', 'someChannel', 'publish', 'message', 'oneOf', '0', 'examples', '0', 'payload'], - severity: DiagnosticSeverity.Error, - }, - ], - }, - - { - name: 'invalid case (inside components.messages)', - document: { - asyncapi: '2.0.0', - components: { - messages: { - someMessage: { - payload: { - type: 'string', - }, - headers: { - type: 'object', - }, - examples: [ - { - payload: 2137, - headers: { - someKey: 'someValue', - }, - }, - ], - }, - }, - }, - }, - errors: [ - { - message: '"payload" property type must be string', - path: ['components', 'messages', 'someMessage', 'examples', '0', 'payload'], - severity: DiagnosticSeverity.Error, - }, - ], - }, - + { name: 'invalid case (with multiple errors)', document: { @@ -175,159 +36,10 @@ testRule('asyncapi2-message-examples-custom-format', [ }, }, }, - errors: [ - { - message: '"payload" property must have required property "key2"', - path: ['components', 'messages', 'someMessage', 'examples', '0', 'payload'], - severity: DiagnosticSeverity.Error, - }, - { - message: '"key1" property type must be string', - path: ['components', 'messages', 'someMessage', 'examples', '0', 'payload', 'key1'], - severity: DiagnosticSeverity.Error, - }, - { - message: '"headers" property type must be object', - path: ['components', 'messages', 'someMessage', 'examples', '0', 'headers'], - severity: DiagnosticSeverity.Error, - }, - ], - }, - - { - name: 'case with omitted payload', - document: { - asyncapi: '2.0.0', - components: { - messages: { - someMessage: { - examples: [ - { - payload: { - key1: 2137, - }, - }, - ], - }, - }, - }, - }, - errors: [], - }, - - { - name: 'case with falsy payload (valid case)', - document: { - asyncapi: '2.0.0', - components: { - messages: { - someMessage: { - payload: false, - examples: [ - {}, - ], - }, - }, - }, - }, - errors: [], - }, - - { - name: 'case with falsy payload (invalid case)', - document: { - asyncapi: '2.0.0', - components: { - messages: { - someMessage: { - payload: false, - examples: [ - { - payload: { - key: '2137' - } - }, - ], - }, - }, - }, - }, - errors: [ - { - message: '"payload" property must not be valid', - path: ['components', 'messages', 'someMessage', 'examples', '0', 'payload'], - severity: DiagnosticSeverity.Error, - }, - ], - }, - - { - name: 'case with omitted headers', - document: { - asyncapi: '2.0.0', - components: { - messages: { - someMessage: { - examples: [ - { - headers: { - key1: 2137, - }, - }, - ], - }, - }, - }, - }, + // no errors as this rule is just checking examples where schemaFormat is set errors: [], }, - { - name: 'case with falsy headers (valid case)', - document: { - asyncapi: '2.0.0', - components: { - messages: { - someMessage: { - headers: false, - examples: [ - {}, - ], - }, - }, - }, - }, - errors: [], - }, - - { - name: 'case with falsy headers (invalid case)', - document: { - asyncapi: '2.0.0', - components: { - messages: { - someMessage: { - headers: false, - examples: [ - { - headers: { - key: '2137' - } - }, - ], - }, - }, - }, - }, - errors: [ - { - message: '"headers" property must not be valid', - path: ['components', 'messages', 'someMessage', 'examples', '0', 'headers'], - severity: DiagnosticSeverity.Error, - }, - ], - }, - { name: 'valid avro spec case', document: { @@ -428,4 +140,97 @@ testRule('asyncapi2-message-examples-custom-format', [ }, ], }, + + { + name: 'avro can contain null values', + document: { + asyncapi: '2.6.0', + channels: { + someChannel: { + publish: { + message: { + schemaFormat: 'application/vnd.apache.avro;version=1.9.0', + payload: { + type: 'record', + name: 'Command', + fields: [{ + name: 'foo', + default: null, + type: ['null', 'string'], + }], + }, + examples: [ + { + payload: {} + }, + ], + }, + }, + }, + }, + }, + errors: [], + }, + + { + name: 'handles oneOf processing', + document: { + asyncapi: '2.6.0', + channels: { + someChannel: { + publish: { + message: { + oneOf: [ + { + schemaFormat: 'application/vnd.apache.avro;version=1.9.0', + payload: { + type: 'record', + name: 'Command', + fields: [{ + name: 'foo', + default: null, + type: ['null', 'string'], + }], + }, + examples: [ + { + payload: {foo: 1} + }, + ], + }, + { + payload: { + type: 'string' + }, + examples: [ + { + // no error for this as this rule is just checking examples where schemaFormat is set + payload: 1 + }, + ], + }, + ], + }, + }, + }, + }, + }, + errors: [ + { + message: '"foo" property type must be string', + path: ['channels', 'someChannel', 'publish', 'message', 'oneOf', '0', 'examples', '0', 'payload', 'foo'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"foo" property type must be null', + path: ['channels', 'someChannel', 'publish', 'message', 'oneOf', '0', 'examples', '0', 'payload', 'foo'], + severity: DiagnosticSeverity.Error, + }, + { + message: '"foo" property must match exactly one schema in oneOf', + path: ['channels', 'someChannel', 'publish', 'message', 'oneOf', '0', 'examples', '0', 'payload', 'foo'], + severity: DiagnosticSeverity.Error, + } + ], + }, ]); diff --git a/test/ruleset/rules/v2/asyncapi2-message-examples.spec.ts b/test/ruleset/rules/v2/asyncapi2-message-examples.spec.ts index c4cb68767..ec07b4f2a 100644 --- a/test/ruleset/rules/v2/asyncapi2-message-examples.spec.ts +++ b/test/ruleset/rules/v2/asyncapi2-message-examples.spec.ts @@ -415,6 +415,7 @@ testRule('asyncapi2-message-examples', [ }, }, }, + // no errors as this rule is just checking examples where schemaFormat is set errors: [], }, @@ -471,7 +472,8 @@ testRule('asyncapi2-message-examples', [ }, examples: [ { - payload: {} + // no error for this as this rule is just checking examples where schemaFormat is not set + payload: {foo: 1} }, ], },