Skip to content

Commit

Permalink
feat: add Spectral rule to validate operation messages (#911)
Browse files Browse the repository at this point in the history
* feat: add Spectral rule to validate operation messages

* fix test example

* be more precise when matching the json pointer

* improve test by testing multiple messages in operation
  • Loading branch information
smoya authored Nov 27, 2023
1 parent 4345060 commit fe51a04
Show file tree
Hide file tree
Showing 7 changed files with 402 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/ruleset/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { coreRuleset, recommendedRuleset } from './ruleset';
import { v2CoreRuleset, v2SchemasRuleset, v2RecommendedRuleset } from './v2';
import { v3CoreRuleset } from './v3';

import type { Parser } from '../parser';
import type { RulesetDefinition } from '@stoplight/spectral-core';
Expand All @@ -18,6 +19,7 @@ export function createRuleset(parser: Parser, options?: RulesetOptions): Ruleset
useCore && v2CoreRuleset,
useCore && v2SchemasRuleset(parser),
useRecommended && v2RecommendedRuleset,
useCore && v3CoreRuleset,
...(options as any || {})?.extends || [],
].filter(Boolean);

Expand Down
44 changes: 44 additions & 0 deletions src/ruleset/v3/functions/operationMessagesUnambiguity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createRulesetFunction } from '@stoplight/spectral-core';
import type { IFunctionResult } from '@stoplight/spectral-core';
import { SchemaDefinition } from '@stoplight/spectral-core/dist/ruleset/function';

const referenceSchema: SchemaDefinition = {
type: 'object',
properties: {
$ref: {
type: 'string',
format: 'uri-reference'
},
},
};

export const operationMessagesUnambiguity = createRulesetFunction<{ channel?: {'$ref': string}; messages?: [{'$ref': string}] }, null>(
{
input: {
type: 'object',
properties: {
channel: referenceSchema,
messages: {
type: 'array',
items: referenceSchema,
},
},
},
options: null,
},
(targetVal, _, ctx) => {
const results: IFunctionResult[] = [];
const channelPointer = targetVal.channel?.$ref as string; // required

targetVal.messages?.forEach((message, index) => {
if (!message.$ref.startsWith(`${channelPointer}/messages`)) {
results.push({
message: 'Operation message does not belong to the specified channel.',
path: [...ctx.path, 'messages', index],
});
}
});

return results;
},
);
1 change: 1 addition & 0 deletions src/ruleset/v3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ruleset';
28 changes: 28 additions & 0 deletions src/ruleset/v3/ruleset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable sonarjs/no-duplicate-string */

import { AsyncAPIFormats } from '../formats';
import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity';

export const v3CoreRuleset = {
description: 'Core AsyncAPI 3.x.x ruleset.',
formats: AsyncAPIFormats.filterByMajorVersions(['3']).formats(),
rules: {
/**
* Operation Object rules
*/
'asyncapi3-operation-messages-from-referred-channel': {
description: 'Operation "messages" must be a subset of the messages defined in the channel referenced in this operation.',
message: '{{error}}',
severity: 'error',
recommended: true,
resolved: false, // We use the JSON pointer to match the channel.
given: [
'$.operations.*',
'$.components.operations.*',
],
then: {
function: operationMessagesUnambiguity,
},
},
},
};
2 changes: 1 addition & 1 deletion test/custom-operations/parse-schema-v3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('custom operations for v3 - parse schemas', function() {
},
messages: [
{
$ref: '#/components/messages/message'
$ref: '#/channels/channel/messages/message'
}
]
}
Expand Down
Loading

0 comments on commit fe51a04

Please sign in to comment.