Skip to content

Commit

Permalink
feat: add Spectral rules to validate required operation channe and ch…
Browse files Browse the repository at this point in the history
…annel servers field (#913)
  • Loading branch information
smoya authored Nov 28, 2023
1 parent c3cb73c commit e6cbd74
Show file tree
Hide file tree
Showing 3 changed files with 504 additions and 0 deletions.
33 changes: 33 additions & 0 deletions src/ruleset/v3/ruleset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { AsyncAPIFormats } from '../formats';
import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity';
import { pattern } from '@stoplight/spectral-functions';

export const v3CoreRuleset = {
description: 'Core AsyncAPI 3.x.x ruleset.',
Expand All @@ -24,5 +25,37 @@ export const v3CoreRuleset = {
function: operationMessagesUnambiguity,
},
},
'asyncapi3-required-operation-channel-unambiguity': {
description: 'The "channel" field of an operation under the root "operations" object must always reference a channel under the root "channels" object.',
severity: 'error',
recommended: true,
resolved: false, // We use the JSON pointer to match the channel.
given: '$.operations.*',
then: {
field: 'channel.$ref',
function: pattern,
functionOptions: {
match: '#\\/channels\\/', // If doesn't match, rule fails.
},
},
},

/**
* Channel Object rules
*/
'asyncapi3-required-channel-servers-unambiguity': {
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.',
severity: 'error',
recommended: true,
resolved: false, // We use the JSON pointer to match the channel.
given: '$.channels.*',
then: {
field: '$.servers.*.$ref',
function: pattern,
functionOptions: {
match: '#\\/servers\\/', // If doesn't match, rule fails.
},
},
}
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { testRule, DiagnosticSeverity } from '../../tester';

testRule('asyncapi3-required-channel-servers-unambiguity', [
{
name: 'valid case - required channel (under root) server field points to a subset of required servers (under root)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
servers: {
prod: {
host: 'my-api.com',
protocol: 'ws',
},
dev: {
host: 'localhost',
protocol: 'ws',
},
},
channels: {
UserSignedUp: {
servers: [
{ $ref: '#/servers/prod' },
{ $ref: '#/servers/dev' },
]
}
},
},
errors: [],
},
{
name: 'valid case - required channel (under root) server field points to a subset of required servers (under root) from an external doc',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
channels: {
UserSignedUp: {
servers: [
{ $ref: 'http://foo.bar/components/file.yml#/servers/prod' },
{ $ref: 'http://foo.bar/components/file.yml#/servers/dev' },
]
}
},
},
errors: [],
},
{
name: 'valid case - optional channel (under components) server field points to a subset of required servers (under root)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
servers: {
prod: {
host: 'my-api.com',
protocol: 'ws',
},
dev: {
host: 'localhost',
protocol: 'ws',
},
},
components: {
channels: {
UserSignedUp: {
servers: [
{ $ref: '#/servers/prod' },
{ $ref: '#/servers/dev' },
]
}
},
},
},
errors: [],
},
{
name: 'valid case - optional channel (under components) server field points to a subset of optional servers (under components)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
components: {
servers: {
prod: {
host: 'my-api.com',
protocol: 'ws',
},
dev: {
host: 'localhost',
protocol: 'ws',
},
},
channels: {
UserSignedUp: {
messages: {
UserSignedUp: {
payload: {
type: 'object',
properties: {
displayName: {
type: 'string'
},
email: {
type: 'string'
}
}
}
}
}
}
},
operations: {
UserSignedUp: {
action: 'send',
channel: {
$ref: '#/components/channels/UserSignedUp'
},
messages: [
{
$ref: '#/components/channels/UserSignedUp/messages/UserSignedUp'
}
]
}
}
}
},
errors: [],
},
{
name: 'invalid case - required channel (in root) servers field points to a subset of optional servers (under components)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
channels: {
UserSignedUp: {
servers: [
{ $ref: '#/components/servers/prod' },
{ $ref: '#/components/servers/dev' },
]
}
},
components: {
servers: {
prod: {
host: 'my-api.com',
protocol: 'ws',
},
dev: {
host: 'localhost',
protocol: 'ws',
},
}
}
},
errors: [
{
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.',
path: ['channels', 'UserSignedUp', 'servers', '0', '$ref'],
severity: DiagnosticSeverity.Error,
},
{
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.',
path: ['channels', 'UserSignedUp', 'servers', '1', '$ref'],
severity: DiagnosticSeverity.Error,
}
],
},
{
name: 'invalid case - required channel (in root) servers field points to a subset of optional servers (under components) from an external doc',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
channels: {
UserSignedUp: {
servers: [
{ $ref: 'http://foo.bar/components/file.yml#/components/servers/prod' },
{ $ref: 'http://foo.bar/components/file.yml#/components/servers/dev' },
]
}
}
},
errors: [
{
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.',
path: ['channels', 'UserSignedUp', 'servers', '0', '$ref'],
severity: DiagnosticSeverity.Error,
},
{
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.',
path: ['channels', 'UserSignedUp', 'servers', '1', '$ref'],
severity: DiagnosticSeverity.Error,
}
],
},
]);
Loading

0 comments on commit e6cbd74

Please sign in to comment.