From 26eda0c23251fdab83793edcd94a2e1083f041ba Mon Sep 17 00:00:00 2001 From: Chris Patmore Date: Thu, 15 Jun 2023 14:06:05 +0100 Subject: [PATCH] refactor: only run new rule if schemaFormat is set switch the new rule to only run of the schemaFormat is set in the message. Also adjust the tests to better align with this separation. Signed-off-by: Chris Patmore --- .../messageExamples-spectral-rule-v2.ts | 43 +- src/ruleset/v2/ruleset.ts | 4 +- ...capi2-message-examples-spectral-v2.spec.ts | 385 +++++------------- .../v2/asyncapi2-message-examples.spec.ts | 4 +- 4 files changed, 113 insertions(+), 323 deletions(-) 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} }, ], },