Enforcing consistency guidelines - part 2

Jonas Lagoni Avatar

Jonas Lagoni

ยท6 min read

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.

I have suggested including each of the custom rules into the built-in AsyncAPI ruleset which is linked to at the end of each. If you also would like to see them added, please go to the corresponding issue voice it ๐Ÿ‘

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.

Photo by HONG LIN on Unsplash