Skip to content

Commit fe51a04

Browse files
authored
feat: add Spectral rule to validate operation messages (#911)
* 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
1 parent 4345060 commit fe51a04

File tree

7 files changed

+402
-1
lines changed

7 files changed

+402
-1
lines changed

src/ruleset/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { coreRuleset, recommendedRuleset } from './ruleset';
22
import { v2CoreRuleset, v2SchemasRuleset, v2RecommendedRuleset } from './v2';
3+
import { v3CoreRuleset } from './v3';
34

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createRulesetFunction } from '@stoplight/spectral-core';
2+
import type { IFunctionResult } from '@stoplight/spectral-core';
3+
import { SchemaDefinition } from '@stoplight/spectral-core/dist/ruleset/function';
4+
5+
const referenceSchema: SchemaDefinition = {
6+
type: 'object',
7+
properties: {
8+
$ref: {
9+
type: 'string',
10+
format: 'uri-reference'
11+
},
12+
},
13+
};
14+
15+
export const operationMessagesUnambiguity = createRulesetFunction<{ channel?: {'$ref': string}; messages?: [{'$ref': string}] }, null>(
16+
{
17+
input: {
18+
type: 'object',
19+
properties: {
20+
channel: referenceSchema,
21+
messages: {
22+
type: 'array',
23+
items: referenceSchema,
24+
},
25+
},
26+
},
27+
options: null,
28+
},
29+
(targetVal, _, ctx) => {
30+
const results: IFunctionResult[] = [];
31+
const channelPointer = targetVal.channel?.$ref as string; // required
32+
33+
targetVal.messages?.forEach((message, index) => {
34+
if (!message.$ref.startsWith(`${channelPointer}/messages`)) {
35+
results.push({
36+
message: 'Operation message does not belong to the specified channel.',
37+
path: [...ctx.path, 'messages', index],
38+
});
39+
}
40+
});
41+
42+
return results;
43+
},
44+
);

src/ruleset/v3/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './ruleset';

src/ruleset/v3/ruleset.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* eslint-disable sonarjs/no-duplicate-string */
2+
3+
import { AsyncAPIFormats } from '../formats';
4+
import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity';
5+
6+
export const v3CoreRuleset = {
7+
description: 'Core AsyncAPI 3.x.x ruleset.',
8+
formats: AsyncAPIFormats.filterByMajorVersions(['3']).formats(),
9+
rules: {
10+
/**
11+
* Operation Object rules
12+
*/
13+
'asyncapi3-operation-messages-from-referred-channel': {
14+
description: 'Operation "messages" must be a subset of the messages defined in the channel referenced in this operation.',
15+
message: '{{error}}',
16+
severity: 'error',
17+
recommended: true,
18+
resolved: false, // We use the JSON pointer to match the channel.
19+
given: [
20+
'$.operations.*',
21+
'$.components.operations.*',
22+
],
23+
then: {
24+
function: operationMessagesUnambiguity,
25+
},
26+
},
27+
},
28+
};

test/custom-operations/parse-schema-v3.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('custom operations for v3 - parse schemas', function() {
2626
},
2727
messages: [
2828
{
29-
$ref: '#/components/messages/message'
29+
$ref: '#/channels/channel/messages/message'
3030
}
3131
]
3232
}

0 commit comments

Comments
 (0)