Skip to content

Commit

Permalink
feat(new-rule): ibm-schema-naming-convention (#627)
Browse files Browse the repository at this point in the history
Adds a new rule that enforces the schema naming guidelines in the API Handbook.

Specifically, the new rule checks for correctly named collection schemas, resource
collection element schemas, creation/replacement schemas, and patch schemas against
the name of the associated canonical schema.

There is one expection for creation/replacement schemas The Handbook states:

"There is an important exception to this guidance if the nature of an API is
to provide clients with full control over entire resources, with no
system-defined or immutable aspects. For such APIs, the canonical schema
itself SHOULD be used in requests to create or replace a resource."

The rule allows for the name of the canonical schema to be used for
resource-oriented POSTs and PUTs.

Signed-off-by: Dustin Popp <[email protected]>
Co-authored-by: Phil Adams <[email protected]>
  • Loading branch information
dpopp07 and padamstx committed Oct 2, 2023
1 parent 10042b2 commit 17060c8
Show file tree
Hide file tree
Showing 29 changed files with 1,850 additions and 185 deletions.
78 changes: 78 additions & 0 deletions docs/ibm-cloud-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package.
* [ibm-response-status-codes](#ibm-response-status-codes)
* [ibm-schema-description](#ibm-schema-description)
* [ibm-schema-keywords](#ibm-schema-keywords)
* [ibm-schema-naming-convention](#ibm-schema-naming-convention)
* [ibm-schema-type](#ibm-schema-type)
* [ibm-schema-type-format](#ibm-schema-type-format)
* [ibm-sdk-operations](#ibm-sdk-operations)
Expand Down Expand Up @@ -518,6 +519,12 @@ specific "allow-listed" keywords.</td>
<td>oas3_1</td>
</tr>
<tr>
<td><a href="#ibm-schema-naming-convention">ibm-schema-naming-convention</a></td>
<td>warn</td>
<td>Schemas should follow the API Handbook naming conventions.</td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-schema-type">ibm-schema-type</a></td>
<td>off</td>
<td>Schemas and schema properties should have a non-empty <code>type</code> field. <b>This rule is disabled by default.</b></td>
Expand Down Expand Up @@ -5493,6 +5500,77 @@ components:
</table>


### ibm-schema-naming-convention
<table>
<tr>
<td><b>Rule id:</b></td>
<td><b>ibm-schema-naming-convention</b></td>
</tr>
<tr>
<td valign=top><b>Description:</b></td>
<td>
The name of each schema should follow the <a href="https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-schemas#naming">IBM Cloud API Handbook schema naming conventions</a>.

The rule checks the names of collection schemas, resource collection element schemas, creation/replacement schemas, and patch schemas against the name of the associated canonical schema to ensure the names follow the guidelines.
</td>
</tr>
<tr>
<td><b>Severity:</b></td>
<td>warn</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/things:
post:
requestBody:
content:
'application/json':
schema:
$ref: '#/components/schemas/ThingCreator' # Should be ThingPrototype
/v1/things/{id}:
get:
responses:
200:
content:
'application/json':
schema:
$ref: '#/components/schemas/Thing' # Canonical schema
</pre>
</td>
</tr>
<tr>
<td valign=top><b>Compliant example:</b></td>
<td>
<pre>
paths:
/v1/things:
post:
requestBody:
content:
'application/json':
schema:
$ref: '#/components/schemas/ThingPrototype'
/v1/things/{id}:
get:
responses:
200:
content:
'application/json':
schema:
$ref: '#/components/schemas/Thing'
</pre>
</td>
</tr>
</table>


### ibm-schema-type
<table>
<tr>
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 @@ -55,6 +55,7 @@ module.exports = {
responseExampleExists: require('./response-example-exists'),
responseStatusCodes: require('./response-status-codes'),
schemaDescriptionExists: require('./schema-description-exists'),
schemaNamingConvention: require('./schema-naming-convention'),
schemaOrContentProvided: require('./schema-or-content-provided'),
schemaTypeExists: require('./schema-type-exists'),
schemaTypeFormat: require('./schema-type-format'),
Expand Down
64 changes: 7 additions & 57 deletions packages/ruleset/src/functions/resource-response-consistency.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { isObject } = require('@ibm-cloud/openapi-ruleset-utilities');
const {
getResourceSpecificSiblingPath,
getResponseCodes,
getSuccessResponseSchemaForOperation,
isCreateOperation,
isJsonMimeType,
isOperationOfType,
Expand Down Expand Up @@ -167,9 +168,9 @@ function resourceResponseConsistency(operation, path, apidef) {

function getCanonicalSchema(path, apidef) {
const resourceSpecificPath = isOperationOfType('post', path)
? getResourceSpecificSiblingPath(path, apidef)
? getResourceSpecificSiblingPath(path.at(-2), apidef)
: // This is a PUT or PATCH and should already be on the path we need
path[path.length - 2].toString().trim();
path.at(-2).trim();

logger.debug(
`${ruleId}: calculated resource-specific path to be "${resourceSpecificPath}"`
Expand All @@ -189,62 +190,11 @@ function getCanonicalSchema(path, apidef) {
);
return;
}
if (!resourceGetOperation.responses) {
logger.debug(
`${ruleId}: no responses defined on GET operation at path "${resourceSpecificPath}"`
);
return;
}

const [, getOpSuccessCodes] = getResponseCodes(
resourceGetOperation.responses
);

logger.debug(
`${ruleId}: corresponding GET operation has the following status codes: ${getOpSuccessCodes.join(
', '
)}`
);

if (
!getOpSuccessCodes ||
!getOpSuccessCodes.length ||
!getOpSuccessCodes.includes('200')
) {
logger.debug(
`${ruleId}: no 200 success code found in responses defined on GET operation at path "${resourceSpecificPath}"`
);
return;
}

const successResponse = resourceGetOperation.responses['200'];
if (!successResponse.content || !isObject(successResponse.content)) {
return;
}

// Find the first content object determined to be JSON -
// we are only interested in JSON content.
const jsonMimeType = Object.keys(successResponse.content).find(mimeType =>
isJsonMimeType(mimeType)
);
if (!jsonMimeType) {
logger.debug(
`${ruleId}: no JSON content found on 200 response defined on GET operation at path "${resourceSpecificPath}"`
);
return;
}

const jsonContent = successResponse.content[jsonMimeType];
if (!isObject(jsonContent) || !jsonContent.schema) {
logger.debug(
`${ruleId}: no JSON content schema found in 200 response defined on GET operation at path "${resourceSpecificPath}"`
);
return;
}

logger.debug(
`${ruleId}: found schema in ${jsonMimeType} content object in 200 response defined on GET operation at path "${resourceSpecificPath}"`
const { schemaObject } = getSuccessResponseSchemaForOperation(
resourceGetOperation,
path
);

return jsonContent.schema;
return schemaObject;
}
5 changes: 4 additions & 1 deletion packages/ruleset/src/functions/response-status-codes.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ function responseStatusCodes(operation, path, apidef) {
}

function hasBodyRepresentation(path, apidef) {
const resourceSpecificPath = getResourceSpecificSiblingPath(path, apidef);
const resourceSpecificPath = getResourceSpecificSiblingPath(
path.at(-2),
apidef
);

logger.debug(
`${ruleId}: calculated resource-specific path to be "${resourceSpecificPath}"`
Expand Down
Loading

0 comments on commit 17060c8

Please sign in to comment.