Skip to content

Commit

Permalink
feat(new-rule): ibm-operation-summary-length (#663)
Browse files Browse the repository at this point in the history
Adds a rule that verifies operation summaries are 80 characters
or less in length, based on the summary writing guidance in the
API Handbook.

Signed-off-by: Dustin Popp <dpopp07@gmail.com>
  • Loading branch information
dpopp07 committed May 9, 2024
1 parent 8a355a3 commit 901cc1a
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 1 deletion.
56 changes: 56 additions & 0 deletions docs/ibm-cloud-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package.
* [ibm-openapi-tags-used](#ibm-openapi-tags-used)
* [ibm-operation-responses](#ibm-operation-responses)
* [ibm-operation-summary](#ibm-operation-summary)
* [ibm-operation-summary-length](#ibm-operation-summary-length)
* [ibm-operationid-casing-convention](#ibm-operationid-casing-convention)
* [ibm-operationid-naming-convention](#ibm-operationid-naming-convention)
* [ibm-pagination-style](#ibm-pagination-style)
Expand Down Expand Up @@ -374,6 +375,12 @@ should probably be required instead of optional.</td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-operation-summary-length">ibm-operation-summary-length</a></td>
<td>error</td>
<td>The value of an operation <code>summary</code> must be 80 characters or less in length.</td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-operation-responses">ibm-operation-responses</a></td>
<td>error</td>
<td>Verifies that each operation has a <code>responses</code> field.</td>
Expand Down Expand Up @@ -3823,6 +3830,55 @@ paths:
</table>


### ibm-operation-summary-length
<table>
<tr>
<td><b>Rule id:</b></td>
<td><b>ibm-operation-summary-length</b></td>
</tr>
<tr>
<td valign=top><b>Description:</b></td>
<td> This rule verifies that each operation's <code>summary</code> is at most 80 characters in length,
per guidance in the <a href="https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-writing#operation-summaries">API Handbook</a>.
</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/things':
post:
operationId: create_thing
description: Create a new Thing instance.
summary: Create a shiny, brand new, hot-off-the-press instance of the standard Thing resource
</pre>
</td>
</tr>
<tr>
<td valign=top><b>Compliant example:</b></td>
<td>
<pre>
paths:
'/v1/things':
get:
operationId: create_thing
description: Create a new Thing instance.
summary: Create a Thing
</pre>
</td>
</tr>
</table>


### ibm-operationid-casing-convention
<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 @@ -31,6 +31,7 @@ module.exports = {
operationIdCasingConvention: require('./operationid-casing-convention'),
operationIdNamingConvention: require('./operationid-naming-convention'),
operationSummaryExists: require('./operation-summary-exists'),
operationSummaryLength: require('./operation-summary-length'),
optionalRequestBody: require('./optional-request-body'),
paginationStyle: require('./pagination-style'),
parameterCasingConvention: require('./parameter-casing-convention'),
Expand Down
41 changes: 41 additions & 0 deletions packages/ruleset/src/functions/operation-summary-length.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const { LoggerFactory } = require('../utils');

let ruleId;
let logger;

/**
* The implementation for this rule makes assumptions that are dependent on the
* presence of the following other rules:
*
* - operation-summary: all operations define a non-empty summary
*/

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

function checkSummaryLength(summary, path) {
logger.debug(
`${ruleId}: checking operation summary at location: ${path.join('.')}`
);

if (summary && summary.length > 80) {
return [
{
message: 'Operation summaries must be 80 characters or less in length',
path,
},
];
}

return [];
}
1 change: 1 addition & 0 deletions packages/ruleset/src/ibm-oas.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ module.exports = {
'ibm-openapi-tags-used': ibmRules.unusedTags,
'ibm-operation-responses': ibmRules.operationResponses,
'ibm-operation-summary': ibmRules.operationSummaryExists,
'ibm-operation-summary-length': ibmRules.operationSummaryLength,
'ibm-operationid-casing-convention': ibmRules.operationIdCasingConvention,
'ibm-operationid-naming-convention': ibmRules.operationIdNamingConvention,
'ibm-pagination-style': ibmRules.paginationStyle,
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 @@ -40,6 +40,7 @@ module.exports = {
operationIdNamingConvention: require('./operationid-naming-convention'),
operationResponses: require('./operation-responses'),
operationSummaryExists: require('./operation-summary-exists'),
operationSummaryLength: require('./operation-summary-length'),
optionalRequestBody: require('./optional-request-body'),
paginationStyle: require('./pagination-style'),
parameterCasingConvention: require('./parameter-casing-convention'),
Expand Down
21 changes: 21 additions & 0 deletions packages/ruleset/src/rules/operation-summary-length.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const {
operations,
} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections');
const { oas3 } = require('@stoplight/spectral-formats');
const { operationSummaryLength } = require('../functions');

module.exports = {
description: 'Operation summaries must be 80 characters or less in length',
given: operations.map(op => `${op}.summary`),
severity: 'error',
formats: [oas3],
resolved: true,
then: {
function: operationSummaryLength,
},
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright 2017 - 2023 IBM Corporation.
* Copyright 2017 - 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

Expand Down
49 changes: 49 additions & 0 deletions packages/ruleset/test/operation-summary-length.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2024 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const { operationSummaryLength } = require('../src/rules');
const { makeCopy, rootDocument, testRule, severityCodes } = require('./utils');

const rule = operationSummaryLength;
const ruleId = 'ibm-operation-summary';
const expectedSeverity = severityCodes.error;
const expectedMsg =
'Operation summaries must be 80 characters or less in length';

describe(`Spectral rule: ${ruleId}`, () => {
describe('Should not yield errors', () => {
it('Clean spec', async () => {
const results = await testRule(ruleId, rule, rootDocument);
expect(results).toHaveLength(0);
});

it('Operation has no summary - handled by separate rule', async () => {
const testDocument = makeCopy(rootDocument);

delete testDocument.paths['/v1/drinks'].post.summary;

const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(0);
});
});

describe('Should yield errors', () => {
it('Operation summary is greater than 80 characters', async () => {
const testDocument = makeCopy(rootDocument);

testDocument.paths['/v1/drinks'].post.summary =
'This operation summary for the operation to create a new drink is simply too long';

const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);
for (const r of results) {
expect(r.code).toBe(ruleId);
expect(r.message).toBe(expectedMsg);
expect(r.severity).toBe(expectedSeverity);
expect(r.path.join('.')).toBe('paths./v1/drinks.post.summary');
}
});
});
});

0 comments on commit 901cc1a

Please sign in to comment.