Skip to content

Commit

Permalink
feat: add Spectral rule to validate required operation channel field
Browse files Browse the repository at this point in the history
  • Loading branch information
smoya committed Nov 28, 2023
1 parent c3cb73c commit 40d8e7a
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 0 deletions.
42 changes: 42 additions & 0 deletions src/ruleset/v3/functions/requiredOperationChannelUnambiguity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 requiredOperationChannelUnambiguity = 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

if (!channelPointer.includes('#/channels/')) {
results.push({
message: 'The channel field of a required operation should point to a required channel.',
path: [...ctx.path, 'channel'],
});
}

return results;
},
);
14 changes: 14 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 { requiredOperationChannelUnambiguity } from './functions/requiredOperationChannelUnambiguity';

export const v3CoreRuleset = {
description: 'Core AsyncAPI 3.x.x ruleset.',
Expand All @@ -24,5 +25,18 @@ export const v3CoreRuleset = {
function: operationMessagesUnambiguity,
},
},
'asyncapi3-required-operation-channel-unambiguity': {
description: 'Required operation (under root channels) "channel" must reference to a required channel (under root channels).',
message: '{{error}}',
severity: 'error',
recommended: true,
resolved: false, // We use the JSON pointer to match the channel.
given: [
'$.operations.*',
],
then: {
function: requiredOperationChannelUnambiguity,
},
}
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import { testRule, DiagnosticSeverity } from '../../tester';

testRule('asyncapi3-required-operation-channel-unambiguity', [
{
name: 'valid case - required operation (under root) channel field points to a required channel (under root)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
channels: {
UserSignedUp: {
messages: {
UserSignedUp: {
payload: {
type: 'object',
properties: {
displayName: {
type: 'string'
},
email: {
type: 'string'
}
}
}
}
}
}
},
operations: {
UserSignedUp: {
action: 'send',
channel: {
$ref: '#/channels/UserSignedUp'
},
messages: [
{
$ref: '#/channels/UserSignedUp/messages/UserSignedUp'
}
]
}
}
},
errors: [],
},
{
name: 'valid case - required operation (under root) channel field points to a required channel (under root) from an external doc',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
channels: {
UserSignedUp: {
messages: {
UserSignedUp: {
payload: {
type: 'object',
properties: {
displayName: {
type: 'string'
},
email: {
type: 'string'
}
}
}
}
}
}
},
operations: {
UserSignedUp: {
action: 'send',
channel: {
$ref: 'http://foo.bar/components/file.yml#/channels/UserSignedUp'
},
messages: [
{
$ref: 'http://foo.bar/components/file.yml#/channels/UserSignedUp/messages/UserSignedUp'
}
]
}
}
},
errors: [],
},
{
name: 'valid case - required operation (under components) channel field points to a required channel (under root)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
channels: {
UserSignedUp: {
messages: {
UserSignedUp: {
payload: {
type: 'object',
properties: {
displayName: {
type: 'string'
},
email: {
type: 'string'
}
}
}
}
}
}
},
components: {
operations: {
UserSignedUp: {
action: 'send',
channel: {
$ref: '#/channels/UserSignedUp'
},
messages: [
{
$ref: '#/channels/UserSignedUp/messages/UserSignedUp'
}
]
}
}
}
},
errors: [],
},
{
name: 'valid case - optional operation (under components) channel field points to an optional channel (under components)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
components: {
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 operation (in root) channel field points to an optional channel (under components)',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
operations: {
UserSignedUp: {
action: 'send',
channel: {
$ref: '#/components/channels/UserSignedUp'
},
messages: [
{
$ref: '#/components/channels/UserSignedUp/messages/UserSignedUp'
}
]
}
},
components: {
channels: {
UserSignedUp: {
messages: {
UserSignedUp: {
payload: {
type: 'object',
properties: {
displayName: {
type: 'string'
},
email: {
type: 'string'
}
}
}
}
}
}
},
},
},
errors: [
{
message: 'The channel field of a required operation should point to a required channel.',
path: ['operations', 'UserSignedUp', 'channel'],
severity: DiagnosticSeverity.Error,
}
],
},
{
name: 'invalid case - required operation (in root) channel field points to an optional channel (under components) from an external doc',
document: {
asyncapi: '3.0.0',
info: {
title: 'Account Service',
version: '1.0.0'
},
operations: {
UserSignedUp: {
action: 'send',
channel: {
$ref: 'http://foo.bar/components/file.yml#/components/channels/UserSignedUp'
},
messages: [
{
$ref: 'http://foo.bar/components/file.yml#/components/channels/UserSignedUp/messages/UserSignedUp'
}
]
}
}
},
errors: [
{
message: 'The channel field of a required operation should point to a required channel.',
path: ['operations', 'UserSignedUp', 'channel'],
severity: DiagnosticSeverity.Error,
}
],
},
]);

0 comments on commit 40d8e7a

Please sign in to comment.