Skip to content

Commit e6cbd74

Browse files
authored
feat: add Spectral rules to validate required operation channe and channel servers field (#913)
1 parent c3cb73c commit e6cbd74

3 files changed

+504
-0
lines changed

src/ruleset/v3/ruleset.ts

+33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { AsyncAPIFormats } from '../formats';
44
import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity';
5+
import { pattern } from '@stoplight/spectral-functions';
56

67
export const v3CoreRuleset = {
78
description: 'Core AsyncAPI 3.x.x ruleset.',
@@ -24,5 +25,37 @@ export const v3CoreRuleset = {
2425
function: operationMessagesUnambiguity,
2526
},
2627
},
28+
'asyncapi3-required-operation-channel-unambiguity': {
29+
description: 'The "channel" field of an operation under the root "operations" object must always reference a channel under the root "channels" object.',
30+
severity: 'error',
31+
recommended: true,
32+
resolved: false, // We use the JSON pointer to match the channel.
33+
given: '$.operations.*',
34+
then: {
35+
field: 'channel.$ref',
36+
function: pattern,
37+
functionOptions: {
38+
match: '#\\/channels\\/', // If doesn't match, rule fails.
39+
},
40+
},
41+
},
42+
43+
/**
44+
* Channel Object rules
45+
*/
46+
'asyncapi3-required-channel-servers-unambiguity': {
47+
description: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
48+
severity: 'error',
49+
recommended: true,
50+
resolved: false, // We use the JSON pointer to match the channel.
51+
given: '$.channels.*',
52+
then: {
53+
field: '$.servers.*.$ref',
54+
function: pattern,
55+
functionOptions: {
56+
match: '#\\/servers\\/', // If doesn't match, rule fails.
57+
},
58+
},
59+
}
2760
},
2861
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import { testRule, DiagnosticSeverity } from '../../tester';
2+
3+
testRule('asyncapi3-required-channel-servers-unambiguity', [
4+
{
5+
name: 'valid case - required channel (under root) server field points to a subset of required servers (under root)',
6+
document: {
7+
asyncapi: '3.0.0',
8+
info: {
9+
title: 'Account Service',
10+
version: '1.0.0'
11+
},
12+
servers: {
13+
prod: {
14+
host: 'my-api.com',
15+
protocol: 'ws',
16+
},
17+
dev: {
18+
host: 'localhost',
19+
protocol: 'ws',
20+
},
21+
},
22+
channels: {
23+
UserSignedUp: {
24+
servers: [
25+
{ $ref: '#/servers/prod' },
26+
{ $ref: '#/servers/dev' },
27+
]
28+
}
29+
},
30+
},
31+
errors: [],
32+
},
33+
{
34+
name: 'valid case - required channel (under root) server field points to a subset of required servers (under root) from an external doc',
35+
document: {
36+
asyncapi: '3.0.0',
37+
info: {
38+
title: 'Account Service',
39+
version: '1.0.0'
40+
},
41+
channels: {
42+
UserSignedUp: {
43+
servers: [
44+
{ $ref: 'http://foo.bar/components/file.yml#/servers/prod' },
45+
{ $ref: 'http://foo.bar/components/file.yml#/servers/dev' },
46+
]
47+
}
48+
},
49+
},
50+
errors: [],
51+
},
52+
{
53+
name: 'valid case - optional channel (under components) server field points to a subset of required servers (under root)',
54+
document: {
55+
asyncapi: '3.0.0',
56+
info: {
57+
title: 'Account Service',
58+
version: '1.0.0'
59+
},
60+
servers: {
61+
prod: {
62+
host: 'my-api.com',
63+
protocol: 'ws',
64+
},
65+
dev: {
66+
host: 'localhost',
67+
protocol: 'ws',
68+
},
69+
},
70+
components: {
71+
channels: {
72+
UserSignedUp: {
73+
servers: [
74+
{ $ref: '#/servers/prod' },
75+
{ $ref: '#/servers/dev' },
76+
]
77+
}
78+
},
79+
},
80+
},
81+
errors: [],
82+
},
83+
{
84+
name: 'valid case - optional channel (under components) server field points to a subset of optional servers (under components)',
85+
document: {
86+
asyncapi: '3.0.0',
87+
info: {
88+
title: 'Account Service',
89+
version: '1.0.0'
90+
},
91+
components: {
92+
servers: {
93+
prod: {
94+
host: 'my-api.com',
95+
protocol: 'ws',
96+
},
97+
dev: {
98+
host: 'localhost',
99+
protocol: 'ws',
100+
},
101+
},
102+
channels: {
103+
UserSignedUp: {
104+
messages: {
105+
UserSignedUp: {
106+
payload: {
107+
type: 'object',
108+
properties: {
109+
displayName: {
110+
type: 'string'
111+
},
112+
email: {
113+
type: 'string'
114+
}
115+
}
116+
}
117+
}
118+
}
119+
}
120+
},
121+
operations: {
122+
UserSignedUp: {
123+
action: 'send',
124+
channel: {
125+
$ref: '#/components/channels/UserSignedUp'
126+
},
127+
messages: [
128+
{
129+
$ref: '#/components/channels/UserSignedUp/messages/UserSignedUp'
130+
}
131+
]
132+
}
133+
}
134+
}
135+
},
136+
errors: [],
137+
},
138+
{
139+
name: 'invalid case - required channel (in root) servers field points to a subset of optional servers (under components)',
140+
document: {
141+
asyncapi: '3.0.0',
142+
info: {
143+
title: 'Account Service',
144+
version: '1.0.0'
145+
},
146+
channels: {
147+
UserSignedUp: {
148+
servers: [
149+
{ $ref: '#/components/servers/prod' },
150+
{ $ref: '#/components/servers/dev' },
151+
]
152+
}
153+
},
154+
components: {
155+
servers: {
156+
prod: {
157+
host: 'my-api.com',
158+
protocol: 'ws',
159+
},
160+
dev: {
161+
host: 'localhost',
162+
protocol: 'ws',
163+
},
164+
}
165+
}
166+
},
167+
errors: [
168+
{
169+
message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
170+
path: ['channels', 'UserSignedUp', 'servers', '0', '$ref'],
171+
severity: DiagnosticSeverity.Error,
172+
},
173+
{
174+
message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
175+
path: ['channels', 'UserSignedUp', 'servers', '1', '$ref'],
176+
severity: DiagnosticSeverity.Error,
177+
}
178+
],
179+
},
180+
{
181+
name: 'invalid case - required channel (in root) servers field points to a subset of optional servers (under components) from an external doc',
182+
document: {
183+
asyncapi: '3.0.0',
184+
info: {
185+
title: 'Account Service',
186+
version: '1.0.0'
187+
},
188+
channels: {
189+
UserSignedUp: {
190+
servers: [
191+
{ $ref: 'http://foo.bar/components/file.yml#/components/servers/prod' },
192+
{ $ref: 'http://foo.bar/components/file.yml#/components/servers/dev' },
193+
]
194+
}
195+
}
196+
},
197+
errors: [
198+
{
199+
message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
200+
path: ['channels', 'UserSignedUp', 'servers', '0', '$ref'],
201+
severity: DiagnosticSeverity.Error,
202+
},
203+
{
204+
message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
205+
path: ['channels', 'UserSignedUp', 'servers', '1', '$ref'],
206+
severity: DiagnosticSeverity.Error,
207+
}
208+
],
209+
},
210+
]);

0 commit comments

Comments
 (0)