Skip to content

Commit 4397cf0

Browse files
feat(ipa): new rule xgen-IPA-117-operation-summary-format (#897)
1 parent 75f3934 commit 4397cf0

File tree

6 files changed

+231
-22
lines changed

6 files changed

+231
-22
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import testRule from './__helpers__/testRule';
2+
import { DiagnosticSeverity } from '@stoplight/types';
3+
4+
testRule('xgen-IPA-117-operation-summary-format', [
5+
{
6+
name: 'valid description',
7+
document: {
8+
paths: {
9+
'/resource/{id}': {
10+
get: {
11+
summary: 'Return One Resource by ID',
12+
},
13+
},
14+
},
15+
},
16+
errors: [],
17+
},
18+
{
19+
name: 'invalid descriptions',
20+
document: {
21+
paths: {
22+
'/resource/{id}': {
23+
get: {
24+
summary: 'Return One Resource.',
25+
},
26+
},
27+
'/resource': {
28+
get: {
29+
summary: 'Return all resources',
30+
},
31+
},
32+
'/resource/{id}/child': {
33+
get: {
34+
summary: 'return all child resources.',
35+
},
36+
},
37+
'/resource/{id}/child/{id}': {
38+
get: {
39+
summary: 'Return **One** Child Resource',
40+
},
41+
},
42+
},
43+
},
44+
errors: [
45+
{
46+
code: 'xgen-IPA-117-operation-summary-format',
47+
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
48+
path: ['paths', '/resource/{id}', 'get'],
49+
severity: DiagnosticSeverity.Warning,
50+
},
51+
{
52+
code: 'xgen-IPA-117-operation-summary-format',
53+
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
54+
path: ['paths', '/resource', 'get'],
55+
severity: DiagnosticSeverity.Warning,
56+
},
57+
{
58+
code: 'xgen-IPA-117-operation-summary-format',
59+
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
60+
path: ['paths', '/resource/{id}/child', 'get'],
61+
severity: DiagnosticSeverity.Warning,
62+
},
63+
{
64+
code: 'xgen-IPA-117-operation-summary-format',
65+
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
66+
path: ['paths', '/resource/{id}/child/{id}', 'get'],
67+
severity: DiagnosticSeverity.Warning,
68+
},
69+
],
70+
},
71+
{
72+
name: 'invalid description with exceptions',
73+
document: {
74+
paths: {
75+
'/resource/{id}': {
76+
get: {
77+
summary: 'Return One Resource.',
78+
'x-xgen-IPA-exception': {
79+
'xgen-IPA-117-operation-summary-format': 'reason',
80+
},
81+
},
82+
},
83+
'/resource': {
84+
get: {
85+
summary: 'Return all resources',
86+
'x-xgen-IPA-exception': {
87+
'xgen-IPA-117-operation-summary-format': 'reason',
88+
},
89+
},
90+
},
91+
'/resource/{id}/child': {
92+
get: {
93+
summary: 'return all child resources.',
94+
'x-xgen-IPA-exception': {
95+
'xgen-IPA-117-operation-summary-format': 'reason',
96+
},
97+
},
98+
},
99+
},
100+
},
101+
errors: [],
102+
},
103+
]);

tools/spectral/ipa/rulesets/IPA-117.yaml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ functions:
1111
- IPA117PlaintextResponseMustHaveExample
1212
- IPA117ObjectsMustBeWellDefined
1313
- IPA117ParameterHasExamplesOrSchema
14+
- IPA117OperationSummaryFormat
1415

1516
aliases:
1617
OperationObject:
@@ -222,3 +223,62 @@ rules:
222223
- '$.components.parameters[*]'
223224
then:
224225
function: 'IPA117ParameterHasExamplesOrSchema'
226+
xgen-IPA-117-operation-summary-format:
227+
description: |
228+
Operation summaries must use Title Case, must not end with a period and must not use CommonMark.
229+
230+
##### Implementation details
231+
The rule checks that the `summary` property of all operations are in Title Case.
232+
233+
##### Configuration
234+
This rule includes two configuration options:
235+
- `ignoreList`: Words that are allowed to maintain their specific casing (e.g., "API", "AWS", "DNS")
236+
- `grammaticalWords`: Common words that can remain lowercase in titles (e.g., "and", "or", "the")
237+
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-117-operation-summary-format'
238+
severity: warn
239+
given:
240+
- '#OperationObject.summary'
241+
then:
242+
function: 'IPA117OperationSummaryFormat'
243+
functionOptions:
244+
ignoreList:
245+
- 'ID'
246+
- 'IDs'
247+
- 'MongoDB'
248+
- 'OpenAPI'
249+
- 'API'
250+
- 'AWS'
251+
- 'GCP'
252+
- 'IP'
253+
- 'CIDR'
254+
- 'DNS'
255+
- 'LDAP'
256+
- 'OIDC'
257+
- 'JWKS'
258+
- 'X.509'
259+
- 'M2'
260+
- 'M5'
261+
- 'RAM'
262+
- 'LTS'
263+
- 'VPC'
264+
- 'DN'
265+
- 'CSV'
266+
grammaticalWords:
267+
- 'and'
268+
- 'or'
269+
- 'to'
270+
- 'in'
271+
- 'as'
272+
- 'for'
273+
- 'of'
274+
- 'with'
275+
- 'by'
276+
- 'but'
277+
- 'the'
278+
- 'a'
279+
- 'an'
280+
- 'from'
281+
- 'at'
282+
- 'into'
283+
- 'via'
284+
- 'on'

tools/spectral/ipa/rulesets/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,19 @@ The rule checks for the presence of the `schema`, `examples` or `example` proper
851851
- Operation parameters
852852
- Parameters defined in `components/parameters`
853853

854+
#### xgen-IPA-117-operation-summary-format
855+
856+
![warn](https://img.shields.io/badge/warning-yellow)
857+
Operation summaries must use Title Case, must not end with a period and must not use CommonMark.
858+
859+
##### Implementation details
860+
The rule checks that the `summary` property of all operations are in Title Case.
861+
862+
##### Configuration
863+
This rule includes two configuration options:
864+
- `ignoreList`: Words that are allowed to maintain their specific casing (e.g., "API", "AWS", "DNS")
865+
- `grammaticalWords`: Common words that can remain lowercase in titles (e.g., "and", "or", "the")
866+
854867

855868

856869
### IPA-118
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js';
2+
import { isTitleCase } from './utils/casing.js';
3+
import { resolveObject } from './utils/componentUtils.js';
4+
5+
export default (input, { ignoreList, grammaticalWords }, { path, rule, documentInventory }) => {
6+
const operationObjectPath = path.slice(0, -1);
7+
const operationObject = resolveObject(documentInventory.resolved, operationObjectPath);
8+
const errors = checkViolationsAndReturnErrors(input, ignoreList, grammaticalWords, operationObjectPath, rule.name);
9+
return evaluateAndCollectAdoptionStatus(errors, rule.name, operationObject, operationObjectPath);
10+
};
11+
12+
function checkViolationsAndReturnErrors(summary, ignoreList, grammaticalWords, path, ruleName) {
13+
try {
14+
if (!isTitleCase(summary, ignoreList, grammaticalWords)) {
15+
return [
16+
{
17+
path,
18+
message: `Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.`,
19+
},
20+
];
21+
}
22+
return [];
23+
} catch (e) {
24+
handleInternalError(ruleName, path, e);
25+
}
26+
}
Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js';
2+
import { isTitleCase } from './utils/casing.js';
23

34
const RULE_NAME = 'xgen-IPA-126-tag-names-should-use-title-case';
45

@@ -18,25 +19,3 @@ export default (input, { ignoreList, grammaticalWords }, { path }) => {
1819

1920
return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path);
2021
};
21-
22-
function isTitleCase(str, ignoreList, grammaticalWords) {
23-
// Split by spaces to check each word/word-group
24-
// First character should be uppercase, rest lowercase, all alphabetical
25-
const words = str.split(' ');
26-
27-
return words.every((wordGroup, index) => {
28-
// For hyphenated words, check each part
29-
if (wordGroup.includes('-')) {
30-
const hyphenatedParts = wordGroup.split('-');
31-
return hyphenatedParts.every((part) => {
32-
if (ignoreList.includes(part)) return true;
33-
return /^[A-Z][a-z]*$/.test(part);
34-
});
35-
}
36-
37-
// For regular words
38-
if (ignoreList.includes(wordGroup)) return true;
39-
if (index !== 0 && grammaticalWords.includes(wordGroup)) return true;
40-
return /^[A-Z][a-z]*$/.test(wordGroup);
41-
});
42-
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Check if a string is in title case.
3+
* @param {string} str the string to check
4+
* @param {Array<string>} ignoreList list of words to ignore (e.g. abbreviations, acronyms)
5+
* @param {Array<string>} grammaticalWords list of grammatical words that must be lowercase (e.g., "and", "or", "the")
6+
* @returns {boolean} true if the string is in title case, false otherwise
7+
*/
8+
export function isTitleCase(str, ignoreList, grammaticalWords) {
9+
// Split by spaces to check each word/word-group
10+
// First character should be uppercase, rest lowercase, all alphabetical
11+
const words = str.split(' ');
12+
13+
return words.every((wordGroup, index) => {
14+
// For hyphenated words, check each part
15+
if (wordGroup.includes('-')) {
16+
const hyphenatedParts = wordGroup.split('-');
17+
return hyphenatedParts.every((part) => {
18+
if (ignoreList.includes(part)) return true;
19+
return /^[A-Z][a-z]*$/.test(part);
20+
});
21+
}
22+
23+
// For regular words
24+
if (ignoreList.includes(wordGroup)) return true;
25+
if (index !== 0 && grammaticalWords.includes(wordGroup)) return true;
26+
return /^[A-Z][a-z]*$/.test(wordGroup);
27+
});
28+
}

0 commit comments

Comments
 (0)