Skip to content

Commit

Permalink
feat(ibm-required-array-properties-in-response): add new validator ru…
Browse files Browse the repository at this point in the history
…le (#684)

This commit introduces the new 'ibm-required-array-properties-in-response'
validator rule, which enforces API Handbook guidance related to array
properties defined within response schemas.
This rule will look for and warn about any array properties defined as optional
within each response body's schema (and any sub-schemas reachable from there).

Signed-off-by: Phil Adams <[email protected]>
  • Loading branch information
padamstx authored Sep 24, 2024
1 parent b941f6d commit 7cc4be2
Show file tree
Hide file tree
Showing 16 changed files with 463 additions and 39 deletions.
90 changes: 90 additions & 0 deletions docs/ibm-cloud-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package.
* [ibm-request-and-response-content](#ibm-request-and-response-content)
* [ibm-requestbody-is-object](#ibm-requestbody-is-object)
* [ibm-requestbody-name](#ibm-requestbody-name)
* [ibm-required-array-properties-in-response](#ibm-required-array-properties-in-response)
* [ibm-resource-response-consistency](#ibm-resource-response-consistency)
* [ibm-response-status-codes](#ibm-response-status-codes)
* [ibm-schema-casing-convention](#ibm-schema-casing-convention)
Expand Down Expand Up @@ -549,6 +550,13 @@ has non-form content. <b>This rule is disabled by default.</b></td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-required-array-properties-in-response">ibm-required-array-properties-in-response</a></td>
<td>error</td>
<td>Array properties defined in a response should be required, per API Handbook guidance.
</td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-resource-response-consistency">ibm-resource-response-consistency</a></td>
<td>warn</td>
<td>Operations that create or update a resource should return the same schema as the "GET" request for the resource.</td>
Expand Down Expand Up @@ -5619,6 +5627,87 @@ paths:
</tr>
</table>


### ibm-required-array-properties-in-response
<table>
<tr>
<td><b>Rule id:</b></td>
<td><b>ibm-required-array-properties-in-response</b></td>
</tr>
<tr>
<td valign=top><b>Description:</b></td>
<td>
The <a href="https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-types#array">IBM Cloud API Handbook</a>
states that within a response body, an array property MUST NOT be optional.
This validation rule checks each schema used within a response to make sure each array property is defined as required
and not optional.
</td>
</tr>
<tr>
<td><b>Severity:</b></td>
<td>error</td>
</tr>
<tr>
<td><b>OAS Versions:</b></td>
<td>oas3</td>
</tr>
<tr>
<td valign=top><b>Non-compliant example:<b></td>
<td>
<pre>
paths:
'/v1/thing':
get:
operationId: list_things
responses:
200:
content:
'application/json':
schema:
$ref: '#/components/schemas/ThingCollection'
components:
schemas:
ThingCollection:
type: object
properties:
things: &lt;&lt; array property is optional
type: array
items:
$ref: '#/components/schemas/Thing'
</pre>
</td>
</tr>
<tr>
<td valign=top><b>Compliant example:</b></td>
<td>
<pre>
paths:
'/v1/thing':
get:
operationId: list_things
responses:
200:
content:
'application/json':
schema:
$ref: '#/components/schemas/ThingCollection'
components:
schemas:
ThingCollection:
type: object
required:
- things &lt;&lt; array property is now required
properties:
things:
type: array
items:
$ref: '#/components/schemas/Thing'
</pre>
</td>
</tr>
</table>


### ibm-resource-response-consistency
<table>
<tr>
Expand Down Expand Up @@ -5701,6 +5790,7 @@ paths:
</tr>
</table>


### ibm-response-status-codes
<table>
<tr>
Expand Down
54 changes: 27 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions packages/ruleset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@
},
"dependencies": {
"@ibm-cloud/openapi-ruleset-utilities": "1.3.2",
"@stoplight/spectral-formats": "^1.6.0",
"@stoplight/spectral-functions": "^1.8.0",
"@stoplight/spectral-rulesets": "^1.19.1",
"@stoplight/spectral-formats": "^1.7.0",
"@stoplight/spectral-functions": "^1.9.0",
"@stoplight/spectral-rulesets": "^1.20.2",
"chalk": "^4.1.2",
"lodash": "^4.17.21",
"loglevel": "^1.9.1",
"loglevel": "^1.9.2",
"loglevel-plugin-prefix": "0.8.4",
"minimatch": "^6.2.0",
"validator": "^13.11.0"
},
"devDependencies": {
"@stoplight/spectral-core": "^1.18.3",
"@stoplight/spectral-core": "^1.19.1",
"jest": "^27.4.5"
},
"engines": {
Expand Down
1 change: 1 addition & 0 deletions packages/ruleset/src/functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ module.exports = {
refSiblingDuplicateDescription: require('./ref-sibling-duplicate-description'),
requestAndResponseContent: require('./request-and-response-content'),
requestBodyName: require('./requestbody-name'),
requiredArrayPropertiesInResponse: require('./required-array-properties-in-response'),
requiredProperty: require('./required-property'),
resourceResponseConsistency: require('./resource-response-consistency'),
responseExampleExists: require('./response-example-exists'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const {
isArraySchema,
isObject,
validateSubschemas,
} = require('@ibm-cloud/openapi-ruleset-utilities');
const { LoggerFactory } = require('../utils');

let ruleId;
let logger;

module.exports = function (schema, _opts, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}

return validateSubschemas(
schema,
context.path,
checkForOptionalArrays,
true,
false
);
};

/**
* Checks "schema" for any optional array properties.
* @param {*} schema the schema to check
* @param {*} path the json path location of "schema"
* @returns
*/
function checkForOptionalArrays(schema, path) {
// If "schema" defines properties, then add an error for each optional array property.
if (isObject(schema) && isObject(schema.properties)) {
logger.debug(
`${ruleId}: examining object schema at location: ${path.join('.')}`
);

const errors = [];

const requiredProps = schema.required || [];
for (const [name, prop] of Object.entries(schema.properties)) {
if (isArraySchema(prop) && !requiredProps.includes(name)) {
errors.push({
message: 'In a response body, an array field MUST NOT be optional',
path: [...path, 'properties', name],
});
}
}

return errors;
}

return [];
}
2 changes: 2 additions & 0 deletions packages/ruleset/src/ibm-oas.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ module.exports = {
'ibm-request-and-response-content': ibmRules.requestAndResponseContent,
'ibm-requestbody-is-object': ibmRules.requestBodyIsObject,
'ibm-requestbody-name': ibmRules.requestBodyName,
'ibm-required-array-properties-in-response':
ibmRules.requiredArrayPropertiesInResponse,
'ibm-resource-response-consistency': ibmRules.resourceResponseConsistency,
'ibm-response-status-codes': ibmRules.responseStatusCodes,
'ibm-schema-casing-convention': ibmRules.schemaCasingConvention,
Expand Down
1 change: 1 addition & 0 deletions packages/ruleset/src/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ module.exports = {
requestAndResponseContent: require('./request-and-response-content'),
requestBodyIsObject: require('./requestbody-is-object'),
requestBodyName: require('./requestbody-name'),
requiredArrayPropertiesInResponse: require('./required-array-properties-in-response'),
requiredPropertyMissing: require('./required-property-missing'),
resourceResponseConsistency: require('./resource-response-consistency'),
responseExampleExists: require('./response-example-exists'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright 2017 - 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const {
responseSchemas,
} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections');

const { requiredArrayPropertiesInResponse } = require('../functions');

module.exports = {
description: 'Array properties defined in a response should be required.',
message: '{{error}}',
given: responseSchemas,
severity: 'error',
resolved: true,
then: {
function: requiredArrayPropertiesInResponse,
},
};
Loading

0 comments on commit 7cc4be2

Please sign in to comment.