diff --git a/.eslintrc.js b/.eslintrc.js index 0010df0..71f477a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,4 +13,12 @@ module.exports = { // set max line length to 120 'max-len': ['error', { code: 120 }], }, + overrides: [ + { + files: ['scripts/**/*.js'], + rules: { + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + }, + }, + ], }; diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 108441b..0c6ce52 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -23,6 +23,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm run build --if-present + - run: npm run validate - run: npm test - run: npm run lint diff --git a/aep/0004.yaml b/aep/0004.yaml index c4e07f9..a324dc2 100644 --- a/aep/0004.yaml +++ b/aep/0004.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json rules: aep-0004-x-aep-resource-structure: description: x-aep-resource must have correct structure with required fields (singular, plural) diff --git a/aep/0122.yaml b/aep/0122.yaml index 5174ed5..07fd75a 100644 --- a/aep/0122.yaml +++ b/aep/0122.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: AepResource: description: A schema that represents an AEP resource diff --git a/aep/0131.yaml b/aep/0131.yaml index 7122de4..7dda91c 100644 --- a/aep/0131.yaml +++ b/aep/0131.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: GetOperation: description: A Get operation is a get on path that ends in a path parameter diff --git a/aep/0132.yaml b/aep/0132.yaml index 5c28431..f84e4b3 100644 --- a/aep/0132.yaml +++ b/aep/0132.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: ListOperation: description: A list operation is a get on path that does not end in a path parameter diff --git a/aep/0133.yaml b/aep/0133.yaml index 589b109..6562ffd 100644 --- a/aep/0133.yaml +++ b/aep/0133.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: CreateOperation: description: A create operation is a post on path without a ":" in the final segment diff --git a/aep/0134.yaml b/aep/0134.yaml index e6cca9b..fddb0bf 100644 --- a/aep/0134.yaml +++ b/aep/0134.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: UpdateOperation: description: An Update method is a patch on path that ends in a path parameter diff --git a/aep/0135.yaml b/aep/0135.yaml index 6a4c1d2..544678e 100644 --- a/aep/0135.yaml +++ b/aep/0135.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: DeleteOperation: description: A Delete operation is a delete on path that ends in a path parameter diff --git a/aep/0136.yaml b/aep/0136.yaml index dc1bc12..51ce38a 100644 --- a/aep/0136.yaml +++ b/aep/0136.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: CustomOperation: description: A custom method is identified by a path containing a colon (:) before the verb diff --git a/aep/0137.yaml b/aep/0137.yaml index bcb9cc8..7dbabab 100644 --- a/aep/0137.yaml +++ b/aep/0137.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: ApplyOperation: description: An Apply method is a put on path that ends in a path parameter diff --git a/aep/0140.yaml b/aep/0140.yaml index ff094ed..179fcf5 100644 --- a/aep/0140.yaml +++ b/aep/0140.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json rules: aep-140-boolean-property-naming: description: 'Boolean properties should omit the prefix "is".' diff --git a/aep/0142.yaml b/aep/0142.yaml index 24a5033..5a21e25 100644 --- a/aep/0142.yaml +++ b/aep/0142.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json functionsDir: ../functions functions: - aep-142-time-field-type diff --git a/aep/0143.yaml b/aep/0143.yaml index e2d2df5..380cbb1 100644 --- a/aep/0143.yaml +++ b/aep/0143.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json functionsDir: ../functions functions: - standardized-codes diff --git a/aep/0144.yaml b/aep/0144.yaml index bfb7ced..d1a4004 100644 --- a/aep/0144.yaml +++ b/aep/0144.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: AddRemovePathItem: description: An Update Array operation is a post request that updates an array with add or remove operation diff --git a/aep/0151.yaml b/aep/0151.yaml index ac67bb8..f1a8855 100644 --- a/aep/0151.yaml +++ b/aep/0151.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: LongRunningOperation: description: An operation that returns 202 Accepted for long-running operations diff --git a/aep/0158.yaml b/aep/0158.yaml index 0dabe6d..ba8aaaa 100644 --- a/aep/0158.yaml +++ b/aep/0158.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json aliases: ListOperation: description: A list operation is a get on path that does not end in a path parameter diff --git a/aep/0193.yaml b/aep/0193.yaml index 6ef946c..61ee4b0 100644 --- a/aep/0193.yaml +++ b/aep/0193.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json rules: aep-193-error-response-schema: description: Error response body should contain all required fields according to AEP-193. diff --git a/package-lock.json b/package-lock.json index c647bbe..a8d8ba1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,11 +18,13 @@ "@stoplight/spectral-ruleset-migrator": "^1.11.1", "@stoplight/spectral-rulesets": "^1.21.3", "ajv": "^8.6.2", + "ajv-formats": "^3.0.1", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.23.4", "jest": "^29.7.0", + "js-yaml": "^4.1.1", "prettier": "^3.2.5" } }, @@ -81,7 +83,6 @@ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.0", @@ -1365,6 +1366,23 @@ "node": "^16.20 || ^18.18 || >= 20.17" } }, + "node_modules/@stoplight/spectral-core/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/@stoplight/spectral-formats": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/@stoplight/spectral-formats/-/spectral-formats-1.8.2.tgz", @@ -1402,6 +1420,23 @@ "node": "^16.20 || ^18.18 || >= 20.17" } }, + "node_modules/@stoplight/spectral-functions/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/@stoplight/spectral-parsers": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@stoplight/spectral-parsers/-/spectral-parsers-1.0.5.tgz", @@ -1522,6 +1557,24 @@ "node": "^16.20 || ^18.18 || >= 20.17" } }, + "node_modules/@stoplight/spectral-rulesets/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/@stoplight/spectral-runtime": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@stoplight/spectral-runtime/-/spectral-runtime-1.1.3.tgz", @@ -1755,7 +1808,6 @@ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1778,7 +1830,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1814,9 +1865,10 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -2226,7 +2278,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -2884,7 +2935,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3024,7 +3074,6 @@ "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -4945,7 +4994,6 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", - "peer": true, "engines": { "node": ">= 10.16.0" } diff --git a/package.json b/package.json index 89a0c28..2716e2c 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "access": "public" }, "scripts": { - "lint": "prettier --check . && eslint --cache --quiet --ext '.js' functions test", - "lint-fix": "prettier --write . && eslint --cache --quiet --ext '.js' --fix functions test", + "lint": "prettier --check . && eslint --cache --quiet --ext '.js' functions scripts test", + "lint-fix": "prettier --write . && eslint --cache --quiet --ext '.js' --fix functions scripts test", "test": "jest --coverage", + "validate": "node scripts/validate-ruleset.js", "release:patch": "./scripts/prepare-release.sh patch", "release:minor": "./scripts/prepare-release.sh minor", "release:major": "./scripts/prepare-release.sh major" @@ -31,11 +32,13 @@ "@stoplight/spectral-ruleset-migrator": "^1.11.1", "@stoplight/spectral-rulesets": "^1.21.3", "ajv": "^8.6.2", + "ajv-formats": "^3.0.1", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.23.4", "jest": "^29.7.0", + "js-yaml": "^4.1.1", "prettier": "^3.2.5" }, "jest": { diff --git a/scripts/validate-ruleset.js b/scripts/validate-ruleset.js new file mode 100644 index 0000000..d9fada1 --- /dev/null +++ b/scripts/validate-ruleset.js @@ -0,0 +1,56 @@ +#!/usr/bin/env node +// Validates spectral.yaml and aep/*.yaml against the remote Spectral ruleset +// JSON schema so authoring mistakes are caught in CI. + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const Ajv2020 = require('ajv/dist/2020'); +const addFormats = require('ajv-formats'); + +const SCHEMA_URL = 'https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json'; + +const ROOT = path.join(__dirname, '..'); +const FILES = [ + 'spectral.yaml', + ...fs + .readdirSync(path.join(ROOT, 'aep')) + .filter((f) => f.endsWith('.yaml')) + .map((f) => path.join('aep', f)), +]; + +async function main() { + const res = await fetch(SCHEMA_URL); + if (!res.ok) { + throw new Error(`Failed to fetch ruleset schema: ${res.status} ${res.statusText}`); + } + const schema = await res.json(); + + const ajv = new Ajv2020({ allErrors: true, strict: false }); + addFormats(ajv); + const validate = ajv.compile(schema); + + let hasErrors = false; + FILES.forEach((file) => { + const fullPath = path.join(ROOT, file); + const doc = yaml.load(fs.readFileSync(fullPath, 'utf8')); + if (!validate(doc)) { + hasErrors = true; + console.error(`\n${file} failed schema validation:`); + validate.errors.forEach((err) => { + console.error(` ${err.instancePath || '/'} ${err.message}`); + }); + } else { + console.log(`${file}: OK`); + } + }); + + if (hasErrors) { + process.exitCode = 1; + } +} + +main().catch((err) => { + console.error(err.message); + process.exitCode = 1; +}); diff --git a/spectral.yaml b/spectral.yaml index 428b8b5..d6eb781 100644 --- a/spectral.yaml +++ b/spectral.yaml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=https://spec.spotlight-rules.com/schema/v1/spectral-ruleset.schema.json extends: - spectral:oas - ./aep/0004.yaml