From 7ea4363ab2d62702d618b66782946e537b24fdf8 Mon Sep 17 00:00:00 2001 From: Lovisa Berggren Date: Fri, 28 Mar 2025 17:03:05 +0000 Subject: [PATCH] CLOUDP-306572: Adds rule for "well defined schema" --- .../IPA117ObjectsMustBeWellDefined.test.js | 371 ++++++++++++++++++ ...IPA117ParameterHasExamplesOrSchema.test.js | 96 +++++ tools/spectral/ipa/rulesets/IPA-117.yaml | 52 +++ tools/spectral/ipa/rulesets/README.md | 31 ++ .../IPA117ObjectsMustBeWellDefined.js | 81 ++++ .../IPA117ParameterHasExamplesOrSchema.js | 34 ++ .../functions/utils/componentUtils.js | 41 ++ .../rulesets/functions/utils/schemaUtils.js | 15 +- 8 files changed, 718 insertions(+), 3 deletions(-) create mode 100644 tools/spectral/ipa/__tests__/IPA117ObjectsMustBeWellDefined.test.js create mode 100644 tools/spectral/ipa/__tests__/IPA117ParameterHasExamplesOrSchema.test.js create mode 100644 tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js create mode 100644 tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js diff --git a/tools/spectral/ipa/__tests__/IPA117ObjectsMustBeWellDefined.test.js b/tools/spectral/ipa/__tests__/IPA117ObjectsMustBeWellDefined.test.js new file mode 100644 index 0000000000..6269961e2f --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA117ObjectsMustBeWellDefined.test.js @@ -0,0 +1,371 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-117-objects-must-be-well-defined', [ + { + name: 'valid objects', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + type: 'object', + schema: {}, + }, + 'application/vnd.atlas.2023-08-05+json': { + type: 'object', + properties: { + name: { + type: 'object', + properties: {}, + }, + hobbies: { + type: 'array', + items: { + type: 'object', + example: 'test', + }, + }, + }, + }, + }, + }, + }, + requestBody: { + content: { + 'application/vnd.atlas.2023-08-05+json': { + type: 'object', + examples: {}, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + SchemaOneOf: { + type: 'object', + oneOf: {}, + }, + SchemaAllOf: { + type: 'object', + allOf: {}, + }, + SchemaAnyOf: { + type: 'object', + anyOf: {}, + }, + ArraySchema: { + type: 'array', + items: { + type: 'object', + schema: {}, + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'valid schema ref', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Schema: { + type: 'object', + properties: {}, + }, + }, + }, + }, + errors: [], + }, + { + name: 'invalid objects', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + type: 'object', + }, + 'application/vnd.atlas.2023-08-05+json': { + type: 'object', + properties: { + name: { + type: 'object', + }, + hobbies: { + type: 'array', + items: { + type: 'object', + }, + }, + }, + }, + }, + }, + }, + requestBody: { + content: { + 'application/vnd.atlas.2023-08-05+json': { + type: 'object', + }, + }, + }, + }, + }, + }, + components: { + schemas: { + SchemaOneOf: { + type: 'object', + }, + SchemaAllOf: { + type: 'object', + }, + SchemaAnyOf: { + type: 'object', + }, + ArraySchema: { + type: 'array', + items: { + type: 'object', + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: [ + 'paths', + '/resource/{id}', + 'get', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2024-08-05+json', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: [ + 'paths', + '/resource/{id}', + 'get', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2023-08-05+json', + 'properties', + 'name', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: [ + 'paths', + '/resource/{id}', + 'get', + 'responses', + '200', + 'content', + 'application/vnd.atlas.2023-08-05+json', + 'properties', + 'hobbies', + 'items', + ], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: ['paths', '/resource/{id}', 'get', 'requestBody', 'content', 'application/vnd.atlas.2023-08-05+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: ['components', 'schemas', 'SchemaOneOf'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: ['components', 'schemas', 'SchemaAllOf'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: ['components', 'schemas', 'SchemaAnyOf'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: ['components', 'schemas', 'ArraySchema', 'items'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid schema ref', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + $ref: '#/components/schemas/Schema', + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Schema: { + type: 'object', + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-117-objects-must-be-well-defined', + message: + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.', + path: ['components', 'schemas', 'Schema'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid OAS with exceptions', + document: { + paths: { + '/resource/{id}': { + get: { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-08-05+json': { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + 'application/vnd.atlas.2023-08-05+json': { + type: 'object', + properties: { + name: { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + hobbies: { + type: 'array', + items: { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + }, + }, + }, + }, + }, + }, + requestBody: { + content: { + 'application/vnd.atlas.2023-08-05+json': { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + SchemaOneOf: { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + SchemaAllOf: { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + SchemaAnyOf: { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + ArraySchema: { + type: 'array', + items: { + type: 'object', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-objects-must-be-well-defined': 'reason', + }, + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/__tests__/IPA117ParameterHasExamplesOrSchema.test.js b/tools/spectral/ipa/__tests__/IPA117ParameterHasExamplesOrSchema.test.js new file mode 100644 index 0000000000..46179af11a --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA117ParameterHasExamplesOrSchema.test.js @@ -0,0 +1,96 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +testRule('xgen-IPA-117-parameter-has-examples-or-schema', [ + { + name: 'valid parameters', + document: { + paths: { + '/resource/{id}': { + get: { + parameters: [ + { + name: 'id', + example: '123', + }, + ], + }, + }, + }, + components: { + parameters: { + id: { + schema: { + type: 'string', + }, + }, + }, + }, + }, + errors: [], + }, + { + name: 'invalid parameters', + document: { + paths: { + '/resource/{id}': { + get: { + parameters: [ + { + name: 'id', + }, + ], + }, + }, + }, + components: { + parameters: { + id: {}, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-117-parameter-has-examples-or-schema', + message: 'API producers must provide a well-defined schema or example(s) for parameters.', + path: ['paths', '/resource/{id}', 'get', 'parameters', '0'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-117-parameter-has-examples-or-schema', + message: 'API producers must provide a well-defined schema or example(s) for parameters.', + path: ['components', 'parameters', 'id'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid parameters with exceptions', + document: { + paths: { + '/resource/{id}': { + get: { + parameters: [ + { + name: 'id', + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-parameter-has-examples-or-schema': 'reason', + }, + }, + ], + }, + }, + }, + components: { + parameters: { + id: { + 'x-xgen-IPA-exception': { + 'xgen-IPA-117-parameter-has-examples-or-schema': 'reason', + }, + }, + }, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/rulesets/IPA-117.yaml b/tools/spectral/ipa/rulesets/IPA-117.yaml index 3953fa785b..5ed56c16cb 100644 --- a/tools/spectral/ipa/rulesets/IPA-117.yaml +++ b/tools/spectral/ipa/rulesets/IPA-117.yaml @@ -9,6 +9,8 @@ functions: - IPA117DescriptionShouldNotUseTables - IPA117DescriptionShouldNotUseLinks - IPA117PlaintextResponseMustHaveExample + - IPA117ObjectsMustBeWellDefined + - IPA117ParameterHasExamplesOrSchema rules: xgen-IPA-117-description: @@ -175,3 +177,53 @@ rules: function: 'IPA117PlaintextResponseMustHaveExample' functionOptions: allowedTypes: ['json', 'yaml'] + xgen-IPA-117-objects-must-be-well-defined: + description: | + Components of type "object" must be well-defined, i.e. have of one of the properties: + - `schema` + - `examples` + - `example` + - `oneOf`, `anyOf` or `allOf` + - `properties` + - `additionalProperties` + + ##### Implementation details + The rule applies to the following components: + - Inline operation responses/request bodies (JSON only) + - Inline operation response/request body properties (JSON only) + - Inline operation response/request body array items (JSON only) + - Schemas defined in `components/schemas` + - Schema properties defined in `components/schemas` + - `items` properties defined in `components/schemas` + The rule is applied to the unresolved OAS, and ignores components with `$ref` properties. Specific paths can be ignored using the `ignoredPaths` option. + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-117-objects-must-be-well-defined' + severity: warn + resolved: false + given: + - '$.paths[*][get,put,post,delete,options,head,patch,trace]..content[*]' + - '$.paths[*][get,put,post,delete,options,head,patch,trace]..content..properties[*]' + - '$.paths[*][get,put,post,delete,options,head,patch,trace]..content..items' + - '$.components.schemas[*]' + - '$.components.schemas..properties[*]' + - '$.components.schemas..items' + then: + function: 'IPA117ObjectsMustBeWellDefined' + functionOptions: + ignoredPaths: + - 'components.schemas.NoBody' + - 'components.schemas.ApiError.properties.parameters.items' + xgen-IPA-117-parameter-has-examples-or-schema: + description: | + API producers must provide a well-defined schema or example(s) for parameters. + + ##### Implementation details + The rule checks for the presence of the `schema`, `examples` or `example` property in: + - Operation parameters + - Parameters defined in `components/parameters` + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-117-parameter-has-examples-or-schema' + severity: warn + given: + - '$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[*]' + - '$.components.parameters[*]' + then: + function: 'IPA117ParameterHasExamplesOrSchema' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index e5036fde09..eecd964c75 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -719,6 +719,37 @@ For APIs that respond with plain text, for example CSV, API producers must provi - The rule ignores JSON and YAML responses (passed as `allowedTypes`) - The rule checks for the presence of the example property as a sibling to the `schema` property, or inside the `schema` object +#### xgen-IPA-117-objects-must-be-well-defined + + ![warn](https://img.shields.io/badge/warning-yellow) +Components of type "object" must be well-defined, i.e. have of one of the properties: + - `schema` + - `examples` + - `example` + - `oneOf`, `anyOf` or `allOf` + - `properties` + - `additionalProperties` + +##### Implementation details +The rule applies to the following components: + - Inline operation responses/request bodies (JSON only) + - Inline operation response/request body properties (JSON only) + - Inline operation response/request body array items (JSON only) + - Schemas defined in `components/schemas` + - Schema properties defined in `components/schemas` + - `items` properties defined in `components/schemas` +The rule is applied to the unresolved OAS, and ignores components with `$ref` properties. Specific paths can be ignored using the `ignoredPaths` option. + +#### xgen-IPA-117-parameter-has-examples-or-schema + + ![warn](https://img.shields.io/badge/warning-yellow) +API producers must provide a well-defined schema or example(s) for parameters. + +##### Implementation details +The rule checks for the presence of the `schema`, `examples` or `example` property in: + - Operation parameters + - Parameters defined in `components/parameters` + ### IPA-123 diff --git a/tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js b/tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js new file mode 100644 index 0000000000..f7672ffe01 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA117ObjectsMustBeWellDefined.js @@ -0,0 +1,81 @@ +import { hasException } from './utils/exceptions.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; +import { pathIsForRequestVersion, pathIsForResponseVersion } from './utils/componentUtils.js'; +import { schemaIsObject } from './utils/schemaUtils.js'; + +const RULE_NAME = 'xgen-IPA-117-objects-must-be-well-defined'; +const ERROR_MESSAGE = + 'Components of type "object" must be well-defined with for example a schema, example(s) or properties.'; + +/** + * The rule checks components of `type: 'object'` for the presence of one of the properties: + * `schema`, `examples`, `example`, `oneOf`, `anyOf`, `allOf`, `properties` or `additionalProperties`. + * + * @param input the component to evaluate + * @param {string[]} ignoredPaths paths to ignore, for example: 'components.schemas.MySchema' + * @param path the path to the component being evaluated + */ +export default (input, { ignoredPaths }, { path }) => { + // Ignore paths that match the passed ignoredPaths + const joinedPath = path.join('.'); + if (ignoredPaths.some((ignoredPath) => ignoredPath === joinedPath)) { + return; + } + + // Ignore types other than object + if (!schemaIsObject(input)) { + return; + } + + // Ignore non-JSON requests + if (pathIsForRequestVersion(path) && !path[5].endsWith('json')) { + return; + } + + // Ignore non-JSON responses + if (pathIsForResponseVersion(path) && !path[6].endsWith('json')) { + return; + } + + // Ignore references + if (input['$ref']) { + 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); +}; + +function checkViolationsAndReturnErrors(object, path) { + try { + const validProperties = [ + 'schema', + 'example', + 'examples', + 'allOf', + 'anyOf', + 'oneOf', + 'properties', + 'additionalProperties', + ]; + if (Object.keys(object).some((key) => validProperties.includes(key))) { + return []; + } + return [{ path, message: ERROR_MESSAGE }]; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js b/tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js new file mode 100644 index 0000000000..3960192b24 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA117ParameterHasExamplesOrSchema.js @@ -0,0 +1,34 @@ +import { hasException } from './utils/exceptions.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + 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); +}; + +function checkViolationsAndReturnErrors(object, path) { + try { + if (object['schema'] || object['example'] || object['examples']) { + return []; + } + return [{ path, message: ERROR_MESSAGE }]; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js b/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js index 494537218a..2710f2e5d6 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/componentUtils.js @@ -46,3 +46,44 @@ export function resolveObject(oas, objectPath) { return current && current[key] ? current[key] : undefined; }, oas); } + +/** + * Checks if a path array points to a specific response version, for example: + * [ + * "paths", + * "/resource/{id}", + * "get", + * "responses", + * "200", + * "content", + * "application/vnd.atlas.2023-08-05+json", + * ], + * + * The array may have more elements beyond the version. + * + * @param {string[]} path + * @returns {boolean} + */ +export function pathIsForResponseVersion(path) { + return path.length > 6 && path[0] === 'paths' && path[3] === 'responses' && path[5] === 'content'; +} + +/** + * Checks if a path array points to a specific request body version, for example: + * [ + * "paths", + * "/resource/{id}", + * "get", + * "requestBody", + * "content", + * "application/vnd.atlas.2023-08-05+json", + * ], + * + * The array may have more elements beyond the version. + * + * @param {string[]} path + * @returns {boolean} + */ +export function pathIsForRequestVersion(path) { + return path.length > 5 && path[0] === 'paths' && path[3] === 'requestBody' && path[4] === 'content'; +} diff --git a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js index 45dcb1ba19..5dfd8d0991 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -4,9 +4,7 @@ * @returns true if schema object returns results property (pagination), false otherwise */ export function schemaIsPaginated(schema) { - const hasResultsArray = schema.properties?.results?.type === 'array'; - - return hasResultsArray; + return schema.properties?.results?.type === 'array'; } /** @@ -20,6 +18,17 @@ export function schemaIsArray(schema) { return fields.includes('type') && schema['type'] === 'array'; } +/** + * Checks if schema is an object type of schema + * + * @param {Object} schema + * @returns + */ +export function schemaIsObject(schema) { + const fields = Object.keys(schema); + return fields.includes('type') && schema['type'] === 'object'; +} + export function getSchemaPathFromEnumPath(path) { const enumIndex = path.lastIndexOf('enum'); if (path[enumIndex - 1] === 'items') {