Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rulesets): add rule to check if messageId is defined #2281

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/reference/asyncapi-rules.md
Expand Up @@ -259,6 +259,12 @@ channels:
messageId: turnOffMessage
```

### asyncapi-message-messageId

Each Message should have a "messageId" field defined. It is recommended, but still optional. Tools can use it to define function names, class method names and even URL hashes in documentation systems.

**Recommended:** Yes

### asyncapi-operation-description

Operation objects should have a description.
Expand Down Expand Up @@ -297,7 +303,7 @@ channels:

### asyncapi-operation-operationId

This operation ID is essentially a reference for the operation. Tools may use it for defining function names, class method names, and even URL hashes in documentation systems.
Each Operation must have an "operationId" field defined. Tools can use it to define function names, class method names and even URL hashes in documentation systems.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Each Operation must have an "operationId" field defined. Tools can use it to define function names, class method names and even URL hashes in documentation systems.
Each Operation must have an "operationId" field defined. Tools may use it to define function names, class method names, and even URL hashes in documentation systems.


**Recommended:** Yes

Expand Down
@@ -0,0 +1,179 @@
import { DiagnosticSeverity } from '@stoplight/types';
import testRule from './__helpers__/tester';

testRule('asyncapi-message-messageId', [
{
name: 'valid case',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
message: {
messageId: 'firstId',
},
},
subscribe: {
message: {
messageId: 'secondId',
},
},
},
},
},
errors: [],
},

{
name: 'valid case (unsupported version)',
document: {
asyncapi: '2.3.0',
channels: {
one: {
publish: {
message: {},
},
subscribe: {
message: {},
},
},
},
},
errors: [],
},

{
name: 'valid case (with traits)',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
message: {
traits: [
{},
{
messageId: 'firstId',
},
],
},
},
subscribe: {
message: {
messageId: 'secondId',
},
},
},
},
},
errors: [],
},

{
name: 'invalid case',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
message: {},
},
subscribe: {
message: {},
},
},
},
},
errors: [
{
message: 'Message should have a "messageId" field defined.',
path: ['channels', 'one', 'publish', 'message'],
severity: DiagnosticSeverity.Warning,
},
{
message: 'Message should have a "messageId" field defined.',
path: ['channels', 'one', 'subscribe', 'message'],
severity: DiagnosticSeverity.Warning,
},
],
},

{
name: 'invalid case (oneOf case)',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
message: {},
externalDocs: {},
},
subscribe: {
message: {
oneOf: [
{},
{
messageId: 'someId',
},
{},
{
traits: [
{},
{
messageId: 'anotherId',
},
],
},
],
},
},
},
},
},
errors: [
{
message: 'Message should have a "messageId" field defined.',
path: ['channels', 'one', 'publish', 'message'],
severity: DiagnosticSeverity.Warning,
},
{
message: 'Message should have a "messageId" field defined.',
path: ['channels', 'one', 'subscribe', 'message', 'oneOf', '0'],
severity: DiagnosticSeverity.Warning,
},
{
message: 'Message should have a "messageId" field defined.',
path: ['channels', 'one', 'subscribe', 'message', 'oneOf', '2'],
severity: DiagnosticSeverity.Warning,
},
],
},

{
name: 'invalid case (with traits)',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
message: {
traits: [{}, {}],
},
},
subscribe: {
message: {
messageId: 'secondId',
},
},
},
},
},
errors: [
{
message: 'Message should have a "messageId" field defined.',
path: ['channels', 'one', 'publish', 'message'],
severity: DiagnosticSeverity.Warning,
},
],
},
]);
@@ -1,39 +1,94 @@
import { DiagnosticSeverity } from '@stoplight/types';
import produce from 'immer';
import testRule from './__helpers__/tester';

const document = {
asyncapi: '2.0.0',
channels: {
one: {
publish: {
operationId: 'onePubId',
},
subscribe: {
operationId: 'oneSubId',
testRule('asyncapi-operation-operationId', [
{
name: 'valid case',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
operationId: 'firstId',
},
subscribe: {
operationId: 'secondId',
},
},
},
},
errors: [],
},
};

testRule('asyncapi-operation-operationId', [
{
name: 'valid case',
document,
name: 'valid case (with traits)',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
traits: [
{},
{
operationId: 'firstId',
},
],
},
subscribe: {
operationId: 'secondId',
},
},
},
},
errors: [],
},

...['publish', 'subscribe'].map(property => ({
name: `channels.{channel}.${property}.operationId property is missing`,
document: produce(document, draft => {
delete draft.channels.one[property].operationId;
}),
{
name: 'invalid case',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {},
subscribe: {},
},
},
},
errors: [
{
message: 'Operation must have an "operationId" field defined.',
path: ['channels', 'one', 'publish'],
severity: DiagnosticSeverity.Error,
},
{
message: 'Operation must have an "operationId" field defined.',
path: ['channels', 'one', 'subscribe'],
severity: DiagnosticSeverity.Error,
},
],
},

{
name: 'invalid case (with traits)',
document: {
asyncapi: '2.4.0',
channels: {
one: {
publish: {
traits: [{}, {}],
},
subscribe: {
operationId: 'secondId',
},
},
},
},
errors: [
{
message: 'Operation must have "operationId".',
path: ['channels', 'one', property],
message: 'Operation must have an "operationId" field defined.',
path: ['channels', 'one', 'publish'],
severity: DiagnosticSeverity.Error,
},
],
})),
},
]);
34 changes: 34 additions & 0 deletions packages/rulesets/src/asyncapi/functions/asyncApi2CheckId.ts
@@ -0,0 +1,34 @@
import { createRulesetFunction } from '@stoplight/spectral-core';
import { truthy } from '@stoplight/spectral-functions';
import { mergeTraits } from './utils/mergeTraits';

import type { MaybeHaveTraits } from './utils/mergeTraits';

export default createRulesetFunction<MaybeHaveTraits, { idField: 'operationId' | 'messageId' }>(
{
input: {
type: 'object',
properties: {
traits: {
type: 'array',
items: {
type: 'object',
},
},
},
},
options: {
type: 'object',
properties: {
idField: {
type: 'string',
enum: ['operationId', 'messageId'],
},
},
},
},
function asyncApi2CheckId(targetVal, options, ctx) {
const mergedValue = mergeTraits(targetVal);
return truthy(mergedValue[options.idField], null, ctx);
},
);
@@ -0,0 +1,32 @@
import { mergeTraits } from '../mergeTraits';

describe('mergeTraits', () => {
test('should merge one trait', () => {
const result = mergeTraits({ payload: {}, traits: [{ payload: { someKey: 'someValue' } }] });
expect(result.payload).toEqual({ someKey: 'someValue' });
});

test('should merge two or more traits', () => {
const result = mergeTraits({
payload: {},
traits: [
{ payload: { someKey1: 'someValue1' } },
{ payload: { someKey2: 'someValue2' } },
{ payload: { someKey3: 'someValue3' } },
],
});
expect(result.payload).toEqual({ someKey1: 'someValue1', someKey2: 'someValue2', someKey3: 'someValue3' });
});

test('should override fields', () => {
const result = mergeTraits({
payload: { someKey: 'someValue' },
traits: [
{ payload: { someKey: 'someValue1' } },
{ payload: { someKey: 'someValue2' } },
{ payload: { someKey: 'someValue3' } },
],
});
expect(result.payload).toEqual({ someKey: 'someValue3' });
});
});