Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions tools/spectral/ipa/__tests__/IPA117OperationSummaryFormat.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import testRule from './__helpers__/testRule';
import { DiagnosticSeverity } from '@stoplight/types';

testRule('xgen-IPA-117-operation-summary-format', [
{
name: 'valid description',
document: {
paths: {
'/resource/{id}': {
get: {
summary: 'Return One Resource by ID',
},
},
},
},
errors: [],
},
{
name: 'invalid descriptions',
document: {
paths: {
'/resource/{id}': {
get: {
summary: 'Return One Resource.',
},
},
'/resource': {
get: {
summary: 'Return all resources',
},
},
'/resource/{id}/child': {
get: {
summary: 'return all child resources.',
},
},
'/resource/{id}/child/{id}': {
get: {
summary: 'Return **One** Child Resource',
},
},
},
},
errors: [
{
code: 'xgen-IPA-117-operation-summary-format',
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
path: ['paths', '/resource/{id}', 'get'],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-117-operation-summary-format',
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
path: ['paths', '/resource', 'get'],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-117-operation-summary-format',
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
path: ['paths', '/resource/{id}/child', 'get'],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-117-operation-summary-format',
message: 'Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.',
path: ['paths', '/resource/{id}/child/{id}', 'get'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid description with exceptions',
document: {
paths: {
'/resource/{id}': {
get: {
summary: 'Return One Resource.',
'x-xgen-IPA-exception': {
'xgen-IPA-117-operation-summary-format': 'reason',
},
},
},
'/resource': {
get: {
summary: 'Return all resources',
'x-xgen-IPA-exception': {
'xgen-IPA-117-operation-summary-format': 'reason',
},
},
},
'/resource/{id}/child': {
get: {
summary: 'return all child resources.',
'x-xgen-IPA-exception': {
'xgen-IPA-117-operation-summary-format': 'reason',
},
},
},
},
},
errors: [],
},
]);
60 changes: 60 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-117.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ functions:
- IPA117PlaintextResponseMustHaveExample
- IPA117ObjectsMustBeWellDefined
- IPA117ParameterHasExamplesOrSchema
- IPA117OperationSummaryFormat

aliases:
OperationObject:
Expand Down Expand Up @@ -222,3 +223,62 @@ rules:
- '$.components.parameters[*]'
then:
function: 'IPA117ParameterHasExamplesOrSchema'
xgen-IPA-117-operation-summary-format:
description: |
Operation summaries must use Title Case, must not end with a period and must not use CommonMark.

##### Implementation details
The rule checks that the `summary` property of all operations are in Title Case.

##### Configuration
This rule includes two configuration options:
- `ignoreList`: Words that are allowed to maintain their specific casing (e.g., "API", "AWS", "DNS")
- `grammaticalWords`: Common words that can remain lowercase in titles (e.g., "and", "or", "the")
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-117-operation-summary-format'
severity: warn
given:
- '#OperationObject.summary'
then:
function: 'IPA117OperationSummaryFormat'
functionOptions:
ignoreList:
- 'ID'
- 'IDs'
- 'MongoDB'
- 'OpenAPI'
- 'API'
- 'AWS'
- 'GCP'
- 'IP'
- 'CIDR'
- 'DNS'
- 'LDAP'
- 'OIDC'
- 'JWKS'
- 'X.509'
- 'M2'
- 'M5'
- 'RAM'
- 'LTS'
- 'VPC'
- 'DN'
- 'CSV'
grammaticalWords:
- 'and'
- 'or'
- 'to'
- 'in'
- 'as'
- 'for'
- 'of'
- 'with'
- 'by'
- 'but'
- 'the'
- 'a'
- 'an'
- 'from'
- 'at'
- 'into'
- 'via'
- 'on'
13 changes: 13 additions & 0 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,19 @@ The rule checks for the presence of the `schema`, `examples` or `example` proper
- Operation parameters
- Parameters defined in `components/parameters`

#### xgen-IPA-117-operation-summary-format

![warn](https://img.shields.io/badge/warning-yellow)
Operation summaries must use Title Case, must not end with a period and must not use CommonMark.

##### Implementation details
The rule checks that the `summary` property of all operations are in Title Case.

##### Configuration
This rule includes two configuration options:
- `ignoreList`: Words that are allowed to maintain their specific casing (e.g., "API", "AWS", "DNS")
- `grammaticalWords`: Common words that can remain lowercase in titles (e.g., "and", "or", "the")



### IPA-118
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js';
import { isTitleCase } from './utils/casing.js';
import { resolveObject } from './utils/componentUtils.js';

export default (input, { ignoreList, grammaticalWords }, { path, rule, documentInventory }) => {
const operationObjectPath = path.slice(0, -1);
const operationObject = resolveObject(documentInventory.resolved, operationObjectPath);
const errors = checkViolationsAndReturnErrors(input, ignoreList, grammaticalWords, operationObjectPath, rule.name);
return evaluateAndCollectAdoptionStatus(errors, rule.name, operationObject, operationObjectPath);
};

function checkViolationsAndReturnErrors(summary, ignoreList, grammaticalWords, path, ruleName) {
try {
if (!isTitleCase(summary, ignoreList, grammaticalWords)) {
return [
{
path,
message: `Operation summaries must be in Title Case, must not end with a period and must not use CommonMark.`,
},
];
}
return [];
} catch (e) {
handleInternalError(ruleName, path, e);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js';
import { isTitleCase } from './utils/casing.js';

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

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

return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path);
};

function isTitleCase(str, ignoreList, grammaticalWords) {
// Split by spaces to check each word/word-group
// First character should be uppercase, rest lowercase, all alphabetical
const words = str.split(' ');

return words.every((wordGroup, index) => {
// For hyphenated words, check each part
if (wordGroup.includes('-')) {
const hyphenatedParts = wordGroup.split('-');
return hyphenatedParts.every((part) => {
if (ignoreList.includes(part)) return true;
return /^[A-Z][a-z]*$/.test(part);
});
}

// For regular words
if (ignoreList.includes(wordGroup)) return true;
if (index !== 0 && grammaticalWords.includes(wordGroup)) return true;
return /^[A-Z][a-z]*$/.test(wordGroup);
});
}
28 changes: 28 additions & 0 deletions tools/spectral/ipa/rulesets/functions/utils/casing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Check if a string is in title case.
* @param {string} str the string to check
* @param {Array<string>} ignoreList list of words to ignore (e.g. abbreviations, acronyms)
* @param {Array<string>} grammaticalWords list of grammatical words that must be lowercase (e.g., "and", "or", "the")
* @returns {boolean} true if the string is in title case, false otherwise
*/
export function isTitleCase(str, ignoreList, grammaticalWords) {
// Split by spaces to check each word/word-group
// First character should be uppercase, rest lowercase, all alphabetical
const words = str.split(' ');

return words.every((wordGroup, index) => {
// For hyphenated words, check each part
if (wordGroup.includes('-')) {
const hyphenatedParts = wordGroup.split('-');
return hyphenatedParts.every((part) => {
if (ignoreList.includes(part)) return true;
return /^[A-Z][a-z]*$/.test(part);
});
}

// For regular words
if (ignoreList.includes(wordGroup)) return true;
if (index !== 0 && grammaticalWords.includes(wordGroup)) return true;
return /^[A-Z][a-z]*$/.test(wordGroup);
});
}
Loading