Part 2 clarifies the more advanced Spectral rules for enforcing the consistency governance guidelines. For a better introduction into Spectral and the setup please checkout part 1 of enforcing consistency guidelines.
Enforce _at
suffix for date/time properties
MUST name date/time properties with _at suffix.
This one is a bit tricky. I ended up going with a rule that matches any object containing properties
keyword and calls the custom function use-suffix-for-date-time
for each.
1...
2functions: [..., use-suffix-for-date-time]
3rules:
4 ...
5 asyncapi-date-time-must-use-at-suffix:
6 given: $.channels.[*][subscribe,publish].message..properties
7 severity: error
8 then:
9 function: use-suffix-for-date-time
The custom function then checks all properties against whether the property use date/time formats and if they do, ensure that the property ends with _at
:
1// Based on https://json-schema.org/understanding-json-schema/reference/string.html#dates-and-times
2export default (properties, _, { path }) => {
3 const results = [];
4 for (const [propertyName, property] of Object.entries(properties)) {
5 const formats = ["date-time", "time", "date"];
6 const formatsForMessage = formats.map(format => `"${format}"`).join(',');
7 if(property.format && formats.includes(property.format)) {
8 const lastThreeChars = propertyName.slice(propertyName.length-3, propertyName.length);
9 if(lastThreeChars !== '_at'){
10 results.push({
11 message: `Formats ${formatsForMessage} MUST end with "_at". Expected property "${propertyName}" to be called "${propertyName}_at"`,
12 path: [...path, propertyName],
13 });
14 }
15 }
16 }
17 return results;
18};
Issue proposing to bring this as part of the built-in ruleset in Spectral: https://github.com/stoplightio/spectral/issues/2114
Force snake_case format for property names
MUST property names must be snake_case.
Same as with the date/time suffix check, the best way I found to solve this was with a rule that matches any object containing properties
keyword and calls the custom function snake-case-properties
for each.
1...
2functions: [..., snake-case-properties]
3rules:
4 ...
5 asyncapi-properties-must-follow-snake-case:
6 given: $.channels.[*][subscribe,publish].message..properties
7 severity: error
8 then:
9 function: snake-case-properties
The custom function then for each property, format it as snake_case, and if the original property is not equal to the snake case variant, return there is an issue.
1function snake_case_string(str) {
2 return str && str.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
3 .map(s => s.toLowerCase())
4 .join('_');
5}
6
7export default (properties, _, { path }) => {
8 const results = [];
9 for (const [property, _] of Object.entries(properties)) {
10 const expectedPropertyName = snake_case_string(property);
11 if (property !== expectedPropertyName) {
12 results.push({
13 message: `Property MUST follow snake-case. Expected property "${property}" to be called "${expectedPropertyName}"`,
14 path: [...path, property],
15 });
16 }
17 }
18 return results;
19};
Issue proposing to bring this as part of the built-in ruleset in Spectral: https://github.com/stoplightio/spectral/issues/2115
Include versioning in channels
MUST include semantic versioning in channels (each channel and path must use resource versioning. Read more about the versioning strategy here).
This check can be done by matching all channel keys against a regex pattern, which ensures v<NUMBER>
MUST be present, otherwise, it will result in an error.
1rules:
2 ...
3 asyncapi-channel-must-have-version:
4 given: "$.channels"
5 description: 'Channel MUST have version number included in the topic. Example: "v1/channel/example"'
6 severity: error
7 then:
8 field: "@key"
9 function: pattern
10 functionOptions:
11 match: ".*(v(.*[0-9])).*"
Issue proposing to bring this as part of the built-in ruleset in Spectral: https://github.com/stoplightio/spectral/issues/2116
Always use references
MUST use references where at all possible (to leverage reusability, references MUST be used where at all possible).
Spectral normally resolves all references before you can check them, but you can disable that through resolved: false
. I will only enforce reusability for parameters and message objects for now, as I have no idea how to change the depth references are resolved.
Parameters
The channel parameters can be checked that each is always defined with $ref
instead of any information.
1rules:
2 ...
3 asyncapi-parameters-must-use-references:
4 description: Channel parameters must be references
5 given: $.channels.[*]
6 severity: error
7 resolved: false
8 then:
9 function: schema
10 functionOptions:
11 schema:
12 properties:
13 parameters:
14 additionalProperties:
15 type: object
16 required:
17 - $ref
Issue proposing to bring this as part of the built-in ruleset in Spectral: https://github.com/stoplightio/spectral/issues/2117
Messages
Messages are a bit different because you can define messages with oneOf
, and if that is the case, we need to match against an array of references instead of a single instance.
1rules:
2 ...
3 asyncapi-messages-must-use-references:
4 description: Operation messages must be references
5 given: $.channels.[*][subscribe,publish].message
6 severity: error
7 resolved: false
8 then:
9 function: schema
10 functionOptions:
11 schema:
12 if:
13 required:
14 - oneOf
15 then:
16 properties:
17 oneOf:
18 type: array
19 items:
20 type: object
21 required:
22 - $ref
23 else:
24 type: object
25 required:
26 - $ref
Issue proposing to bring this as part of the built-in ruleset in Spectral: https://github.com/stoplightio/spectral/issues/2118
Message payload
I would have liked to force message payloads to always contain references. However, because messages themselves are references, you need a way to control the "depth" references are resolved.
I proposed this is a new feature for Spectral: https://github.com/stoplightio/spectral/issues/2120
Always use top-level JSON object
MUST always return JSON objects as top-level data structures
For this rule, we can check that the message payloads always are defined as type: "object"
.
1rules:
2 ...
3 asyncapi-message-payload-must-have-root-level-object:
4 description: Message payloads must at the root be an object
5 given: $.channels.[*][subscribe,publish].message..payload
6 severity: error
7 then:
8 function: schema
9 functionOptions:
10 schema:
11 properties:
12 type:
13 const: "object"
Issue proposing to bring this as part of the built-in ruleset in Spectral: https://github.com/stoplightio/spectral/issues/2119
Only use additionalProperties
for map types
SHOULD avoid additionalProperties unless used as a map partly inspired by 210 (each object should be as constrained as possible and additionalProperties should not be used unless used to define a map.).
For this, the simplest form of rule I found, was to match all objects @object()
under the message payload.
1...
2functions: [..., payload-with-no-additional-properties-unless-map]
3rules:
4 ...
5 asyncapi-payload-with-no-additional-properties-unless-map:
6 given: $.channels.[*][subscribe,publish].message..payload.@object()
7 severity: error
8 then:
9 function: payload-with-no-additional-properties-unless-map
And then ensure that properties
and additionalProperties
cannot be defined at the same time for each JSON object. The easiest way to solve this was through a custom function.
1export default (jsonObject, _, { path }) => {
2 const results = [];
3 if(jsonObject.properties !== undefined && jsonObject.additionalProperties !== false) {
4 results.push({
5 message: `Object with properties should not also define additionalProperties`,
6 path: [...path, propertyName],
7 });
8 }
9 return results;
10};
Conclusion
The rest of the consistency guidelines are not possible to enforce by Spectral because it is not about the AsyncAPI document itself, but the processes and documentation around it.
I think Spectral is perfect for enforcing consistency for AsyncAPI documents. I am looking forward to having a more comprehensive standard ruleset, which will make it easier for everyone to use. Because it does take some time to learn how to target
rules and how and what functions to then
apply.