Skip to content

Commit

Permalink
fix(rulesets): handle empty payload and headers in AsyncAPI message's…
Browse files Browse the repository at this point in the history
… examples validation
  • Loading branch information
magicmatatjahu committed Oct 4, 2022
1 parent ba90a20 commit 57cd059
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,121 @@ testRule('asyncapi-message-examples', [
errors: [],
},

{
name: 'valid case (with omitted payload)',
document: {
asyncapi: '2.0.0',
channels: {
someChannel: {
publish: {
message: {
headers: {
type: 'object',
},
examples: [
{
payload: 'foobar',
headers: {
someKey: 'someValue',
},
},
],
},
},
},
},
},
errors: [],
},

{
name: 'valid case (with omitted headers)',
document: {
asyncapi: '2.0.0',
channels: {
someChannel: {
publish: {
message: {
payload: {
type: 'string',
},
examples: [
{
payload: 'foobar',
headers: {
someKey: 'someValue',
},
},
],
},
},
},
},
},
errors: [],
},

{
name: 'valid case (with omitted paylaod and headers)',
document: {
asyncapi: '2.0.0',
channels: {
someChannel: {
publish: {
message: {
examples: [
{
payload: 'foobar',
headers: {
someKey: 'someValue',
},
},
],
},
},
},
},
},
errors: [],
},

{
name: 'valid case (with traits)',
document: {
asyncapi: '2.0.0',
channels: {
someChannel: {
publish: {
message: {
payload: {
type: 'string',
},
headers: {
type: 'object',
},
examples: [
{
payload: 2137,
headers: {
someKey: 'someValue',
},
},
],
traits: [
{
payload: {
type: 'number',
},
},
],
},
},
},
},
},
errors: [],
},

{
name: 'invalid case',
document: {
Expand Down Expand Up @@ -194,4 +309,47 @@ testRule('asyncapi-message-examples', [
},
],
},

{
name: 'invalid case (with traits)',
document: {
asyncapi: '2.0.0',
channels: {
someChannel: {
publish: {
message: {
payload: {
type: 'number',
},
headers: {
type: 'object',
},
examples: [
{
payload: 2137,
headers: {
someKey: 'someValue',
},
},
],
traits: [
{
payload: {
type: 'string',
},
},
],
},
},
},
},
},
errors: [
{
message: '"payload" property type must be string',
path: ['channels', 'someChannel', 'publish', 'message', 'examples', '0', 'payload'],
severity: DiagnosticSeverity.Error,
},
],
},
]);
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { schema as schemaFn } from '@stoplight/spectral-functions';
import type { JsonPath } from '@stoplight/types';
import type { IFunctionResult, RulesetFunctionContext } from '@stoplight/spectral-core';
import type { JSONSchema7 } from 'json-schema';
import { mergeTraits } from './utils/mergeTraits';

interface MessageExample {
name?: string;
Expand All @@ -15,6 +16,7 @@ interface MessageExample {
export interface MessageFragment {
payload: unknown;
headers: unknown;
traits?: any[];
examples?: MessageExample[];
}

Expand Down Expand Up @@ -68,6 +70,7 @@ export default createRulesetFunction<MessageFragment, null>(
options: null,
},
function asyncApi2MessageExamplesValidation(targetVal, _, ctx) {
targetVal = mergeTraits(targetVal); // first merge all traits of message
if (!targetVal.examples) return;
const examples = getMessageExamples(targetVal);

Expand All @@ -76,15 +79,17 @@ export default createRulesetFunction<MessageFragment, null>(
for (const example of examples) {
// validate payload
if (example.value.payload !== undefined) {
const errors = validate(example.value.payload, example.path, 'payload', targetVal.payload, ctx);
const payload = targetVal.payload ?? {}; // if payload is undefined we treat it as any schema
const errors = validate(example.value.payload, example.path, 'payload', payload, ctx);
if (Array.isArray(errors)) {
results.push(...errors);
}
}

// validate headers
if (example.value.headers !== undefined) {
const errors = validate(example.value.headers, example.path, 'headers', targetVal.headers, ctx);
const headers = targetVal.headers ?? {}; // if headers are undefined we treat them as any schema
const errors = validate(example.value.headers, example.path, 'headers', headers, ctx);
if (Array.isArray(errors)) {
results.push(...errors);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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' });
});
});
36 changes: 36 additions & 0 deletions packages/rulesets/src/asyncapi/functions/utils/mergeTraits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isPlainObject } from '@stoplight/json';

type HaveTraits = { traits?: any[] } & Record<string, any>;

export function mergeTraits<T extends HaveTraits>(data: T): T {
if (Array.isArray(data.traits)) {
data = { ...data }; // shallow copy
for (const trait of data.traits as T[]) {
for (const key in trait) {
data[key] = merge(data[key], trait[key]);
}
}
}
return data;
}

function merge<T>(origin: unknown, patch: unknown): T {
// If the patch is not an object, it replaces the origin.
if (!isPlainObject(patch)) {
return patch as T;
}

const result = !isPlainObject(origin)
? {} // Non objects are being replaced.
: Object.assign({}, origin); // Make sure we never modify the origin.

Object.keys(patch).forEach(key => {
const patchVal = patch[key];
if (patchVal === null) {
delete result[key];
} else {
result[key] = merge(result[key], patchVal);
}
});
return result as T;
}

0 comments on commit 57cd059

Please sign in to comment.