diff --git a/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js b/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js index cb02e27347..cf5c874530 100644 --- a/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js +++ b/tools/spectral/ipa/__tests__/createMethodShouldNotHaveQueryParameters.test.js @@ -15,6 +15,13 @@ const componentSchemas = { type: 'string', }, }, + QueryParam2: { + name: 'query-param-2', + in: 'query', + schema: { + type: 'string', + }, + }, PathParam: { name: 'resource-id', in: 'path', @@ -102,6 +109,9 @@ testRule('xgen-IPA-106-create-method-should-not-have-query-parameters', [ { $ref: '#/components/parameters/QueryParam', }, + { + $ref: '#/components/parameters/QueryParam2', + }, ], }, }, @@ -110,13 +120,20 @@ testRule('xgen-IPA-106-create-method-should-not-have-query-parameters', [ errors: [ { code: 'xgen-IPA-106-create-method-should-not-have-query-parameters', - message: 'Create operations should not have query parameters. http://go/ipa/106', + message: 'Input parameter [filter]: Create operations should not have query parameters. http://go/ipa/106', path: ['paths', '/resource', 'post'], severity: DiagnosticSeverity.Warning, }, { code: 'xgen-IPA-106-create-method-should-not-have-query-parameters', - message: 'Create operations should not have query parameters. http://go/ipa/106', + message: 'Input parameter [query-param]: Create operations should not have query parameters. http://go/ipa/106', + path: ['paths', '/resource2', 'post'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-106-create-method-should-not-have-query-parameters', + message: + 'Input parameter [query-param-2]: Create operations should not have query parameters. http://go/ipa/106', path: ['paths', '/resource2', 'post'], severity: DiagnosticSeverity.Warning, }, diff --git a/tools/spectral/ipa/rulesets/IPA-104.yaml b/tools/spectral/ipa/rulesets/IPA-104.yaml index 4fcb8b6d6d..1905849d55 100644 --- a/tools/spectral/ipa/rulesets/IPA-104.yaml +++ b/tools/spectral/ipa/rulesets/IPA-104.yaml @@ -22,8 +22,9 @@ rules: description: 'The purpose of the Get method is to return data from a single resource. http://go/ipa/104' message: '{{error}} http://go/ipa/104' severity: warn - given: '$.paths[*].get' + given: '$.paths[*].get.responses[*].content' then: + field: '@key' function: 'getMethodReturnsSingleResource' xgen-IPA-104-get-method-response-code-is-200: description: 'The Get method must return a 200 OK response. http://go/ipa/104' diff --git a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsRequestSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsRequestSuffixedObject.js index 3ec368dd59..6a88bea280 100644 --- a/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsRequestSuffixedObject.js +++ b/tools/spectral/ipa/rulesets/functions/createMethodRequestBodyIsRequestSuffixedObject.js @@ -1,5 +1,10 @@ import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; import { getSchemaRef } from './utils/methodUtils.js'; @@ -11,28 +16,37 @@ const ERROR_MESSAGE_SCHEMA_REF = 'The response body schema is defined inline and export default (input, _, { path, documentInventory }) => { const oas = documentInventory.unresolved; const resourcePath = path[1]; - const contentMediaType = path[path.length - 1]; + const contentPerMediaType = resolveObject(oas, path); - if (isCustomMethodIdentifier(resourcePath) || !contentMediaType.endsWith('json')) { + if (isCustomMethodIdentifier(resourcePath) || !input.endsWith('json') || !contentPerMediaType.schema) { return; } - const contentPerMediaType = resolveObject(oas, path); - if (hasException(contentPerMediaType, RULE_NAME)) { collectException(contentPerMediaType, RULE_NAME, path); return; } - if (contentPerMediaType.schema) { + const errors = checkViolationsAndReturnErrors(contentPerMediaType, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(contentPerMediaType, path) { + try { const schema = contentPerMediaType.schema; const schemaRef = getSchemaRef(schema); + if (!schemaRef) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_REF); + return [{ path, message: ERROR_MESSAGE_SCHEMA_REF }]; } if (!schemaRef.endsWith('Request')) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_NAME); + return [{ path, message: ERROR_MESSAGE_SCHEMA_NAME }]; } - collectAdoption(path, RULE_NAME); + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); } -}; +} diff --git a/tools/spectral/ipa/rulesets/functions/createMethodShouldNotHaveQueryParameters.js b/tools/spectral/ipa/rulesets/functions/createMethodShouldNotHaveQueryParameters.js index b1a910a553..6f748d7e62 100644 --- a/tools/spectral/ipa/rulesets/functions/createMethodShouldNotHaveQueryParameters.js +++ b/tools/spectral/ipa/rulesets/functions/createMethodShouldNotHaveQueryParameters.js @@ -1,5 +1,10 @@ import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; const RULE_NAME = 'xgen-IPA-106-create-method-should-not-have-query-parameters'; @@ -24,11 +29,26 @@ export default (input, _, { path }) => { return; } - for (const parameter of postMethod.parameters) { - if (parameter.in === 'query' && !ignoredParameters.includes(parameter.name)) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } + const errors = checkViolationsAndReturnErrors(postMethod.parameters, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); } - collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(postMethodParameters, path) { + const errors = []; + try { + for (const parameter of postMethodParameters) { + if (parameter.in === 'query' && !ignoredParameters.includes(parameter.name)) { + errors.push({ + path: path, + message: `Input parameter [${parameter.name}]: ${ERROR_MESSAGE}`, + }); + } + } + return errors; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/deleteMethod204Response.js b/tools/spectral/ipa/rulesets/functions/deleteMethod204Response.js index 2c947ba5f2..0034247d8e 100644 --- a/tools/spectral/ipa/rulesets/functions/deleteMethod204Response.js +++ b/tools/spectral/ipa/rulesets/functions/deleteMethod204Response.js @@ -1,4 +1,9 @@ -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { hasException } from './utils/exceptions.js'; const RULE_NAME = 'xgen-IPA-108-delete-method-return-204-response'; @@ -17,9 +22,29 @@ export default (input, _, { path }) => { return; } - const responses = input.responses; - if (!responses || !responses['204']) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); + const errors = checkViolationsAndReturnErrors(input, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); } - return collectAdoption(path, RULE_NAME); + collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(input, path) { + try { + console.log(input); + const responses = input.responses; + console.log(responses); + // If there is no 204 response, return a violation + if (!responses || !responses['204']) { + return [{ path, message: ERROR_MESSAGE }]; + } + + // If there are other 2xx responses that are not 204, return a violation + if (Object.keys(responses).some((key) => key.startsWith('2') && key !== '204')) { + return [{ path, message: ERROR_MESSAGE }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/deleteMethodResponseShouldNotHaveSchema.js b/tools/spectral/ipa/rulesets/functions/deleteMethodResponseShouldNotHaveSchema.js index cc1dcbbe3a..b5d723ae23 100644 --- a/tools/spectral/ipa/rulesets/functions/deleteMethodResponseShouldNotHaveSchema.js +++ b/tools/spectral/ipa/rulesets/functions/deleteMethodResponseShouldNotHaveSchema.js @@ -23,7 +23,7 @@ export default (input, _, { path }) => { } // 2. Validation - const errors = checkViolations(input, path); + const errors = checkViolationsAndReturnErrors(input, path); if (errors.length > 0) { return collectAndReturnViolation(path, RULE_NAME, errors); } @@ -37,7 +37,7 @@ export default (input, _, { path }) => { * @param {object} jsonPathArray - The jsonPathArray covering location in the OpenAPI schema * @return {Array} - errors array () */ -function checkViolations(input, jsonPathArray) { +function checkViolationsAndReturnErrors(input, jsonPathArray) { const errors = []; try { const successResponse = input; diff --git a/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js index 976e1f0e28..bfa76b4e39 100644 --- a/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js +++ b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustBeGetOrPost.js @@ -1,13 +1,18 @@ import { isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-109-custom-method-must-be-GET-or-POST'; const ERROR_MESSAGE = 'The HTTP method for custom methods must be GET or POST.'; const VALID_METHODS = ['get', 'post']; const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']; -export default (input, opts, { path }) => { +export default (input, _, { path }) => { // Extract the path key (e.g., '/a/{exampleId}:method') from the JSONPath. let pathKey = path[1]; @@ -20,21 +25,32 @@ export default (input, opts, { path }) => { return; } - //Extract the keys which are equivalent of the http methods - let keys = Object.keys(input); - const httpMethods = keys.filter((key) => HTTP_METHODS.includes(key)); - - // Check for invalid methods - if (httpMethods.some((method) => !VALID_METHODS.includes(method))) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } - - // Check for multiple valid methods - const validMethodCount = httpMethods.filter((method) => VALID_METHODS.includes(method)).length; - - if (validMethodCount > 1) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); + const errors = checkViolationsAndReturnErrors(input, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); } - collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(input, path) { + try { + //Extract the keys which are equivalent of the http methods + let keys = Object.keys(input); + const httpMethods = keys.filter((key) => HTTP_METHODS.includes(key)); + + // Check for invalid methods + if (httpMethods.some((method) => !VALID_METHODS.includes(method))) { + return [{ path, message: ERROR_MESSAGE }]; + } + + // Check for multiple valid methods + const validMethodCount = httpMethods.filter((method) => VALID_METHODS.includes(method)).length; + + if (validMethodCount > 1) { + return [{ path, message: ERROR_MESSAGE }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js index d445c4a6d1..65edf7bbb1 100644 --- a/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js +++ b/tools/spectral/ipa/rulesets/functions/eachCustomMethodMustUseCamelCase.js @@ -1,7 +1,12 @@ import { getCustomMethodName, isCustomMethodIdentifier } from './utils/resourceEvaluation.js'; import { hasException } from './utils/exceptions.js'; import { casing } from '@stoplight/spectral-functions'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-109-custom-method-must-use-camel-case'; @@ -18,16 +23,27 @@ export default (input, opts, { path }) => { return; } - let methodName = getCustomMethodName(pathKey); - if (methodName.length === 0 || methodName.trim().length === 0) { - const errorMessage = 'Custom method name cannot be empty or blank.'; - return collectAndReturnViolation(path, RULE_NAME, errorMessage); + const errors = checkViolationsAndReturnErrors(pathKey, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); } - - if (casing(methodName, { type: 'camel', disallowDigits: true })) { - const errorMessage = `${methodName} must use camelCase format.`; - return collectAndReturnViolation(path, RULE_NAME, errorMessage); - } - collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(pathKey, path) { + try { + let methodName = getCustomMethodName(pathKey); + if (methodName.length === 0 || methodName.trim().length === 0) { + const errorMessage = 'Custom method name cannot be empty or blank.'; + return [{ path, message: errorMessage }]; + } + + if (casing(methodName, { type: 'camel', disallowDigits: true })) { + const errorMessage = `${methodName} must use camelCase format.`; + return [{ path, message: errorMessage }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/eachEnumValueMustBeUpperSnakeCase.js b/tools/spectral/ipa/rulesets/functions/eachEnumValueMustBeUpperSnakeCase.js index 7f798e361e..5b8374ebc6 100644 --- a/tools/spectral/ipa/rulesets/functions/eachEnumValueMustBeUpperSnakeCase.js +++ b/tools/spectral/ipa/rulesets/functions/eachEnumValueMustBeUpperSnakeCase.js @@ -1,43 +1,49 @@ import { hasException } from './utils/exceptions.js'; import { resolveObject } from './utils/componentUtils.js'; import { casing } from '@stoplight/spectral-functions'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; +import { getSchemaPathFromEnumPath } from './utils/schemaUtils.js'; const RULE_NAME = 'xgen-IPA-123-enum-values-must-be-upper-snake-case'; const ERROR_MESSAGE = 'enum value must be UPPER_SNAKE_CASE.'; -function getSchemaPathFromEnumPath(path) { - const enumIndex = path.lastIndexOf('enum'); - if (path[enumIndex - 1] === 'items') { - return path.slice(0, enumIndex - 1); - } - return path.slice(0, enumIndex); -} - export default (input, _, { path, documentInventory }) => { const oas = documentInventory.resolved; const schemaPath = getSchemaPathFromEnumPath(path); const schemaObject = resolveObject(oas, schemaPath); + if (hasException(schemaObject, RULE_NAME)) { collectException(schemaObject, RULE_NAME, schemaPath); return; } - const errors = []; - input.forEach((enumValue, index) => { - const isUpperSnakeCase = casing(enumValue, { type: 'macro' }); + const errors = checkViolationsAndReturnErrors(input, schemaPath); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + collectAdoption(schemaPath, RULE_NAME); +}; - if (isUpperSnakeCase) { - errors.push({ - path: schemaPath, - message: `enum[${index}]:${enumValue} ${ERROR_MESSAGE} `, - }); - } - }); +function checkViolationsAndReturnErrors(input, schemaPath) { + const errors = []; + try { + input.forEach((enumValue, index) => { + const isUpperSnakeCase = casing(enumValue, { type: 'macro' }); - if (errors.length === 0) { - collectAdoption(schemaPath, RULE_NAME); - } else { - return collectAndReturnViolation(schemaPath, RULE_NAME, errors); + if (isUpperSnakeCase) { + errors.push({ + path: schemaPath, + message: `enum[${index}]:${enumValue} ${ERROR_MESSAGE} `, + }); + } + }); + return errors; + } catch (e) { + handleInternalError(RULE_NAME, schemaPath, e); } -}; +} diff --git a/tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js b/tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js index b1c48408b9..ed60186e9d 100644 --- a/tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js +++ b/tools/spectral/ipa/rulesets/functions/eachPathAlternatesBetweenResourceNameAndPathParam.js @@ -1,6 +1,11 @@ import { isPathParam } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { AUTH_PREFIX, UNAUTH_PREFIX } from './utils/resourceEvaluation.js'; const RULE_NAME = 'xgen-IPA-102-path-alternate-resource-name-path-param'; @@ -25,10 +30,6 @@ const validatePathStructure = (elements) => { export default (input, _, { path, documentInventory }) => { const oas = documentInventory.resolved; - if (hasException(oas.paths[input], RULE_NAME)) { - collectException(oas.paths[input], RULE_NAME, path); - return; - } const prefix = getPrefix(input); if (!prefix) { @@ -40,11 +41,27 @@ export default (input, _, { path, documentInventory }) => { return; } - let suffix = suffixWithLeadingSlash.slice(1); - let elements = suffix.split('/'); - if (!validatePathStructure(elements)) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); + if (hasException(oas.paths[input], RULE_NAME)) { + collectException(oas.paths[input], RULE_NAME, path); + return; } + const errors = checkViolationsAndReturnErrors(suffixWithLeadingSlash, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(suffixWithLeadingSlash, path) { + try { + let suffix = suffixWithLeadingSlash.slice(1); + let elements = suffix.split('/'); + if (!validatePathStructure(elements)) { + return [{ path, message: ERROR_MESSAGE }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/eachResourceHasGetMethod.js b/tools/spectral/ipa/rulesets/functions/eachResourceHasGetMethod.js index 40b1f36c02..fb8496e192 100644 --- a/tools/spectral/ipa/rulesets/functions/eachResourceHasGetMethod.js +++ b/tools/spectral/ipa/rulesets/functions/eachResourceHasGetMethod.js @@ -6,7 +6,12 @@ import { isSingleResourceIdentifier, } from './utils/resourceEvaluation.js'; import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-104-resource-has-GET'; const ERROR_MESSAGE = 'APIs must provide a get method for resources.'; @@ -23,22 +28,33 @@ export default (input, _, { path, documentInventory }) => { return; } - const resourcePathItems = getResourcePathItems(input, oas.paths); - const resourcePaths = Object.keys(resourcePathItems); - - if (isSingletonResource(resourcePathItems)) { - if (!hasGetMethod(resourcePathItems[resourcePaths[0]])) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } - } else { - if (resourcePaths.length === 1) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } - const singleResourcePath = resourcePaths.find((p) => isSingleResourceIdentifier(p)); - if (!singleResourcePath || !hasGetMethod(resourcePathItems[singleResourcePath])) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } + const errors = checkViolationsAndReturnErrors(oas.paths, input, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); } - collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(oasPaths, input, path) { + try { + const resourcePathItems = getResourcePathItems(input, oasPaths); + const resourcePaths = Object.keys(resourcePathItems); + + if (isSingletonResource(resourcePathItems)) { + if (!hasGetMethod(resourcePathItems[resourcePaths[0]])) { + return [{ path, message: ERROR_MESSAGE }]; + } + } else { + if (resourcePaths.length === 1) { + return [{ path, message: ERROR_MESSAGE }]; + } + const singleResourcePath = resourcePaths.find((p) => isSingleResourceIdentifier(p)); + if (!singleResourcePath || !hasGetMethod(resourcePathItems[singleResourcePath])) { + return [{ path, message: ERROR_MESSAGE }]; + } + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/exceptionExtensionFormat.js b/tools/spectral/ipa/rulesets/functions/exceptionExtensionFormat.js index 21289d08c6..73e89380aa 100644 --- a/tools/spectral/ipa/rulesets/functions/exceptionExtensionFormat.js +++ b/tools/spectral/ipa/rulesets/functions/exceptionExtensionFormat.js @@ -1,4 +1,4 @@ -import { collectAdoption, collectAndReturnViolation } from './utils/collectionUtils.js'; +import { collectAdoption, collectAndReturnViolation, handleInternalError } from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-005-exception-extension-format'; const ERROR_MESSAGE = 'IPA exceptions must have a valid rule name and a reason.'; @@ -6,28 +6,32 @@ const RULE_NAME_PREFIX = 'xgen-IPA-'; // Note: This rule does not allow exceptions export default (input, _, { path }) => { - const exemptedRules = Object.keys(input); - const errors = []; - - exemptedRules.forEach((ruleName) => { - const reason = input[ruleName]; - if (!isValidException(ruleName, reason)) { - errors.push({ - path: path.concat([ruleName]), - message: ERROR_MESSAGE, - }); - } - }); - - if (errors.length === 0) { - collectAdoption(path, RULE_NAME); - } else { + const errors = checkViolationsAndReturnErrors(input, path); + if (errors.length !== 0) { return collectAndReturnViolation(path, RULE_NAME, errors); } - - return errors; + collectAdoption(path, RULE_NAME); }; function isValidException(ruleName, reason) { return ruleName.startsWith(RULE_NAME_PREFIX) && typeof reason === 'string' && reason !== ''; } + +function checkViolationsAndReturnErrors(input, path) { + const errors = []; + try { + const exemptedRules = Object.keys(input); + exemptedRules.forEach((ruleName) => { + const reason = input[ruleName]; + if (!isValidException(ruleName, reason)) { + errors.push({ + path: path.concat([ruleName]), + message: ERROR_MESSAGE, + }); + } + }); + return errors; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/getMethodReturnsResponseSuffixedObject.js b/tools/spectral/ipa/rulesets/functions/getMethodReturnsResponseSuffixedObject.js index a05fd490ef..d4cb7eb90e 100644 --- a/tools/spectral/ipa/rulesets/functions/getMethodReturnsResponseSuffixedObject.js +++ b/tools/spectral/ipa/rulesets/functions/getMethodReturnsResponseSuffixedObject.js @@ -6,7 +6,12 @@ import { } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { getSchemaRef } from './utils/methodUtils.js'; const RULE_NAME = 'xgen-IPA-104-get-method-returns-response-suffixed-object'; @@ -16,33 +21,45 @@ const ERROR_MESSAGE_SCHEMA_REF = 'The response body schema is defined inline and export default (input, _, { path, documentInventory }) => { const resourcePath = path[1]; const responseCode = path[4]; - const contentMediaType = path[path.length - 1]; const oas = documentInventory.unresolved; const resourcePaths = getResourcePathItems(resourcePath, oas.paths); + const contentPerMediaType = resolveObject(oas, path); if ( - responseCode.startsWith('2') && - contentMediaType.endsWith('json') && - (isSingleResourceIdentifier(resourcePath) || - (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths))) + !contentPerMediaType.schema || + !responseCode.startsWith('2') || + !input.endsWith('json') || + (!isSingleResourceIdentifier(resourcePath) && + !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths))) ) { - const contentPerMediaType = resolveObject(oas, path); + return; + } - if (hasException(contentPerMediaType, RULE_NAME)) { - collectException(contentPerMediaType, RULE_NAME, path); - return; - } + if (hasException(contentPerMediaType, RULE_NAME)) { + collectException(contentPerMediaType, RULE_NAME, path); + return; + } - if (contentPerMediaType.schema) { - const schema = contentPerMediaType.schema; - const schemaRef = getSchemaRef(schema); - if (!schemaRef) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_REF); - } - if (!schemaRef.endsWith('Response')) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE_SCHEMA_NAME); - } - collectAdoption(path, RULE_NAME); - } + const errors = checkViolationsAndReturnErrors(contentPerMediaType, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); } + collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(contentPerMediaType, path) { + try { + const schema = contentPerMediaType.schema; + const schemaRef = getSchemaRef(schema); + + if (!schemaRef) { + return [{ path, message: ERROR_MESSAGE_SCHEMA_REF }]; + } + if (!schemaRef.endsWith('Response')) { + return [{ path, message: ERROR_MESSAGE_SCHEMA_NAME }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/getMethodReturnsSingleResource.js b/tools/spectral/ipa/rulesets/functions/getMethodReturnsSingleResource.js index b7ca5adfc6..59ef11e56a 100644 --- a/tools/spectral/ipa/rulesets/functions/getMethodReturnsSingleResource.js +++ b/tools/spectral/ipa/rulesets/functions/getMethodReturnsSingleResource.js @@ -4,8 +4,12 @@ import { isSingleResourceIdentifier, isSingletonResource, } from './utils/resourceEvaluation.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; -import { getAllSuccessfulResponseSchemas } from './utils/methodUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { hasException } from './utils/exceptions.js'; import { schemaIsArray, schemaIsPaginated } from './utils/schemaUtils.js'; import { resolveObject } from './utils/componentUtils.js'; @@ -19,36 +23,48 @@ const ERROR_MESSAGE_SINGLETON_RESOURCE = export default (input, _, { path, documentInventory }) => { const oas = documentInventory.resolved; const resourcePath = path[1]; + const responseCode = path[4]; + const resourcePaths = getResourcePathItems(resourcePath, oas.paths); + const contentPerMediaType = resolveObject(oas, path); const isSingleResource = isSingleResourceIdentifier(resourcePath); - const isSingleton = - isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths)); - - if (isSingleResource || isSingleton) { - const errors = []; - - const responseSchemas = getAllSuccessfulResponseSchemas(input); - responseSchemas.forEach(({ schemaPath, schema }) => { - const fullPath = path.concat(schemaPath); - const responseObject = resolveObject(oas, fullPath); - - if (hasException(responseObject, RULE_NAME)) { - collectException(responseObject, RULE_NAME, fullPath); - } else if (schemaIsPaginated(schema) || schemaIsArray(schema)) { - collectAndReturnViolation( - fullPath, - RULE_NAME, - isSingleton ? ERROR_MESSAGE_SINGLETON_RESOURCE : ERROR_MESSAGE_STANDARD_RESOURCE - ); - errors.push({ - path: fullPath, - message: isSingleton ? ERROR_MESSAGE_SINGLETON_RESOURCE : ERROR_MESSAGE_STANDARD_RESOURCE, - }); - } else { - collectAdoption(fullPath, RULE_NAME); - } - }); + const isSingleton = isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths); + + if ( + !contentPerMediaType.schema || + !responseCode.startsWith('2') || + !input.endsWith('json') || + (!isSingleResource && !isSingleton) + ) { + return; + } + + if (hasException(contentPerMediaType, RULE_NAME)) { + collectException(contentPerMediaType, RULE_NAME, path); + return; + } + + const errors = checkViolationsAndReturnErrors(contentPerMediaType, path, isSingleton); - return errors; + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); } + return collectAdoption(path, RULE_NAME); }; + +function checkViolationsAndReturnErrors(contentPerMediaType, path, isSingleton) { + try { + const schema = contentPerMediaType.schema; + if (schemaIsPaginated(schema) || schemaIsArray(schema)) { + return [ + { + path, + message: isSingleton ? ERROR_MESSAGE_SINGLETON_RESOURCE : ERROR_MESSAGE_STANDARD_RESOURCE, + }, + ]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/getResponseCodeShouldBe200OK.js b/tools/spectral/ipa/rulesets/functions/getResponseCodeShouldBe200OK.js index 0d32abbbca..5a65dc4d02 100644 --- a/tools/spectral/ipa/rulesets/functions/getResponseCodeShouldBe200OK.js +++ b/tools/spectral/ipa/rulesets/functions/getResponseCodeShouldBe200OK.js @@ -1,5 +1,10 @@ import { hasException } from './utils/exceptions.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; import { getResourcePathItems, isResourceCollectionIdentifier, @@ -14,30 +19,41 @@ const ERROR_MESSAGE = export default (input, _, { path, documentInventory }) => { const resourcePath = path[1]; const oas = documentInventory.resolved; + const resourcePaths = getResourcePathItems(resourcePath, oas.paths); if ( - isSingleResourceIdentifier(resourcePath) || - (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths))) + !isSingleResourceIdentifier(resourcePath) && + !(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePaths)) ) { - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } + return; + } - if (input['responses']) { - const responses = input['responses']; + if (hasException(input, RULE_NAME)) { + collectException(input, RULE_NAME, path); + return; + } - // If there is no 200 response, return a violation - if (!responses['200']) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } + const errors = checkViolationsAndReturnErrors(input, path); + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + collectAdoption(path, RULE_NAME); +}; - // If there are other 2xx responses that are not 200, return a violation - if (Object.keys(responses).some((key) => key.startsWith('2') && key !== '200')) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } +function checkViolationsAndReturnErrors(input, path) { + try { + const responses = input.responses; + // If there is no 200 response, return a violation + if (!responses || !responses['200']) { + return [{ path, message: ERROR_MESSAGE }]; } - collectAdoption(path, RULE_NAME); + // If there are other 2xx responses that are not 200, return a violation + if (Object.keys(responses).some((key) => key.startsWith('2') && key !== '200')) { + return [{ path, message: ERROR_MESSAGE }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); } -}; +} diff --git a/tools/spectral/ipa/rulesets/functions/singletonHasNoId.js b/tools/spectral/ipa/rulesets/functions/singletonHasNoId.js index 718e0dbf15..a8b3152456 100644 --- a/tools/spectral/ipa/rulesets/functions/singletonHasNoId.js +++ b/tools/spectral/ipa/rulesets/functions/singletonHasNoId.js @@ -6,34 +6,39 @@ import { } from './utils/resourceEvaluation.js'; import { hasException } from './utils/exceptions.js'; import { getAllSuccessfulResponseSchemas } from './utils/methodUtils.js'; -import { collectAdoption, collectAndReturnViolation, collectException } from './utils/collectionUtils.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; const RULE_NAME = 'xgen-IPA-113-singleton-must-not-have-id'; const ERROR_MESSAGE = 'Singleton resources must not have a user-provided or system-generated ID.'; export default (input, opts, { path, documentInventory }) => { + const oas = documentInventory.resolved; const resourcePath = path[1]; + const resourcePathItems = getResourcePathItems(resourcePath, oas.paths); - if (!isResourceCollectionIdentifier(resourcePath)) { + if (!(isResourceCollectionIdentifier(resourcePath) && isSingletonResource(resourcePathItems))) { return; } - const oas = documentInventory.resolved; - const resourcePathItems = getResourcePathItems(resourcePath, oas.paths); + if (!hasGetMethod(input)) { + return; + } - if (isSingletonResource(resourcePathItems)) { - if (hasException(input, RULE_NAME)) { - collectException(input, RULE_NAME, path); - return; - } - if (hasGetMethod(input)) { - const resourceSchemas = getAllSuccessfulResponseSchemas(input['get']); - if (resourceSchemas.some(({ schema }) => schemaHasIdProperty(schema))) { - return collectAndReturnViolation(path, RULE_NAME, ERROR_MESSAGE); - } - collectAdoption(path, RULE_NAME); - } + 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); }; function schemaHasIdProperty(schema) { @@ -43,3 +48,15 @@ function schemaHasIdProperty(schema) { } return false; } + +function checkViolationsAndReturnErrors(input, path) { + try { + const resourceSchemas = getAllSuccessfulResponseSchemas(input['get']); + if (resourceSchemas.some(({ schema }) => schemaHasIdProperty(schema))) { + return [{ path, message: ERROR_MESSAGE }]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js index 42035daa56..ae10e80e63 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -7,3 +7,11 @@ export function schemaIsArray(schema) { const fields = Object.keys(schema); return fields.includes('type') && schema['type'] === 'array'; } + +export function getSchemaPathFromEnumPath(path) { + const enumIndex = path.lastIndexOf('enum'); + if (path[enumIndex - 1] === 'items') { + return path.slice(0, enumIndex - 1); + } + return path.slice(0, enumIndex); +}