From fcfa9c6e55a261e82f1056ec464a9892c5cd1bea Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Tue, 12 Aug 2025 15:43:24 +0100 Subject: [PATCH] feat(ipa): error on unneeded exceptions IPA 117-126 --- tools/spectral/ipa/CONTRIBUTING.md | 75 +++++----- .../IPA117DescriptionMustNotUseHtml.test.js | 2 +- .../IPA117DescriptionEndsWithPeriod.js | 18 +-- .../IPA117DescriptionMustNotUseHtml.js | 18 +-- .../IPA117DescriptionShouldNotUseLinks.js | 18 +-- .../IPA117DescriptionShouldNotUseTables.js | 18 +-- .../IPA117DescriptionStartsWithUpperCase.js | 18 +-- .../functions/IPA117HasDescription.js | 18 +-- .../IPA117ObjectsMustBeWellDefined.js | 18 +-- .../IPA117ParameterHasExamplesOrSchema.js | 18 +-- .../IPA117PlaintextResponseMustHaveExample.js | 18 +-- .../IPA118NoAdditionalPropertiesFalse.js | 19 +-- .../IPA119NoDefaultForCloudProviders.js | 131 ++++++------------ .../IPA121DateTimeFieldsMentionISO8601.js | 17 +-- ...IPA123EachEnumValueMustBeUpperSnakeCase.js | 18 +-- .../IPA123EnumValuesShouldNotExceed20.js | 16 +-- .../rulesets/functions/IPA124ArrayMaxItems.js | 20 +-- .../IPA125OneOfMustHaveDiscriminator.js | 18 ++- .../functions/IPA125OneOfNoBaseTypes.js | 11 +- .../IPA126TagNamesShouldUseTitleCase.js | 15 +- .../functions/utils/collectionUtils.js | 6 +- 21 files changed, 141 insertions(+), 369 deletions(-) diff --git a/tools/spectral/ipa/CONTRIBUTING.md b/tools/spectral/ipa/CONTRIBUTING.md index 304308cff2..c83db9bdcb 100644 --- a/tools/spectral/ipa/CONTRIBUTING.md +++ b/tools/spectral/ipa/CONTRIBUTING.md @@ -93,11 +93,16 @@ When your PR is approved and merged to main, the package will be automatically p Spectral custom rule functions follow this format: ```js -export default (input, _, { path, documentInventory }) +export default (input, options, context) ``` + - `input`: The current component from the OpenAPI spec. Derived from the given and field values in the rule definition. -- `path`: JSONPath array to the current component. -- `documentInventory`: The entire OpenAPI specification (use `resolved` or `unresolved` depending on rule context). +- `options`: Optional input options passed from the rule definition. +- `context`: Additional input context such as the OAS being evaluated, the following properties are commonly used: + - `context.path`: JSONPath array to the current component. + - `context.documentInventory`: The entire OpenAPI specification (use `resolved` or `unresolved` depending on rule context). + +To learn more about Spectral custom functions, see the [Spectral Documentation](https://docs.stoplight.io/docs/spectral/a781e290eb9f9-custom-functions). --- @@ -144,11 +149,14 @@ As a rule developer, you need to define: --- #### Helper Functions -Use the following helper functions from the `collectionUtils` module: +Use the following helper function from the `collectionUtils` module: + +[`evaluateAndCollectAdoptionStatus(errors, ruleName, object, jsonPath)`](https://github.com/mongodb/openapi/blob/cd4e085a68cb3bb6078e85dba85ad8ce1674f7da/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js#L14) + +Passing the validation errors, the name of the rule, the object (which may contain an exception), and the path to the object, the helper function will collect adoptions, violations and exceptions accordingly. If the object adopts the rule (i.e. there are no errors passed) but the object has an exception. An error message will be returned informing the developer to remove the unnecessary exception. + +The helper [`evaluateAndCollectAdoptionStatusWithoutExceptions(errors, ruleName, jsonPath)`](https://github.com/mongodb/openapi/blob/cd4e085a68cb3bb6078e85dba85ad8ce1674f7da/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js#L32) is used for reporting rule adoptions and violations (exceptions will be ignored - used for rules that do not allow exceptions). -- [`collectAndReturnViolation(jsonPath, ruleName, errorData)`](https://github.com/mongodb/openapi/blob/cd4e085a68cb3bb6078e85dba85ad8ce1674f7da/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js#L14) — for reporting rule violations. -- [`collectAdoption(jsonPath, ruleName)`](https://github.com/mongodb/openapi/blob/cd4e085a68cb3bb6078e85dba85ad8ce1674f7da/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js#L32) — for marking rule adoption. -- [`collectException(object, ruleName, jsonPath)`](https://github.com/mongodb/openapi/blob/cd4e085a68cb3bb6078e85dba85ad8ce1674f7da/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js#L32) — for recording rule exceptions. --- #### How to Decide the component level at which the rule will be processed @@ -183,7 +191,7 @@ When designing a rule, it is important to decide at which component level the ru } } ``` -3. In the rule implementation, use the `collectException(object, ruleName, jsonPath)` helper function to collect exceptions. The object here is what you get when you traverse the path defined by the `jsonPath`. +3. In the rule implementation, use the `evaluateAndCollectAdoptionStatus(errors, ruleName, object, jsonPath)` helper function to collect adoptions, violations and exceptions. The `object` here is what you get when you traverse the path defined by the `jsonPath`. In this example, `jsonPath` would be `['components', 'schemas', 'SchemaName]`. Exceptions can be defined at different levels, such as: - Resource level @@ -274,14 +282,13 @@ A rule must collect **only one** of the following for each evaluation: You can include **multiple error messages** for a violation. To do so: - Gather the messages into an array - - Pass them to `collectAndReturnViolation` + - Pass them to `evaluateAndCollectAdoptionStatus` ###### Considerations - Use the **same `jsonPath`** for: - - `collectAndReturnViolation` - - `collectAdoption` - - `collectException` + - The `path` property in the errors/violations collected + - The `path` property passed to `evaluateAndCollectAdoptionStatus` > 💡 This path should either be the `path` parameter from the rule function or a derived value from it. @@ -294,39 +301,39 @@ You can include **multiple error messages** for a violation. To do so: const RULE_NAME = 'xgen-IPA-xxx-rule-name' export default (input, opts, { path, documentInventory }) => { - //Optional filter cases that we do not want to handle + // Optional filter cases that we do not want to handle // Return no response for those cases. - - //Decide the jsonPath (component level) at which you want to collect exceptions, adoption, and violation - //It can be "path" parameter of custom rule function - //Or, a derived path from "path" parameter - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, jsonPath); - return; + if (!Array.isArray(input)) { + return; } + + // Decide the jsonPath (component level) at which you want to collect exceptions, adoption, and violation + // It can be "path" parameter of custom rule function + // Or, a derived path from "path" parameter - errors = checkViolationsAndReturnErrors(...); - if (errors.length != 0) { - return collectAndReturnViolation(jsonPath, RULE_NAME, errors); - } - return collectAdoption(jsonPath, RULE_NAME); + const errors = checkViolationsAndReturnErrors(input); + + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; -//This function can accept "input", "path", "documentInventory", or other derived parameters -function checkViolationsAndReturnErrors(...){ +// This function can accept "input", "path", "documentInventory", or other derived parameters +function checkViolationsAndReturnErrors(enums){ try { const errors = []; - for (const value of enumValues) { - if (!isUpperSnakeCase(value)) { - errors.push({ - path: [...path, 'enum'], - message: `${value} is not in UPPER_SNAKE_CASE`, - }); - } + enums.forEach((index, enumValue) => { + if (!isUpperSnakeCase(enumValue)) { + // Append the enum index to the path to collect multiple errors + errors.push({ + path: [...path, index], + message: `${enumValue} is not in UPPER_SNAKE_CASE`, + }); + } + }); } return errors; } catch(e) { + // Use common error handler to return useful error messages in case of unexpected failures handleInternalError(RULE_NAME, jsonPathArray, e); } } diff --git a/tools/spectral/ipa/__tests__/IPA117DescriptionMustNotUseHtml.test.js b/tools/spectral/ipa/__tests__/IPA117DescriptionMustNotUseHtml.test.js index 000a895b9f..79f9132538 100644 --- a/tools/spectral/ipa/__tests__/IPA117DescriptionMustNotUseHtml.test.js +++ b/tools/spectral/ipa/__tests__/IPA117DescriptionMustNotUseHtml.test.js @@ -97,7 +97,7 @@ testRule('xgen-IPA-117-description-must-not-use-html', [ }, }, selfClosingHtml: { - description: 'This is something.
With a line break.', + description: 'This is something.
With a line break.', 'x-xgen-IPA-exception': { 'xgen-IPA-117-description-must-not-use-html': 'reason', }, diff --git a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionEndsWithPeriod.js b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionEndsWithPeriod.js index 2ac45dc343..b7d80c3d23 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionEndsWithPeriod.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionEndsWithPeriod.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-117-description-ends-with-period'; const ERROR_MESSAGE_PERIOD = 'Descriptions must end with a full stop(.).'; @@ -15,16 +9,8 @@ export default (input, opts, { path }) => { return; } - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input['description'], path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(description, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionMustNotUseHtml.js b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionMustNotUseHtml.js index 6a315fa35a..39e4f12931 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionMustNotUseHtml.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionMustNotUseHtml.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-117-description-must-not-use-html'; const ERROR_MESSAGE = 'Descriptions must not use raw HTML.'; @@ -15,16 +9,8 @@ export default (input, opts, { path }) => { return; } - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input['description'], path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(description, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseLinks.js b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseLinks.js index d7d09c2ac8..f289657a13 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseLinks.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseLinks.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-117-description-should-not-use-inline-links'; const ERROR_MESSAGE = @@ -16,16 +10,8 @@ export default (input, opts, { path }) => { return; } - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input['description'], path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(description, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseTables.js b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseTables.js index ae713cd37b..46dc9ac566 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseTables.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionShouldNotUseTables.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-117-description-should-not-use-inline-tables'; const ERROR_MESSAGE = @@ -16,16 +10,8 @@ export default (input, opts, { path }) => { return; } - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input['description'], path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(description, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionStartsWithUpperCase.js b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionStartsWithUpperCase.js index 61b964c660..c6c104928e 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117DescriptionStartsWithUpperCase.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117DescriptionStartsWithUpperCase.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-117-description-starts-with-uppercase'; const ERROR_MESSAGE_UPPER_CASE = 'Descriptions must start with Uppercase.'; @@ -14,16 +8,8 @@ export default (input, opts, { path }) => { return; } - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input['description'], path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(description, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117HasDescription.js b/tools/spectral/ipa/rulesets/functions/IPA117HasDescription.js index 4b986f9c5d..f08aa23912 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117HasDescription.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117HasDescription.js @@ -1,26 +1,12 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-117-description'; const ERROR_MESSAGE = 'Description not found. API producers must provide descriptions for Properties, Operations and Parameters.'; export default (input, opts, { path }) => { - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input, path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(input, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js b/tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js index f7672ffe01..01d91097fb 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; import { pathIsForRequestVersion, pathIsForResponseVersion } from './utils/componentUtils.js'; import { schemaIsObject } from './utils/schemaUtils.js'; @@ -47,16 +41,8 @@ export default (input, { ignoredPaths }, { path }) => { return; } - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input, path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(object, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js b/tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js index 3960192b24..7ca5d0c4b8 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js @@ -1,25 +1,11 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-117-parameter-has-examples-or-schema'; const ERROR_MESSAGE = 'API producers must provide a well-defined schema or example(s) for parameters.'; export default (input, _, { path }) => { - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(input, path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(object, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA117PlaintextResponseMustHaveExample.js b/tools/spectral/ipa/rulesets/functions/IPA117PlaintextResponseMustHaveExample.js index 0257909627..6d803ed3c2 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA117PlaintextResponseMustHaveExample.js +++ b/tools/spectral/ipa/rulesets/functions/IPA117PlaintextResponseMustHaveExample.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; import { resolveObject } from './utils/componentUtils.js'; const RULE_NAME = 'xgen-IPA-117-plaintext-response-must-have-example'; @@ -40,16 +34,8 @@ export default (input, { allowedTypes }, { documentInventory, path }) => { return; } - if (hasException(response, RULE_NAME)) { - collectException(response, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(response, path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, response, path); }; function checkViolationsAndReturnErrors(response, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA118NoAdditionalPropertiesFalse.js b/tools/spectral/ipa/rulesets/functions/IPA118NoAdditionalPropertiesFalse.js index e72a647350..b590dcb543 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA118NoAdditionalPropertiesFalse.js +++ b/tools/spectral/ipa/rulesets/functions/IPA118NoAdditionalPropertiesFalse.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; import { resolveObject } from './utils/componentUtils.js'; import { findAdditionalPropertiesFalsePaths } from './utils/compareUtils.js'; @@ -21,17 +15,8 @@ export default (input, _, { path, documentInventory }) => { const oas = documentInventory.unresolved; const schemaObject = resolveObject(oas, path); - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - const errors = checkViolationsAndReturnErrors(schemaObject, path); - if (errors.length > 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(schemaObject, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA119NoDefaultForCloudProviders.js b/tools/spectral/ipa/rulesets/functions/IPA119NoDefaultForCloudProviders.js index ff6a8d28b5..dc76f2b686 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA119NoDefaultForCloudProviders.js +++ b/tools/spectral/ipa/rulesets/functions/IPA119NoDefaultForCloudProviders.js @@ -1,10 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; import { resolveObject } from './utils/componentUtils.js'; const RULE_NAME = 'xgen-IPA-119-no-default-for-cloud-providers'; @@ -18,101 +12,68 @@ export default (input, { propertyNameToLookFor, cloudProviderEnumValues }, { pat return; } - if (hasException(propertyObject, RULE_NAME)) { - collectException(propertyObject, RULE_NAME, path); + if (!inputIsCloudProviderField(fieldType, input, propertyObject, propertyNameToLookFor, cloudProviderEnumValues)) { return; } - const result = checkViolationsAndReturnErrors( - input, - propertyObject, - path, - propertyNameToLookFor, - fieldType, - cloudProviderEnumValues - ); - if (result.errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, result.errors); - } - if (result.isCloudProviderField) { - collectAdoption(path, RULE_NAME); - } + const errors = checkViolationsAndReturnErrors(propertyObject, path, fieldType); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, propertyObject, path); }; -function checkViolationsAndReturnErrors( +function inputIsCloudProviderField( + fieldType, propertyName, propertyObject, - path, propertyNameToLookFor, - fieldType, cloudProviderEnumValues ) { + let isCloudProviderField = false; + if (fieldType === 'properties') { + if (propertyName === propertyNameToLookFor) { + isCloudProviderField = true; + } + + if (Array.isArray(propertyObject.enum) && propertyObject.enum.length > 0) { + isCloudProviderField = cloudProviderEnumValues.every((cloudProviderValue) => + propertyObject.enum.includes(cloudProviderValue) + ); + } + } else if (fieldType === 'parameters') { + if (propertyObject.name === propertyNameToLookFor) { + isCloudProviderField = true; + } + + if (Array.isArray(propertyObject.schema.enum) && propertyObject.schema.enum.length > 0) { + isCloudProviderField = cloudProviderEnumValues.every((cloudProviderValue) => + propertyObject.schema.enum.includes(cloudProviderValue) + ); + } + } + return isCloudProviderField; +} + +function checkViolationsAndReturnErrors(propertyObject, path, fieldType) { try { - const result = { - errors: [], - isCloudProviderField: false, - }; + const errors = []; if (fieldType === 'properties') { - if (propertyName === propertyNameToLookFor) { - result.isCloudProviderField = true; - if (propertyObject.default !== undefined) { - result.errors.push({ - path, - message: ERROR_MESSAGE, - }); - return result; - } - } - - if (Array.isArray(propertyObject.enum) && propertyObject.enum.length > 0) { - const enumValues = propertyObject.enum; - const hasCloudProviderEnumValue = cloudProviderEnumValues.every((cloudProviderValue) => - enumValues.includes(cloudProviderValue) - ); - if (hasCloudProviderEnumValue) { - result.isCloudProviderField = true; - if (propertyObject.default !== undefined) { - result.errors.push({ - path, - message: ERROR_MESSAGE, - }); - return result; - } - } + if (propertyObject.default !== undefined) { + errors.push({ + path, + message: ERROR_MESSAGE, + }); + return errors; } } else if (fieldType === 'parameters') { - if (propertyObject.name === propertyNameToLookFor) { - result.isCloudProviderField = true; - if (propertyObject.schema.default !== undefined) { - result.errors.push({ - path, - message: ERROR_MESSAGE, - }); - return result; - } - } - - if (Array.isArray(propertyObject.schema.enum) && propertyObject.schema.enum.length > 0) { - const enumValues = propertyObject.schema.enum; - const hasCloudProviderEnumValue = cloudProviderEnumValues.every((cloudProviderValue) => - enumValues.includes(cloudProviderValue) - ); - - if (hasCloudProviderEnumValue) { - result.isCloudProviderField = true; - if (propertyObject.schema.default !== undefined) { - result.errors.push({ - path, - message: ERROR_MESSAGE, - }); - return result; - } - } + if (propertyObject.schema.default !== undefined) { + errors.push({ + path, + message: ERROR_MESSAGE, + }); + return errors; } } - - return result; + return errors; } catch (e) { handleInternalError(RULE_NAME, path, e); } diff --git a/tools/spectral/ipa/rulesets/functions/IPA121DateTimeFieldsMentionISO8601.js b/tools/spectral/ipa/rulesets/functions/IPA121DateTimeFieldsMentionISO8601.js index 2dac2f5355..b6dcda874d 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA121DateTimeFieldsMentionISO8601.js +++ b/tools/spectral/ipa/rulesets/functions/IPA121DateTimeFieldsMentionISO8601.js @@ -1,5 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-121-date-time-fields-mention-iso-8601'; const ERROR_MESSAGE = @@ -7,30 +6,26 @@ const ERROR_MESSAGE = export default (input, options, { path }) => { const fieldType = path[path.length - 2]; - - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } + const description = input.description; let format; - let description = input.description; if (fieldType === 'parameters') { format = input.schema?.format; } else if (fieldType === 'properties') { format = input.format; } + let errors = []; if (format === 'date-time') { if (!description?.includes('ISO 8601') && !description?.includes('UTC')) { - return collectAndReturnViolation(path, RULE_NAME, [ + errors = [ { path, message: ERROR_MESSAGE, }, - ]); + ]; } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); } }; diff --git a/tools/spectral/ipa/rulesets/functions/IPA123EachEnumValueMustBeUpperSnakeCase.js b/tools/spectral/ipa/rulesets/functions/IPA123EachEnumValueMustBeUpperSnakeCase.js index 5b8374ebc6..25cd8a3dcc 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA123EachEnumValueMustBeUpperSnakeCase.js +++ b/tools/spectral/ipa/rulesets/functions/IPA123EachEnumValueMustBeUpperSnakeCase.js @@ -1,12 +1,6 @@ -import { hasException } from './utils/exceptions.js'; import { resolveObject } from './utils/componentUtils.js'; import { casing } from '@stoplight/spectral-functions'; -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; import { getSchemaPathFromEnumPath } from './utils/schemaUtils.js'; const RULE_NAME = 'xgen-IPA-123-enum-values-must-be-upper-snake-case'; @@ -17,16 +11,8 @@ export default (input, _, { path, documentInventory }) => { const schemaPath = getSchemaPathFromEnumPath(path); const schemaObject = resolveObject(oas, schemaPath); - if (hasException(schemaObject, RULE_NAME)) { - collectException(schemaObject, RULE_NAME, schemaPath); - return; - } - const errors = checkViolationsAndReturnErrors(input, schemaPath); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(schemaPath, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, schemaObject, path); }; function checkViolationsAndReturnErrors(input, schemaPath) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA123EnumValuesShouldNotExceed20.js b/tools/spectral/ipa/rulesets/functions/IPA123EnumValuesShouldNotExceed20.js index 06aa15bf1a..1870117920 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA123EnumValuesShouldNotExceed20.js +++ b/tools/spectral/ipa/rulesets/functions/IPA123EnumValuesShouldNotExceed20.js @@ -1,5 +1,4 @@ -import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js'; import { getSchemaPathFromEnumPath } from './utils/schemaUtils.js'; import { resolveObject } from './utils/componentUtils.js'; @@ -16,24 +15,19 @@ export default (input, { maxEnumValues }, { path, documentInventory }) => { return; } - // Check for exceptions - if (hasException(schemaObject, RULE_NAME)) { - collectException(schemaObject, RULE_NAME, path); - return; - } - if (!Array.isArray(input)) { return; } + let errors = []; if (input.length > maxEnumValues) { - return collectAndReturnViolation(path, RULE_NAME, [ + errors = [ { path, message: `${ERROR_MESSAGE}${input.length}`, }, - ]); + ]; } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, schemaObject, path); }; diff --git a/tools/spectral/ipa/rulesets/functions/IPA124ArrayMaxItems.js b/tools/spectral/ipa/rulesets/functions/IPA124ArrayMaxItems.js index 429396a920..a7bcfd308b 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA124ArrayMaxItems.js +++ b/tools/spectral/ipa/rulesets/functions/IPA124ArrayMaxItems.js @@ -1,10 +1,4 @@ -import { - collectAdoption, - collectAndReturnViolation, - collectException, - handleInternalError, -} from './utils/collectionUtils.js'; -import { hasException } from './utils/exceptions.js'; +import { evaluateAndCollectAdoptionStatus, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-124-array-max-items'; @@ -16,12 +10,6 @@ const RULE_NAME = 'xgen-IPA-124-array-max-items'; * @param {object} context - The context object containing the path and documentInventory */ export default (input, { maxAllowedValue, ignore = [] }, { path }) => { - // Check for exception at the schema level - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - let propertyName; if (path.includes('parameters')) { propertyName = input.name; @@ -37,11 +25,7 @@ export default (input, { maxAllowedValue, ignore = [] }, { path }) => { } const errors = checkViolationsAndReturnErrors(input, path, maxAllowedValue); - if (errors.length > 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function checkViolationsAndReturnErrors(input, path, maxAllowedValue) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA125OneOfMustHaveDiscriminator.js b/tools/spectral/ipa/rulesets/functions/IPA125OneOfMustHaveDiscriminator.js index f483cbeb5f..f888cce86a 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA125OneOfMustHaveDiscriminator.js +++ b/tools/spectral/ipa/rulesets/functions/IPA125OneOfMustHaveDiscriminator.js @@ -1,6 +1,5 @@ -import { collectAdoption, collectAndReturnViolation } from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js'; import { resolveObject } from './utils/componentUtils.js'; -import { hasException } from './utils/exceptions.js'; const RULE_NAME = 'xgen-IPA-125-oneOf-must-have-discriminator'; const MISSING_DISCRIMINATOR_MESSAGE = 'The schema has oneOf but no discriminator property.'; @@ -15,15 +14,9 @@ export default (input, _, { path, documentInventory }) => { return; } const schema = resolveObject(documentInventory.unresolved, path); - if (hasException(schema, RULE_NAME)) { - return; - } const errors = checkViolationsAndReturnErrors(schema, path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, schema, path); }; function checkViolationsAndReturnErrors(schema, path) { @@ -57,7 +50,12 @@ function checkViolationsAndReturnErrors(schema, path) { // Check for discriminator mappings that don't match any oneOf reference const unmatchedMappings = mappingValues.filter((mapping) => !oneOfRefs.includes(mapping)); if (unmatchedMappings.length > 0) { - return [{ path, message: `${MAPPING_ERROR_MESSAGE} ${unmatchedMappings.join(', ')}` }]; + return [ + { + path, + message: `${MAPPING_ERROR_MESSAGE} ${unmatchedMappings.join(', ')}`, + }, + ]; } return []; } diff --git a/tools/spectral/ipa/rulesets/functions/IPA125OneOfNoBaseTypes.js b/tools/spectral/ipa/rulesets/functions/IPA125OneOfNoBaseTypes.js index 9bb3b2f89c..1280c7b95e 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA125OneOfNoBaseTypes.js +++ b/tools/spectral/ipa/rulesets/functions/IPA125OneOfNoBaseTypes.js @@ -1,6 +1,5 @@ -import { collectAdoption, collectAndReturnViolation } from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js'; import { resolveObject } from './utils/componentUtils.js'; -import { hasException } from './utils/exceptions.js'; const RULE_NAME = 'xgen-IPA-125-oneOf-no-base-types'; const ERROR_MESSAGE_MIXED = 'oneOf should not mix base types with references.'; @@ -11,15 +10,9 @@ export default (input, _, { path, documentInventory }) => { return; } const schema = resolveObject(documentInventory.unresolved, path); - if (hasException(schema, RULE_NAME)) { - return; - } const errors = checkViolationsAndReturnErrors(schema, path); - if (errors.length !== 0) { - return collectAndReturnViolation(path, RULE_NAME, errors); - } - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, schema, path); }; function checkViolationsAndReturnErrors(schema, path) { diff --git a/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js b/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js index 6b17a9b415..4860cb0401 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js +++ b/tools/spectral/ipa/rulesets/functions/IPA126TagNamesShouldUseTitleCase.js @@ -1,27 +1,22 @@ -import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { evaluateAndCollectAdoptionStatus } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-126-tag-names-should-use-title-case'; export default (input, { ignoreList, grammaticalWords }, { path }) => { const tagName = input.name; - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } // Check if the tag name uses Title Case + let errors = []; if (!isTitleCase(tagName, ignoreList, grammaticalWords)) { - return collectAndReturnViolation(path, RULE_NAME, [ + errors = [ { path, message: `Tag name should use Title Case, found: "${tagName}".`, }, - ]); + ]; } - // Tag name uses Title Case - collectAdoption(path, RULE_NAME); + return evaluateAndCollectAdoptionStatus(errors, RULE_NAME, input, path); }; function isTitleCase(str, ignoreList, grammaticalWords) { diff --git a/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js b/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js index 2071aa0a00..c55ad9689c 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/collectionUtils.js @@ -57,7 +57,7 @@ export function evaluateAndCollectAdoptionStatusWithoutExceptions(validationErro * @throws {Error} Throws an error if errorData is neither a string nor an array. * */ -export function collectAndReturnViolation(jsonPath, ruleName, errorData) { +function collectAndReturnViolation(jsonPath, ruleName, errorData) { collector.add(EntryType.VIOLATION, jsonPath, ruleName); if (typeof errorData === 'string') { @@ -75,7 +75,7 @@ export function collectAndReturnViolation(jsonPath, ruleName, errorData) { * @param {Array} jsonPath - The JSON path array for the object where the rule violation occurred. Example: ["paths","./pets","get"] * @param {string} ruleName - The name of the rule that was adopted. */ -export function collectAdoption(jsonPath, ruleName) { +function collectAdoption(jsonPath, ruleName) { collector.add(EntryType.ADOPTION, jsonPath, ruleName); } @@ -86,7 +86,7 @@ export function collectAdoption(jsonPath, ruleName) { * @param {Array} jsonPath - The JSON path array for the object where the rule violation occurred. Example: ["paths","./pets","get"] * @param {string} ruleName - The name of the rule that the exception is defined for. */ -export function collectException(object, ruleName, jsonPath) { +function collectException(object, ruleName, jsonPath) { let exceptionReason = object[EXCEPTION_EXTENSION][ruleName]; if (exceptionReason) { collector.add(EntryType.EXCEPTION, jsonPath, ruleName, exceptionReason);