From f1d79880ade1b44757a8227cd040f5435eab76c7 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 25 Mar 2025 11:52:32 +0000 Subject: [PATCH 1/5] CLOUDP-271998: IPA-110: Pagination: APIs returning collections should provide pagination (Part 1) --- ...A104GetMethodReturnsSingleResource.test.js | 13 +- ...tMethodResponseIsGetMethodResponse.test.js | 16 + ...PA110CollectionsUsePaginatedSchema.test.js | 336 ++++++++++++++++++ tools/spectral/ipa/ipa-spectral.yaml | 1 + tools/spectral/ipa/rulesets/IPA-110.yaml | 21 ++ tools/spectral/ipa/rulesets/README.md | 16 + .../IPA110CollectionsUsePaginatedSchema.js | 87 +++++ .../rulesets/functions/utils/schemaUtils.js | 12 +- 8 files changed, 493 insertions(+), 9 deletions(-) create mode 100644 tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js create mode 100644 tools/spectral/ipa/rulesets/IPA-110.yaml create mode 100644 tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js diff --git a/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js b/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js index 825e055031..30bcf4274a 100644 --- a/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js +++ b/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js @@ -11,15 +11,22 @@ const componentSchemas = { }, }, PaginatedSchema: { - type: 'array', - }, - ArraySchema: { + type: 'object', properties: { + totalCount: { + type: 'integer', + }, results: { type: 'array', + items: { + $ref: '#/components/schemas/Schema', + }, }, }, }, + ArraySchema: { + type: 'array', + }, }, }; diff --git a/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js b/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js index 6458b1c2aa..16ad48f0e6 100644 --- a/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js +++ b/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js @@ -4,7 +4,11 @@ import { DiagnosticSeverity } from '@stoplight/types'; const componentSchemas = { schemas: { PaginatedResourceSchema: { + type: 'object', properties: { + totalCount: { + type: 'integer', + }, results: { type: 'array', items: { @@ -17,7 +21,11 @@ const componentSchemas = { type: 'object', }, PaginatedArraySchema: { + type: 'object', properties: { + totalCount: { + type: 'integer', + }, results: { type: 'array', items: { @@ -79,7 +87,11 @@ testRule('xgen-IPA-105-list-method-response-is-get-method-response', [ content: { 'application/vnd.atlas.2024-08-05+json': { schema: { + type: 'object', properties: { + totalCount: { + type: 'integer', + }, results: { type: 'array', items: { @@ -302,6 +314,10 @@ testRule('xgen-IPA-105-list-method-response-is-get-method-response', [ 'application/vnd.atlas.2024-08-05+json': { schema: { properties: { + type: 'object', + totalCount: { + type: 'integer', + }, results: { type: 'array', items: { diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js new file mode 100644 index 0000000000..b555b2a295 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js @@ -0,0 +1,336 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const componentSchemas = { + Resource: { + type: 'object', + }, + PaginatedResourceList: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + ResourceList: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + NonPaginatedList: { + type: 'object', + properties: { + count: { + type: 'integer', + }, + items: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, +}; + +testRule('xgen-IPA-110-collections-use-paginated-schema', [ + { + name: 'valid schemas with Paginated prefix', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/PaginatedResourceList', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: componentSchemas + }, + }, + errors: [], + }, + { + name: 'valid schemas without Paginated prefix but with correct structure', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ResourceList', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: componentSchemas + }, + }, + errors: [], + }, + { + name: 'invalid list methods without correct structure', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/NonPaginatedList', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas : componentSchemas, + } + }, + errors: [ + { + code: 'xgen-IPA-110-collections-use-paginated-schema', + message: + 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid schema missing totalCount', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/MissingTotalCount', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: { + ...componentSchemas, + MissingTotalCount: { + type: 'object', + properties: { + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + } + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-use-paginated-schema', + message: + 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'invalid schema missing results', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/MissingResults', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: { + MissingResults: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-use-paginated-schema', + message: + 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'inline schema instead of reference', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: componentSchemas + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-use-paginated-schema', + message: + 'The schema is defined inline and must reference a predefined schema with a name starting with "Paginated".', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'valid schema with exception', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-110-collections-use-paginated-schema': 'Reason', + }, + schema: { + $ref: '#/components/schemas/NonPaginatedList', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: componentSchemas + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/ipa-spectral.yaml b/tools/spectral/ipa/ipa-spectral.yaml index ebcf0ee062..3bc7d2371b 100644 --- a/tools/spectral/ipa/ipa-spectral.yaml +++ b/tools/spectral/ipa/ipa-spectral.yaml @@ -7,6 +7,7 @@ extends: - ./rulesets/IPA-107.yaml - ./rulesets/IPA-108.yaml - ./rulesets/IPA-109.yaml + - ./rulesets/IPA-110.yaml - ./rulesets/IPA-112.yaml - ./rulesets/IPA-113.yaml - ./rulesets/IPA-117.yaml diff --git a/tools/spectral/ipa/rulesets/IPA-110.yaml b/tools/spectral/ipa/rulesets/IPA-110.yaml new file mode 100644 index 0000000000..bc239a2e98 --- /dev/null +++ b/tools/spectral/ipa/rulesets/IPA-110.yaml @@ -0,0 +1,21 @@ +# IPA-110: Pagination +# http://go/ipa/110 + +functions: + - IPA110CollectionsUsePaginatedSchema + +rules: + xgen-IPA-110-collections-use-paginated-schema: + description: | + APIs that return collections of resources must use a paginated response schema. + + ##### Implementation details + Rule checks for the following conditions: + - Only applies to List methods (GET operations that return collections of resources) + - Checks that the 200 response schema references a schema with a name starting with "Paginated" + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-list-methods-must-use-paginated-schema' + severity: warn + given: '$.paths[*].get.responses.200.content' + then: + field: '@key' + function: 'IPA110CollectionsUsePaginatedSchema' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index da9199092e..deafc93dd7 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -447,6 +447,22 @@ Rule checks for the following conditions: +### IPA-110 + +Rules are based on [http://go/ipa/IPA-110](http://go/ipa/IPA-110). + +#### xgen-IPA-110-collections-use-paginated-schema + + ![warn](https://img.shields.io/badge/warning-yellow) +APIs that return collections of resources must use a paginated response schema. + +##### Implementation details +Rule checks for the following conditions: + - Only applies to List methods (GET operations that return collections of resources) + - Checks that the 200 response schema references a schema with a name starting with "Paginated" + + + ### IPA-112 Rules are based on [http://go/ipa/IPA-112](http://go/ipa/IPA-112). diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js new file mode 100644 index 0000000000..50faa36c65 --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js @@ -0,0 +1,87 @@ +import { hasException } from './utils/exceptions.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; +import { + getResourcePathItems, + isResourceCollectionIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { resolveObject } from './utils/componentUtils.js'; +import { getSchemaNameFromRef } from './utils/methodUtils.js'; +import { schemaIsPaginated } from './utils/schemaUtils.js'; + +const RULE_NAME = 'xgen-IPA-110-collections-use-paginated-schema'; +const ERROR_MESSAGE = + 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.'; + +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.unresolved; + const resourcePath = path[1]; + const mediaType = input; + + if ( + !mediaType.endsWith('json') || + !isResourceCollectionIdentifier(resourcePath) || + (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths))) + ) { + return; + } + + // Ignore if the List method does not have a response schema + const listMethodResponse = resolveObject(oas, path); + + if (!listMethodResponse || !listMethodResponse.schema) { + return; + } + + if (hasException(listMethodResponse, RULE_NAME)) { + collectException(listMethodResponse, RULE_NAME, path); + return; + } + + const errors = checkViolationsAndReturnErrors(listMethodResponse, oas, path); + + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(listMethodResponse, oas, path) { + try { + if (!listMethodResponse.schema.$ref) { + return [ + { + path, + message: + 'The schema is defined inline and must reference a predefined schema with a name starting with "Paginated".', + }, + ]; + } + + const schemaRef = listMethodResponse.schema.$ref; + const schemaName = getSchemaNameFromRef(schemaRef); + + if (schemaName.startsWith('Paginated')) { + return []; + } + + const listResponseSchema = resolveObject(oas, ['components', 'schemas', schemaName]); + if (!schemaIsPaginated(listResponseSchema)) { + 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 2c15b69e28..a8400a10a5 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -4,12 +4,12 @@ * @returns true if schema object returns results property (pagination), false otherwise */ export function schemaIsPaginated(schema) { - const fields = Object.keys(schema); - return ( - fields.includes('properties') && - Object.keys(schema['properties']).includes('results') && - schema.properties.results.type === 'array' - ); + const hasTotalCount = + schema.properties && schema.properties.totalCount && schema.properties.totalCount.type === 'integer'; + + const hasResultsArray = schema.properties && schema.properties.results && schema.properties.results.type === 'array'; + + return hasTotalCount && hasResultsArray; } /** From 6266a946468f25b2e58e60468c1805a3418387b4 Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 25 Mar 2025 11:56:38 +0000 Subject: [PATCH 2/5] docs change --- ...PA110CollectionsUsePaginatedSchema.test.js | 64 +++++++++---------- tools/spectral/ipa/rulesets/IPA-110.yaml | 3 +- tools/spectral/ipa/rulesets/README.md | 3 +- 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js index b555b2a295..dc724e7e3e 100644 --- a/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js @@ -2,37 +2,37 @@ import testRule from './__helpers__/testRule'; import { DiagnosticSeverity } from '@stoplight/types'; const componentSchemas = { - Resource: { - type: 'object', - }, - PaginatedResourceList: { - type: 'object', - properties: { - totalCount: { - type: 'integer', - }, - results: { - type: 'array', - items: { - $ref: '#/components/schemas/Resource', - }, + Resource: { + type: 'object', + }, + PaginatedResourceList: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', }, }, }, - ResourceList: { - type: 'object', - properties: { - totalCount: { - type: 'integer', - }, - results: { - type: 'array', - items: { - $ref: '#/components/schemas/Resource', - }, + }, + ResourceList: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', }, }, }, + }, NonPaginatedList: { type: 'object', properties: { @@ -86,7 +86,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, }, components: { - schemas: componentSchemas + schemas: componentSchemas, }, }, errors: [], @@ -115,7 +115,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, }, components: { - schemas: componentSchemas + schemas: componentSchemas, }, }, errors: [], @@ -144,8 +144,8 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, }, components: { - schemas : componentSchemas, - } + schemas: componentSchemas, + }, }, errors: [ { @@ -194,7 +194,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, }, }, - } + }, }, }, errors: [ @@ -288,7 +288,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, }, components: { - schemas: componentSchemas + schemas: componentSchemas, }, }, errors: [ @@ -328,7 +328,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, }, components: { - schemas: componentSchemas + schemas: componentSchemas, }, }, errors: [], diff --git a/tools/spectral/ipa/rulesets/IPA-110.yaml b/tools/spectral/ipa/rulesets/IPA-110.yaml index bc239a2e98..6dcde39490 100644 --- a/tools/spectral/ipa/rulesets/IPA-110.yaml +++ b/tools/spectral/ipa/rulesets/IPA-110.yaml @@ -12,7 +12,8 @@ rules: ##### Implementation details Rule checks for the following conditions: - Only applies to List methods (GET operations that return collections of resources) - - Checks that the 200 response schema references a schema with a name starting with "Paginated" + - Checks that the 200 response schema references a schema with a name starting with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields + - If the referenced schema name has "Paginated" prefix, the rule assumes that the schema is paginated and does not check for "totalCount" and "results" fields message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-list-methods-must-use-paginated-schema' severity: warn given: '$.paths[*].get.responses.200.content' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index deafc93dd7..5d3ef4e249 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -459,7 +459,8 @@ APIs that return collections of resources must use a paginated response schema. ##### Implementation details Rule checks for the following conditions: - Only applies to List methods (GET operations that return collections of resources) - - Checks that the 200 response schema references a schema with a name starting with "Paginated" + - Checks that the 200 response schema references a schema with a name starting with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields + - If the referenced schema name has "Paginated" prefix, the rule assumes that the schema is paginated and does not check for "totalCount" and "results" fields From 52698c6679f14120fa9d4335c7d5c0182da269bc Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 25 Mar 2025 14:34:04 +0000 Subject: [PATCH 3/5] address the comments --- ...PA110CollectionsUsePaginatedSchema.test.js | 72 ++++++++++++++----- tools/spectral/ipa/rulesets/IPA-110.yaml | 4 +- tools/spectral/ipa/rulesets/README.md | 4 +- .../IPA110CollectionsUsePaginatedSchema.js | 31 ++++---- 4 files changed, 77 insertions(+), 34 deletions(-) diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js index dc724e7e3e..83bfebcabd 100644 --- a/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js @@ -33,7 +33,7 @@ const componentSchemas = { }, }, }, - NonPaginatedList: { + PaginatedList: { type: 'object', properties: { count: { @@ -92,7 +92,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ errors: [], }, { - name: 'valid schemas without Paginated prefix but with correct structure', + name: 'invalid schemas without Paginated prefix but with correct structure', document: { paths: { '/resources': { @@ -118,7 +118,15 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ schemas: componentSchemas, }, }, - errors: [], + errors: [ + { + code: 'xgen-IPA-110-collections-use-paginated-schema', + message: + 'List methods response must reference a paginated response schema. The response should reference a schema with "Paginated" prefix', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + severity: DiagnosticSeverity.Warning, + }, + ], }, { name: 'invalid list methods without correct structure', @@ -129,11 +137,15 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ responses: { 200: { content: { - 'application/json': { + 'application/vnd.atlas.2024-08-05+json': { schema: { - $ref: '#/components/schemas/NonPaginatedList', + $ref: '#/components/schemas/PaginatedList', }, }, + 'application/vnd.atlas.2024-01-01+json': { + schema: {}, + }, + 'application/vnd.atlas.2024-03-03+json': {}, }, }, }, @@ -151,8 +163,22 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ { code: 'xgen-IPA-110-collections-use-paginated-schema', message: - 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.', - path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + 'List methods response must reference a paginated response schema. The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/vnd.atlas.2024-08-05+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-110-collections-use-paginated-schema', + message: + 'List methods response must reference a paginated response schema. The schema is defined inline and must reference a predefined paginated schema.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/vnd.atlas.2024-01-01+json'], + severity: DiagnosticSeverity.Warning, + }, + { + code: 'xgen-IPA-110-collections-use-paginated-schema', + message: + 'List methods response must reference a paginated response schema. The List method response does not have a schema defined.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/vnd.atlas.2024-03-03+json'], severity: DiagnosticSeverity.Warning, }, ], @@ -168,7 +194,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ content: { 'application/json': { schema: { - $ref: '#/components/schemas/MissingTotalCount', + $ref: '#/components/schemas/PaginatedMissingTotalCount', }, }, }, @@ -183,7 +209,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ components: { schemas: { ...componentSchemas, - MissingTotalCount: { + PaginatedMissingTotalCount: { type: 'object', properties: { results: { @@ -201,7 +227,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ { code: 'xgen-IPA-110-collections-use-paginated-schema', message: - 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.', + 'List methods response must reference a paginated response schema. The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], severity: DiagnosticSeverity.Warning, }, @@ -218,7 +244,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ content: { 'application/json': { schema: { - $ref: '#/components/schemas/MissingResults', + $ref: '#/components/schemas/PaginatedMissingResults', }, }, }, @@ -232,7 +258,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, components: { schemas: { - MissingResults: { + PaginatedMissingResults: { type: 'object', properties: { totalCount: { @@ -247,7 +273,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ { code: 'xgen-IPA-110-collections-use-paginated-schema', message: - 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.', + 'List methods response must reference a paginated response schema. The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], severity: DiagnosticSeverity.Warning, }, @@ -294,15 +320,14 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ errors: [ { code: 'xgen-IPA-110-collections-use-paginated-schema', - message: - 'The schema is defined inline and must reference a predefined schema with a name starting with "Paginated".', + message: 'The schema is defined inline and must reference a predefined paginated schema.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], severity: DiagnosticSeverity.Warning, }, ], }, { - name: 'valid schema with exception', + name: 'invalid schema with exception', document: { paths: { '/resources': { @@ -310,12 +335,23 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ responses: { 200: { content: { - 'application/json': { + 'application/vnd.atlas.2024-08-05+json': { 'x-xgen-IPA-exception': { 'xgen-IPA-110-collections-use-paginated-schema': 'Reason', }, schema: { - $ref: '#/components/schemas/NonPaginatedList', + $ref: '#/components/schemas/PaginatedList', + }, + }, + 'application/vnd.atlas.2024-01-01+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-110-collections-use-paginated-schema': 'Reason', + }, + schema: {}, + }, + 'application/vnd.atlas.2024-03-03+json': { + 'x-xgen-IPA-exception': { + 'xgen-IPA-110-collections-use-paginated-schema': 'Reason', }, }, }, diff --git a/tools/spectral/ipa/rulesets/IPA-110.yaml b/tools/spectral/ipa/rulesets/IPA-110.yaml index 6dcde39490..89e6878c2a 100644 --- a/tools/spectral/ipa/rulesets/IPA-110.yaml +++ b/tools/spectral/ipa/rulesets/IPA-110.yaml @@ -12,8 +12,8 @@ rules: ##### Implementation details Rule checks for the following conditions: - Only applies to List methods (GET operations that return collections of resources) - - Checks that the 200 response schema references a schema with a name starting with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields - - If the referenced schema name has "Paginated" prefix, the rule assumes that the schema is paginated and does not check for "totalCount" and "results" fields + - Checks if List method has a response schema defined + - Checks that the 200 response schema references a schema with a "Paginated" prefix and contain both "totalCount" (integer) and "results" (array) fields message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-list-methods-must-use-paginated-schema' severity: warn given: '$.paths[*].get.responses.200.content' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 5d3ef4e249..9f714eb676 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -459,8 +459,8 @@ APIs that return collections of resources must use a paginated response schema. ##### Implementation details Rule checks for the following conditions: - Only applies to List methods (GET operations that return collections of resources) - - Checks that the 200 response schema references a schema with a name starting with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields - - If the referenced schema name has "Paginated" prefix, the rule assumes that the schema is paginated and does not check for "totalCount" and "results" fields + - Checks if List method has a response schema defined + - Checks that the 200 response schema references a schema with a "Paginated" prefix and contain both "totalCount" (integer) and "results" (array) fields diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js index 50faa36c65..9095e6fa26 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js @@ -15,8 +15,7 @@ import { getSchemaNameFromRef } from './utils/methodUtils.js'; import { schemaIsPaginated } from './utils/schemaUtils.js'; const RULE_NAME = 'xgen-IPA-110-collections-use-paginated-schema'; -const ERROR_MESSAGE = - 'List methods must use a paginated response schema. The response should reference a schema with either a name starting with "Paginated" or contain both "totalCount" (integer) and "results" (array) fields.'; +const ERROR_MESSAGE = 'List methods response must reference a paginated response schema.'; export default (input, _, { path, documentInventory }) => { const oas = documentInventory.unresolved; @@ -31,13 +30,8 @@ export default (input, _, { path, documentInventory }) => { return; } - // Ignore if the List method does not have a response schema const listMethodResponse = resolveObject(oas, path); - if (!listMethodResponse || !listMethodResponse.schema) { - return; - } - if (hasException(listMethodResponse, RULE_NAME)) { collectException(listMethodResponse, RULE_NAME, path); return; @@ -54,12 +48,20 @@ export default (input, _, { path, documentInventory }) => { function checkViolationsAndReturnErrors(listMethodResponse, oas, path) { try { + if (!listMethodResponse.schema) { + return [ + { + path, + message: `${ERROR_MESSAGE} The List method response does not have a schema defined.`, + }, + ]; + } + if (!listMethodResponse.schema.$ref) { return [ { path, - message: - 'The schema is defined inline and must reference a predefined schema with a name starting with "Paginated".', + message: `${ERROR_MESSAGE} The schema is defined inline and must reference a predefined paginated schema.`, }, ]; } @@ -67,8 +69,13 @@ function checkViolationsAndReturnErrors(listMethodResponse, oas, path) { const schemaRef = listMethodResponse.schema.$ref; const schemaName = getSchemaNameFromRef(schemaRef); - if (schemaName.startsWith('Paginated')) { - return []; + if (!schemaName.startsWith('Paginated')) { + return [ + { + path, + message: `${ERROR_MESSAGE} The response should reference a schema with "Paginated" prefix.`, + }, + ]; } const listResponseSchema = resolveObject(oas, ['components', 'schemas', schemaName]); @@ -76,7 +83,7 @@ function checkViolationsAndReturnErrors(listMethodResponse, oas, path) { return [ { path, - message: ERROR_MESSAGE, + message: `${ERROR_MESSAGE} The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.`, }, ]; } From 79d6af4faefab1ba528e544fffd50de5ada52dfa Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 25 Mar 2025 15:29:22 +0000 Subject: [PATCH 4/5] split it into two guidelines --- ...A104GetMethodReturnsSingleResource.test.js | 13 +- ...tMethodResponseIsGetMethodResponse.test.js | 16 -- ...lectionsResponseDefineResultsArray.test.js | 195 ++++++++++++++++++ ...A110CollectionsUsePaginatedPrefix.test.js} | 136 ++---------- tools/spectral/ipa/rulesets/IPA-110.yaml | 27 ++- tools/spectral/ipa/rulesets/README.md | 16 +- ...10CollectionsResponseDefineResultsArray.js | 65 ++++++ ...=> IPA110CollectionsUsePaginatedPrefix.js} | 12 +- .../rulesets/functions/utils/schemaUtils.js | 5 +- 9 files changed, 312 insertions(+), 173 deletions(-) create mode 100644 tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js rename tools/spectral/ipa/__tests__/{IPA110CollectionsUsePaginatedSchema.test.js => IPA110CollectionsUsePaginatedPrefix.test.js} (63%) create mode 100644 tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js rename tools/spectral/ipa/rulesets/functions/{IPA110CollectionsUsePaginatedSchema.js => IPA110CollectionsUsePaginatedPrefix.js} (82%) diff --git a/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js b/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js index 30bcf4274a..825e055031 100644 --- a/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js +++ b/tools/spectral/ipa/__tests__/IPA104GetMethodReturnsSingleResource.test.js @@ -11,22 +11,15 @@ const componentSchemas = { }, }, PaginatedSchema: { - type: 'object', + type: 'array', + }, + ArraySchema: { properties: { - totalCount: { - type: 'integer', - }, results: { type: 'array', - items: { - $ref: '#/components/schemas/Schema', - }, }, }, }, - ArraySchema: { - type: 'array', - }, }, }; diff --git a/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js b/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js index 16ad48f0e6..6458b1c2aa 100644 --- a/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js +++ b/tools/spectral/ipa/__tests__/IPA105ListMethodResponseIsGetMethodResponse.test.js @@ -4,11 +4,7 @@ import { DiagnosticSeverity } from '@stoplight/types'; const componentSchemas = { schemas: { PaginatedResourceSchema: { - type: 'object', properties: { - totalCount: { - type: 'integer', - }, results: { type: 'array', items: { @@ -21,11 +17,7 @@ const componentSchemas = { type: 'object', }, PaginatedArraySchema: { - type: 'object', properties: { - totalCount: { - type: 'integer', - }, results: { type: 'array', items: { @@ -87,11 +79,7 @@ testRule('xgen-IPA-105-list-method-response-is-get-method-response', [ content: { 'application/vnd.atlas.2024-08-05+json': { schema: { - type: 'object', properties: { - totalCount: { - type: 'integer', - }, results: { type: 'array', items: { @@ -314,10 +302,6 @@ testRule('xgen-IPA-105-list-method-response-is-get-method-response', [ 'application/vnd.atlas.2024-08-05+json': { schema: { properties: { - type: 'object', - totalCount: { - type: 'integer', - }, results: { type: 'array', items: { diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js new file mode 100644 index 0000000000..3a9dcbb2c0 --- /dev/null +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js @@ -0,0 +1,195 @@ +import testRule from './__helpers__/testRule'; +import { DiagnosticSeverity } from '@stoplight/types'; + +const componentSchemas = { + Resource: { + type: 'object', + }, + PaginatedResourceList: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + ResourcePaginatedList: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, +}; + +testRule('xgen-IPA-110-collections-response-define-results-array', [ + { + name: 'valid schemas with Paginated prefix', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/PaginatedResourceList', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: componentSchemas, + }, + }, + errors: [], + }, + { + name: 'valid schemas without Paginated prefix but with correct structure', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ResourcePaginatedList', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: componentSchemas, + }, + }, + errors: [], + }, + { + name: 'invalid schema missing results', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/PaginatedMissingResults', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: { + PaginatedMissingResults: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-response-define-results-array', + message: + 'The response for collections must define an array of results containing the paginated resource. The response should reference a schema that contains "results" (array) field.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, + { + name: 'valid inline schema instead of reference', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + results: { + type: 'array', + items: { + $ref: '#/components/schemas/Resource', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: componentSchemas, + }, + }, + errors: [], + }, +]); diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedPrefix.test.js similarity index 63% rename from tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js rename to tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedPrefix.test.js index 83bfebcabd..81e279fb46 100644 --- a/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedSchema.test.js +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsUsePaginatedPrefix.test.js @@ -33,23 +33,9 @@ const componentSchemas = { }, }, }, - PaginatedList: { - type: 'object', - properties: { - count: { - type: 'integer', - }, - items: { - type: 'array', - items: { - $ref: '#/components/schemas/Resource', - }, - }, - }, - }, }; -testRule('xgen-IPA-110-collections-use-paginated-schema', [ +testRule('xgen-IPA-110-collections-use-paginated-prefix', [ { name: 'valid schemas with Paginated prefix', document: { @@ -120,9 +106,9 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, errors: [ { - code: 'xgen-IPA-110-collections-use-paginated-schema', + code: 'xgen-IPA-110-collections-use-paginated-prefix', message: - 'List methods response must reference a paginated response schema. The response should reference a schema with "Paginated" prefix', + 'List methods response must reference a paginated response schema. The response should reference a schema with "Paginated" prefix.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], severity: DiagnosticSeverity.Warning, }, @@ -139,7 +125,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ content: { 'application/vnd.atlas.2024-08-05+json': { schema: { - $ref: '#/components/schemas/PaginatedList', + $ref: '#/components/schemas/ResourceList', }, }, 'application/vnd.atlas.2024-01-01+json': { @@ -161,21 +147,21 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, errors: [ { - code: 'xgen-IPA-110-collections-use-paginated-schema', + code: 'xgen-IPA-110-collections-use-paginated-prefix', message: - 'List methods response must reference a paginated response schema. The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.', + 'List methods response must reference a paginated response schema. The response should reference a schema with "Paginated" prefix.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/vnd.atlas.2024-08-05+json'], severity: DiagnosticSeverity.Warning, }, { - code: 'xgen-IPA-110-collections-use-paginated-schema', + code: 'xgen-IPA-110-collections-use-paginated-prefix', message: 'List methods response must reference a paginated response schema. The schema is defined inline and must reference a predefined paginated schema.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/vnd.atlas.2024-01-01+json'], severity: DiagnosticSeverity.Warning, }, { - code: 'xgen-IPA-110-collections-use-paginated-schema', + code: 'xgen-IPA-110-collections-use-paginated-prefix', message: 'List methods response must reference a paginated response schema. The List method response does not have a schema defined.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/vnd.atlas.2024-03-03+json'], @@ -183,102 +169,6 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, ], }, - { - name: 'invalid schema missing totalCount', - document: { - paths: { - '/resources': { - get: { - responses: { - 200: { - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/PaginatedMissingTotalCount', - }, - }, - }, - }, - }, - }, - }, - '/resources/{id}': { - get: {}, - }, - }, - components: { - schemas: { - ...componentSchemas, - PaginatedMissingTotalCount: { - type: 'object', - properties: { - results: { - type: 'array', - items: { - $ref: '#/components/schemas/Resource', - }, - }, - }, - }, - }, - }, - }, - errors: [ - { - code: 'xgen-IPA-110-collections-use-paginated-schema', - message: - 'List methods response must reference a paginated response schema. The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.', - path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], - severity: DiagnosticSeverity.Warning, - }, - ], - }, - { - name: 'invalid schema missing results', - document: { - paths: { - '/resources': { - get: { - responses: { - 200: { - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/PaginatedMissingResults', - }, - }, - }, - }, - }, - }, - }, - '/resources/{id}': { - get: {}, - }, - }, - components: { - schemas: { - PaginatedMissingResults: { - type: 'object', - properties: { - totalCount: { - type: 'integer', - }, - }, - }, - }, - }, - }, - errors: [ - { - code: 'xgen-IPA-110-collections-use-paginated-schema', - message: - 'List methods response must reference a paginated response schema. The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.', - path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], - severity: DiagnosticSeverity.Warning, - }, - ], - }, { name: 'inline schema instead of reference', document: { @@ -319,7 +209,7 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ }, errors: [ { - code: 'xgen-IPA-110-collections-use-paginated-schema', + code: 'xgen-IPA-110-collections-use-paginated-prefix', message: 'The schema is defined inline and must reference a predefined paginated schema.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], severity: DiagnosticSeverity.Warning, @@ -337,21 +227,21 @@ testRule('xgen-IPA-110-collections-use-paginated-schema', [ content: { 'application/vnd.atlas.2024-08-05+json': { 'x-xgen-IPA-exception': { - 'xgen-IPA-110-collections-use-paginated-schema': 'Reason', + 'xgen-IPA-110-collections-use-paginated-prefix': 'Reason', }, schema: { - $ref: '#/components/schemas/PaginatedList', + $ref: '#/components/schemas/ResourceList', }, }, 'application/vnd.atlas.2024-01-01+json': { 'x-xgen-IPA-exception': { - 'xgen-IPA-110-collections-use-paginated-schema': 'Reason', + 'xgen-IPA-110-collections-use-paginated-prefix': 'Reason', }, schema: {}, }, 'application/vnd.atlas.2024-03-03+json': { 'x-xgen-IPA-exception': { - 'xgen-IPA-110-collections-use-paginated-schema': 'Reason', + 'xgen-IPA-110-collections-use-paginated-prefix': 'Reason', }, }, }, diff --git a/tools/spectral/ipa/rulesets/IPA-110.yaml b/tools/spectral/ipa/rulesets/IPA-110.yaml index 89e6878c2a..876e64f1b8 100644 --- a/tools/spectral/ipa/rulesets/IPA-110.yaml +++ b/tools/spectral/ipa/rulesets/IPA-110.yaml @@ -2,21 +2,36 @@ # http://go/ipa/110 functions: - - IPA110CollectionsUsePaginatedSchema + - IPA110CollectionsUsePaginatedPrefix + - IPA110CollectionsResponseDefineResultsArray rules: - xgen-IPA-110-collections-use-paginated-schema: + xgen-IPA-110-collections-use-paginated-prefix: description: | - APIs that return collections of resources must use a paginated response schema. + APIs that return collections of resources must use a schema with the "Paginated" prefix. ##### Implementation details Rule checks for the following conditions: - Only applies to List methods (GET operations that return collections of resources) - Checks if List method has a response schema defined - - Checks that the 200 response schema references a schema with a "Paginated" prefix and contain both "totalCount" (integer) and "results" (array) fields - message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-list-methods-must-use-paginated-schema' + - Checks that the 200 response schema references a schema with a "Paginated" prefix + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-collections-use-paginated-prefix' severity: warn given: '$.paths[*].get.responses.200.content' then: field: '@key' - function: 'IPA110CollectionsUsePaginatedSchema' + function: 'IPA110CollectionsUsePaginatedPrefix' + xgen-IPA-110-collections-response-define-results-array: + description: | + The response for collections must define an array of results containing the paginated resource. + + ##### Implementation details + Rule checks for the following conditions: + - Only applies to List methods (GET operations that return collections of resources) + - Verifies the 200 response schema has the required results fields + message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-110-collections-response-define-results-array' + severity: warn + given: '$.paths[*].get.responses.200.content' + then: + field: '@key' + function: 'IPA110CollectionsResponseDefineResultsArray' diff --git a/tools/spectral/ipa/rulesets/README.md b/tools/spectral/ipa/rulesets/README.md index 9f714eb676..3a60497e77 100644 --- a/tools/spectral/ipa/rulesets/README.md +++ b/tools/spectral/ipa/rulesets/README.md @@ -451,16 +451,26 @@ Rule checks for the following conditions: Rules are based on [http://go/ipa/IPA-110](http://go/ipa/IPA-110). -#### xgen-IPA-110-collections-use-paginated-schema +#### xgen-IPA-110-collections-use-paginated-prefix ![warn](https://img.shields.io/badge/warning-yellow) -APIs that return collections of resources must use a paginated response schema. +APIs that return collections of resources must use a schema with the "Paginated" prefix. ##### Implementation details Rule checks for the following conditions: - Only applies to List methods (GET operations that return collections of resources) - Checks if List method has a response schema defined - - Checks that the 200 response schema references a schema with a "Paginated" prefix and contain both "totalCount" (integer) and "results" (array) fields + - Checks that the 200 response schema references a schema with a "Paginated" prefix + +#### xgen-IPA-110-collections-response-define-results-array + + ![warn](https://img.shields.io/badge/warning-yellow) +The response for collections must define an array of results containing the paginated resource. + +##### Implementation details +Rule checks for the following conditions: + - Only applies to List methods (GET operations that return collections of resources) + - Verifies the 200 response schema has the required results fields diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js new file mode 100644 index 0000000000..9e660e82df --- /dev/null +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js @@ -0,0 +1,65 @@ +import { hasException } from './utils/exceptions.js'; +import { + collectAdoption, + collectAndReturnViolation, + collectException, + handleInternalError, +} from './utils/collectionUtils.js'; +import { + getResourcePathItems, + isResourceCollectionIdentifier, + isSingletonResource, +} from './utils/resourceEvaluation.js'; +import { resolveObject } from './utils/componentUtils.js'; +import { schemaIsPaginated } from './utils/schemaUtils.js'; + +const RULE_NAME = 'xgen-IPA-110-collections-use-paginated-schema'; +const ERROR_MESSAGE = 'The response for collections must define an array of results containing the paginated resource.'; + +export default (input, _, { path, documentInventory }) => { + const oas = documentInventory.resolved; + const resourcePath = path[1]; + const mediaType = input; + + if ( + !mediaType.endsWith('json') || + !isResourceCollectionIdentifier(resourcePath) || + (isResourceCollectionIdentifier(resourcePath) && isSingletonResource(getResourcePathItems(resourcePath, oas.paths))) + ) { + return; + } + + const listMethodResponse = resolveObject(oas, path); + if (!listMethodResponse.schema) { + return; + } + + if (hasException(listMethodResponse, RULE_NAME)) { + collectException(listMethodResponse, RULE_NAME, path); + return; + } + + const errors = checkViolationsAndReturnErrors(listMethodResponse.schema, oas, path); + + if (errors.length !== 0) { + return collectAndReturnViolation(path, RULE_NAME, errors); + } + + collectAdoption(path, RULE_NAME); +}; + +function checkViolationsAndReturnErrors(listResponseSchema, oas, path) { + try { + if (!schemaIsPaginated(listResponseSchema)) { + return [ + { + path, + message: `${ERROR_MESSAGE} The response should reference a schema that contains "results" (array) field.`, + }, + ]; + } + return []; + } catch (e) { + handleInternalError(RULE_NAME, path, e); + } +} diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedPrefix.js similarity index 82% rename from tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js rename to tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedPrefix.js index 9095e6fa26..4591bdac2b 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedSchema.js +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsUsePaginatedPrefix.js @@ -12,9 +12,8 @@ import { } from './utils/resourceEvaluation.js'; import { resolveObject } from './utils/componentUtils.js'; import { getSchemaNameFromRef } from './utils/methodUtils.js'; -import { schemaIsPaginated } from './utils/schemaUtils.js'; -const RULE_NAME = 'xgen-IPA-110-collections-use-paginated-schema'; +const RULE_NAME = 'xgen-IPA-110-collections-use-paginated-prefix'; const ERROR_MESSAGE = 'List methods response must reference a paginated response schema.'; export default (input, _, { path, documentInventory }) => { @@ -78,15 +77,6 @@ function checkViolationsAndReturnErrors(listMethodResponse, oas, path) { ]; } - const listResponseSchema = resolveObject(oas, ['components', 'schemas', schemaName]); - if (!schemaIsPaginated(listResponseSchema)) { - return [ - { - path, - message: `${ERROR_MESSAGE} The response should reference a schema that contains both "totalCount" (integer) and "results" (array) fields.`, - }, - ]; - } 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 a8400a10a5..5e00c7fa55 100644 --- a/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js +++ b/tools/spectral/ipa/rulesets/functions/utils/schemaUtils.js @@ -4,12 +4,9 @@ * @returns true if schema object returns results property (pagination), false otherwise */ export function schemaIsPaginated(schema) { - const hasTotalCount = - schema.properties && schema.properties.totalCount && schema.properties.totalCount.type === 'integer'; - const hasResultsArray = schema.properties && schema.properties.results && schema.properties.results.type === 'array'; - return hasTotalCount && hasResultsArray; + return hasResultsArray; } /** From 1e94bf005756df23bec4c007f1275e3938a5d16d Mon Sep 17 00:00:00 2001 From: Yeliz Henden Date: Tue, 25 Mar 2025 15:38:18 +0000 Subject: [PATCH 5/5] error message fix --- ...lectionsResponseDefineResultsArray.test.js | 51 ++++++++++++++++++- ...10CollectionsResponseDefineResultsArray.js | 2 +- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js b/tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js index 3a9dcbb2c0..1077241336 100644 --- a/tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js +++ b/tools/spectral/ipa/__tests__/IPA110CollectionsResponseDefineResultsArray.test.js @@ -145,8 +145,7 @@ testRule('xgen-IPA-110-collections-response-define-results-array', [ errors: [ { code: 'xgen-IPA-110-collections-response-define-results-array', - message: - 'The response for collections must define an array of results containing the paginated resource. The response should reference a schema that contains "results" (array) field.', + message: 'The response for collections must define an array of results containing the paginated resource.', path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], severity: DiagnosticSeverity.Warning, }, @@ -192,4 +191,52 @@ testRule('xgen-IPA-110-collections-response-define-results-array', [ }, errors: [], }, + { + name: 'invalid schema missing results with exceptions', + document: { + paths: { + '/resources': { + get: { + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/PaginatedMissingResults', + }, + 'x-xgen-IPA-exception': { + 'xgen-IPA-110-collections-response-define-results-array': 'Reason', + }, + }, + }, + }, + }, + }, + }, + '/resources/{id}': { + get: {}, + }, + }, + components: { + schemas: { + PaginatedMissingResults: { + type: 'object', + properties: { + totalCount: { + type: 'integer', + }, + }, + }, + }, + }, + }, + errors: [ + { + code: 'xgen-IPA-110-collections-response-define-results-array', + message: 'The response for collections must define an array of results containing the paginated resource.', + path: ['paths', '/resources', 'get', 'responses', '200', 'content', 'application/json'], + severity: DiagnosticSeverity.Warning, + }, + ], + }, ]); diff --git a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js index 9e660e82df..11b115535b 100644 --- a/tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js +++ b/tools/spectral/ipa/rulesets/functions/IPA110CollectionsResponseDefineResultsArray.js @@ -54,7 +54,7 @@ function checkViolationsAndReturnErrors(listResponseSchema, oas, path) { return [ { path, - message: `${ERROR_MESSAGE} The response should reference a schema that contains "results" (array) field.`, + message: `${ERROR_MESSAGE}`, }, ]; }