From 2b1d005f3088d99c99d922d065b1bed3c59030b4 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 28 Apr 2025 17:35:07 +0100 Subject: [PATCH 01/12] Implemented spectral lint rule that validates that at any time, an endpoint can only have one API version marked as 'upcoming' --- tools/spectral/.spectral.yaml | 11 +++++ .../acceptHeaderUpcomingVersionLimit.js | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js diff --git a/tools/spectral/.spectral.yaml b/tools/spectral/.spectral.yaml index 8e5a012fb9..28bf2b216f 100644 --- a/tools/spectral/.spectral.yaml +++ b/tools/spectral/.spectral.yaml @@ -15,7 +15,18 @@ aliases: - "#OperationObject.parameters[?(@ && @.in)]" - "$.components.schemas[*]..properties[?(@ && @.type)]" +functions: + - acceptHeaderUpcomingVersionLimit + rules: + accept-header-upcoming-version-limit: + description: Ensure that each operation has at most one upcoming API Accept header. + message: "An operation must not have more than one upcoming API Accept header (format: application/vnd.atlas.YYYY-MM-DD.upcoming+format)." + severity: error + given: $.paths[*][*] + then: + function: "acceptHeaderUpcomingVersionLimit" + xgen-schema-name-pascal-case: description: OpenAPI Schema names should use PascalCase. PascalCase ensures consistency with OpenAPI generated code. message: "`{{property}}` name must follow PascalCase. Please verify if you have provided valid @Schema(name='') annotation" diff --git a/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js b/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js new file mode 100644 index 0000000000..832a5d43ed --- /dev/null +++ b/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js @@ -0,0 +1,41 @@ +module.exports = function (input) { + // Get operationId from context, if this fails return an error + const operationId = input.operationId; + if (!operationId) { + return; + } + + // List of errors + const errors = []; + + // Validate versions in 200 responses + const responseErr = validateContent(operationId, "response", input?.responses?.[200]?.content); + if (responseErr != null) errors.push(responseErr); + + // Validate versions in requests + const requestErr = validateContent(operationId, "request", input?.requestBody?.content); + if (requestErr != null) errors.push(requestErr); + + return errors.length > 0 ? errors : undefined; +}; + +// Check for upcoming API Accept headers +const upcomingRegex = /^application\/vnd\.atlas\.\d{4}-\d{2}-\d{2}\.upcoming\+.+$/; + +function validateContent(operationId, section, content) { + if (content == null) { + return null; + } + + const contentTypes = Object.keys(content); + const upcomingContentTypes = contentTypes.filter(k => upcomingRegex.test(k)); + // If there's less than or equal to one upcoming header then the operation is valid + if (upcomingContentTypes.length <= 1) { + return null; + } + + // Return an error message + return ({ + message: `OperationId: ${operationId} - Found ${upcomingContentTypes.length} upcoming API Accept headers (section: ${section}): ${upcomingContentTypes.join(', ')}`, + }); +} \ No newline at end of file From 3b684bfa7ab0849a234c9c4c7f4d59635e96ba9d Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 28 Apr 2025 18:01:13 +0100 Subject: [PATCH 02/12] ran prettier --- .../acceptHeaderUpcomingVersionLimit.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js b/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js index 832a5d43ed..ffe59b0699 100644 --- a/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js +++ b/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js @@ -9,11 +9,11 @@ module.exports = function (input) { const errors = []; // Validate versions in 200 responses - const responseErr = validateContent(operationId, "response", input?.responses?.[200]?.content); + const responseErr = validateContent(operationId, 'response', input?.responses?.[200]?.content); if (responseErr != null) errors.push(responseErr); // Validate versions in requests - const requestErr = validateContent(operationId, "request", input?.requestBody?.content); + const requestErr = validateContent(operationId, 'request', input?.requestBody?.content); if (requestErr != null) errors.push(requestErr); return errors.length > 0 ? errors : undefined; @@ -28,14 +28,14 @@ function validateContent(operationId, section, content) { } const contentTypes = Object.keys(content); - const upcomingContentTypes = contentTypes.filter(k => upcomingRegex.test(k)); + const upcomingContentTypes = contentTypes.filter((k) => upcomingRegex.test(k)); // If there's less than or equal to one upcoming header then the operation is valid - if (upcomingContentTypes.length <= 1) { - return null; - } - - // Return an error message - return ({ - message: `OperationId: ${operationId} - Found ${upcomingContentTypes.length} upcoming API Accept headers (section: ${section}): ${upcomingContentTypes.join(', ')}`, - }); -} \ No newline at end of file + if (upcomingContentTypes.length <= 1) { + return null; + } + + // Return an error message + return { + message: `OperationId: ${operationId} - Found ${upcomingContentTypes.length} upcoming API Accept headers (section: ${section}): ${upcomingContentTypes.join(', ')}`, + }; +} From f7e2a1a64a74615e0f43670f3362412911959e70 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 12:14:59 +0100 Subject: [PATCH 03/12] Added unit tests for new check + added .tool-versions for asdf --- tools/spectral/.tool-versions | 1 + .../acceptHeaderUpcomingVersionLimit.test.js | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tools/spectral/.tool-versions create mode 100644 tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js diff --git a/tools/spectral/.tool-versions b/tools/spectral/.tool-versions new file mode 100644 index 0000000000..119cdc35fc --- /dev/null +++ b/tools/spectral/.tool-versions @@ -0,0 +1 @@ +nodejs 24.0.2 diff --git a/tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js b/tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js new file mode 100644 index 0000000000..7107796e05 --- /dev/null +++ b/tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js @@ -0,0 +1,99 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import acceptHeaderUpcomingVersionLimit from '../functions/acceptHeaderUpcomingVersionLimit'; + +describe('accept-header-upcoming-version-limit', () => { + it('valid: no upcoming Accept headers', () => { + const operation = { + operationId: 'getTest', + responses: { + 200: { + content: { + 'application/json': {}, + }, + }, + }, + requestBody: { + content: { + 'application/json': {}, + }, + }, + }; + + const result = acceptHeaderUpcomingVersionLimit(operation); + expect(result).toBeUndefined(); + }); + + it('valid: one upcoming Accept header in response', () => { + const operation = { + operationId: 'getTest', + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-06-01.upcoming+json': {}, + 'application/json': {}, + }, + }, + }, + }; + + const result = acceptHeaderUpcomingVersionLimit(operation); + expect(result).toBeUndefined(); + }); + + it('invalid: two upcoming Accept headers in response', () => { + const operation = { + operationId: 'getTest', + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-06-01.upcoming+json': {}, + 'application/vnd.atlas.2024-07-01.upcoming+json': {}, + 'application/json': {}, + }, + }, + }, + }; + + const result = acceptHeaderUpcomingVersionLimit(operation); + expect(result).toEqual([ + { + message: expect.stringMatching(/Found 2 upcoming API Accept headers/), + }, + ]); + }); + + it('invalid: two upcoming Accept headers in request', () => { + const operation = { + operationId: 'postTest', + requestBody: { + content: { + 'application/vnd.atlas.2024-06-01.upcoming+json': {}, + 'application/vnd.atlas.2024-07-01.upcoming+json': {}, + 'application/json': {}, + }, + }, + }; + + const result = acceptHeaderUpcomingVersionLimit(operation); + expect(result).toEqual([ + { + message: expect.stringMatching(/Found 2 upcoming API Accept headers/), + }, + ]); + }); + + it('invalid: missing operationId', () => { + const operation = { + responses: { + 200: { + content: { + 'application/vnd.atlas.2024-06-01.upcoming+json': {}, + }, + }, + }, + }; + + const result = acceptHeaderUpcomingVersionLimit(operation); + expect(result).toBeUndefined(); + }); +}); \ No newline at end of file From bcb61b83242c9fb24f12b95a60351f07baac4e40 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 12:22:07 +0100 Subject: [PATCH 04/12] ran prettier --- .../spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js b/tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js index 7107796e05..2271a6cc94 100644 --- a/tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js +++ b/tools/spectral/__tests__/acceptHeaderUpcomingVersionLimit.test.js @@ -96,4 +96,4 @@ describe('accept-header-upcoming-version-limit', () => { const result = acceptHeaderUpcomingVersionLimit(operation); expect(result).toBeUndefined(); }); -}); \ No newline at end of file +}); From 0dc6831f14824d77229c7dc78ae9cb6a29716364 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 12:56:17 +0100 Subject: [PATCH 05/12] fix eslint --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 726de46d3c..073bcd0e8d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,7 +36,7 @@ export default [ }, }, { - ignores: ['node-modules'], + ignores: ['**/node_modules/**'], }, { files: ['**/*.test.js'], From 257aad339421a21afb8b291bf5f2799303739810 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 12:59:03 +0100 Subject: [PATCH 06/12] second attempt to fix linting --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 073bcd0e8d..8d06003760 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,7 +36,7 @@ export default [ }, }, { - ignores: ['**/node_modules/**'], + ignorePatterns: ['node_modules/*'], }, { files: ['**/*.test.js'], From b810f8f6a20d8a23597d8b277373b6005b7f3ecb Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 13:01:35 +0100 Subject: [PATCH 07/12] third attempt at fixing eslint --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 8d06003760..12eee0bc1a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,7 +36,7 @@ export default [ }, }, { - ignorePatterns: ['node_modules/*'], + ignores: ['node_modules'], }, { files: ['**/*.test.js'], From 160ec8064e281c6f6047a26e4829d53d44dca85b Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 15:14:23 +0100 Subject: [PATCH 08/12] undo linting changes, renamed test to end with .test.js --- eslint.config.mjs | 2 +- ...VersionLimit.js => acceptHeaderUpcomingVersionLimit.test.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tools/spectral/functions/{acceptHeaderUpcomingVersionLimit.js => acceptHeaderUpcomingVersionLimit.test.js} (100%) diff --git a/eslint.config.mjs b/eslint.config.mjs index 12eee0bc1a..726de46d3c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,7 +36,7 @@ export default [ }, }, { - ignores: ['node_modules'], + ignores: ['node-modules'], }, { files: ['**/*.test.js'], diff --git a/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js b/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.test.js similarity index 100% rename from tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js rename to tools/spectral/functions/acceptHeaderUpcomingVersionLimit.test.js From 96b5eacc6e2bbb4e047cb82f8478e8f37ae1cf4d Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 15:21:27 +0100 Subject: [PATCH 09/12] undo rename --- ...ngVersionLimit.test.js => acceptHeaderUpcomingVersionLimit.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/spectral/functions/{acceptHeaderUpcomingVersionLimit.test.js => acceptHeaderUpcomingVersionLimit.js} (100%) diff --git a/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.test.js b/tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js similarity index 100% rename from tools/spectral/functions/acceptHeaderUpcomingVersionLimit.test.js rename to tools/spectral/functions/acceptHeaderUpcomingVersionLimit.js From 99a260a832588d2a5a913bca1b35e12ef520fa8b Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 15:30:21 +0100 Subject: [PATCH 10/12] files are provided using cli, we don't need this config that overrides the ignores --- eslint.config.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 726de46d3c..41dc5e1617 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -38,10 +38,6 @@ export default [ { ignores: ['node-modules'], }, - { - files: ['**/*.test.js'], - ...jest.configs['flat/recommended'], - }, ...compat.config({ plugins: ['require-extensions'], extends: 'plugin:require-extensions/recommended', From c5421c05f7239271a1b63f3442521e2c1db0998c Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 15:31:48 +0100 Subject: [PATCH 11/12] fix ignores --- eslint.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 41dc5e1617..1be4d84937 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,7 +36,7 @@ export default [ }, }, { - ignores: ['node-modules'], + ignores: ['node_modules/'], }, ...compat.config({ plugins: ['require-extensions'], From 2f628019bc43ca194adc02943b263211d378b9a0 Mon Sep 17 00:00:00 2001 From: Jeroen Vervaeke Date: Mon, 19 May 2025 15:40:59 +0100 Subject: [PATCH 12/12] fixed package.json --- eslint.config.mjs | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 1be4d84937..12eee0bc1a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,7 +36,11 @@ export default [ }, }, { - ignores: ['node_modules/'], + ignores: ['node_modules'], + }, + { + files: ['**/*.test.js'], + ...jest.configs['flat/recommended'], }, ...compat.config({ plugins: ['require-extensions'], diff --git a/package.json b/package.json index 8808e59d11..3f221d00f3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "format": "npx prettier . --write", "format-check": "npx prettier . --check", - "lint-js": "npx eslint **/*.js", + "lint-js": "npx eslint .", "gen-ipa-docs": "node tools/spectral/ipa/scripts/generateRulesetReadme.js", "ipa-validation": "spectral lint ./openapi/.raw/v2.yaml --ruleset=./tools/spectral/ipa/ipa-spectral.yaml", "ipa-filter-violations": "node tools/spectral/ipa/scripts/filter-ipa-violations.js",