-
-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rulesets): add rule to validate AsyncAPI message's examples (#2126)
- Loading branch information
1 parent
1f1fd92
commit 87ef046
Showing
4 changed files
with
364 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 197 additions & 0 deletions
197
packages/rulesets/src/asyncapi/__tests__/asyncapi-message-examples.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import { DiagnosticSeverity } from '@stoplight/types'; | ||
import testRule from './__helpers__/tester'; | ||
|
||
testRule('asyncapi-message-examples', [ | ||
{ | ||
name: 'valid case', | ||
document: { | ||
asyncapi: '2.0.0', | ||
channels: { | ||
someChannel: { | ||
publish: { | ||
message: { | ||
payload: { | ||
type: 'string', | ||
}, | ||
headers: { | ||
type: 'object', | ||
}, | ||
examples: [ | ||
{ | ||
payload: 'foobar', | ||
headers: { | ||
someKey: 'someValue', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
errors: [], | ||
}, | ||
|
||
{ | ||
name: 'invalid case', | ||
document: { | ||
asyncapi: '2.0.0', | ||
channels: { | ||
someChannel: { | ||
publish: { | ||
message: { | ||
payload: { | ||
type: 'string', | ||
}, | ||
headers: { | ||
type: 'object', | ||
}, | ||
examples: [ | ||
{ | ||
payload: 2137, | ||
headers: { | ||
someKey: 'someValue', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
errors: [ | ||
{ | ||
message: '"payload" property type must be string', | ||
path: ['channels', 'someChannel', 'publish', 'message', 'examples', '0', 'payload'], | ||
severity: DiagnosticSeverity.Error, | ||
}, | ||
], | ||
}, | ||
|
||
{ | ||
name: 'invalid case (oneOf case)', | ||
document: { | ||
asyncapi: '2.0.0', | ||
channels: { | ||
someChannel: { | ||
publish: { | ||
message: { | ||
oneOf: [ | ||
{ | ||
payload: { | ||
type: 'string', | ||
}, | ||
headers: { | ||
type: 'object', | ||
}, | ||
examples: [ | ||
{ | ||
payload: 2137, | ||
headers: { | ||
someKey: 'someValue', | ||
}, | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
errors: [ | ||
{ | ||
message: '"payload" property type must be string', | ||
path: ['channels', 'someChannel', 'publish', 'message', 'oneOf', '0', 'examples', '0', 'payload'], | ||
severity: DiagnosticSeverity.Error, | ||
}, | ||
], | ||
}, | ||
|
||
{ | ||
name: 'invalid case (inside components.messages)', | ||
document: { | ||
asyncapi: '2.0.0', | ||
components: { | ||
messages: { | ||
someMessage: { | ||
payload: { | ||
type: 'string', | ||
}, | ||
headers: { | ||
type: 'object', | ||
}, | ||
examples: [ | ||
{ | ||
payload: 2137, | ||
headers: { | ||
someKey: 'someValue', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
errors: [ | ||
{ | ||
message: '"payload" property type must be string', | ||
path: ['components', 'messages', 'someMessage', 'examples', '0', 'payload'], | ||
severity: DiagnosticSeverity.Error, | ||
}, | ||
], | ||
}, | ||
|
||
{ | ||
name: 'invalid case (with multiple errors)', | ||
document: { | ||
asyncapi: '2.0.0', | ||
components: { | ||
messages: { | ||
someMessage: { | ||
payload: { | ||
type: 'object', | ||
required: ['key1', 'key2'], | ||
properties: { | ||
key1: { | ||
type: 'string', | ||
}, | ||
key2: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
headers: { | ||
type: 'object', | ||
}, | ||
examples: [ | ||
{ | ||
payload: { | ||
key1: 2137, | ||
}, | ||
headers: 'someValue', | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
errors: [ | ||
{ | ||
message: '"payload" property must have required property "key2"', | ||
path: ['components', 'messages', 'someMessage', 'examples', '0', 'payload'], | ||
severity: DiagnosticSeverity.Error, | ||
}, | ||
{ | ||
message: '"key1" property type must be string', | ||
path: ['components', 'messages', 'someMessage', 'examples', '0', 'payload', 'key1'], | ||
severity: DiagnosticSeverity.Error, | ||
}, | ||
{ | ||
message: '"headers" property type must be object', | ||
path: ['components', 'messages', 'someMessage', 'examples', '0', 'headers'], | ||
severity: DiagnosticSeverity.Error, | ||
}, | ||
], | ||
}, | ||
]); |
96 changes: 96 additions & 0 deletions
96
packages/rulesets/src/asyncapi/functions/asyncApi2MessageExamplesValidation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { createRulesetFunction } from '@stoplight/spectral-core'; | ||
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'; | ||
|
||
interface MessageExample { | ||
name?: string; | ||
summary?: string; | ||
payload?: unknown; | ||
headers?: unknown; | ||
} | ||
|
||
export interface MessageFragment { | ||
payload: unknown; | ||
headers: unknown; | ||
examples?: MessageExample[]; | ||
} | ||
|
||
function getMessageExamples(message: MessageFragment): Array<{ path: JsonPath; value: MessageExample }> { | ||
if (!Array.isArray(message.examples)) { | ||
return []; | ||
} | ||
return ( | ||
message.examples.map((example, index) => { | ||
return { | ||
path: ['examples', index], | ||
value: example, | ||
}; | ||
}) ?? [] | ||
); | ||
} | ||
|
||
function validate( | ||
value: unknown, | ||
path: JsonPath, | ||
type: 'payload' | 'headers', | ||
schema: unknown, | ||
ctx: RulesetFunctionContext, | ||
): ReturnType<typeof schemaFn> { | ||
return schemaFn( | ||
value, | ||
{ | ||
allErrors: true, | ||
schema: schema as JSONSchema7, | ||
}, | ||
{ | ||
...ctx, | ||
path: [...ctx.path, ...path, type], | ||
}, | ||
); | ||
} | ||
|
||
export default createRulesetFunction<MessageFragment, null>( | ||
{ | ||
input: { | ||
type: 'object', | ||
properties: { | ||
name: { | ||
type: 'string', | ||
}, | ||
summary: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
options: null, | ||
}, | ||
function asyncApi2MessageExamplesValidation(targetVal, _, ctx) { | ||
if (!targetVal.examples) return; | ||
const examples = getMessageExamples(targetVal); | ||
|
||
const results: IFunctionResult[] = []; | ||
|
||
for (const example of examples) { | ||
// validate payload | ||
if (example.value.payload !== undefined) { | ||
const errors = validate(example.value.payload, example.path, 'payload', targetVal.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); | ||
if (Array.isArray(errors)) { | ||
results.push(...errors); | ||
} | ||
} | ||
} | ||
|
||
return results; | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters