diff --git a/tools/spectral/ipa/__tests__/IPA117OperationSummaryFormat.test.js b/tools/spectral/ipa/__tests__/IPA117OperationSummaryFormat.test.js new file mode 100644 index 0000000000..851eb56780 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA117OperationSummaryFormat.test.js @@ -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: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-117.yaml b/tools/spectral/ipa/rulesets/IPA-117.yaml index 81badc83bf..9973e60c99 100644 --- a/tools/spectral/ipa/rulesets/IPA-117.yaml +++ b/tools/spectral/ipa/rulesets/IPA-117.yaml @@ -11,6 +11,7 @@ functions: - IPA117PlaintextResponseMustHaveExample - IPA117ObjectsMustBeWellDefined - IPA117ParameterHasExamplesOrSchema + - IPA117OperationSummaryFormat aliases: OperationObject: @@ -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' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 9e09ebb026..34d9452bb0 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -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 diff --git a/tools/spectral/ipa/rulesets/functions/IPA117OperationSummaryFormat.js b/tools/spectral/ipa/rulesets/functions/IPA117OperationSummaryFormat.js new file mode 100644 index 0000000000..a493efa5e4 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA117OperationSummaryFormat.js @@ -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); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js b/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js index 4860cb0401..85a59dd938 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js +++ b/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js @@ -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'; @@ -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); - }); -} diff --git a/tools/spectral/ipa/rulesets/functions/utils/casing.js b/tools/spectral/ipa/rulesets/functions/utils/casing.js new file mode 100644 index 0000000000..dc4cfda59c --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/utils/casing.js @@ -0,0 +1,28 @@ +/** + * Check if a string is in title case. + * @param {string} str the string to check + * @param {Array} ignoreList list of words to ignore (e.g. abbreviations, acronyms) + * @param {Array} 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); + }); +}