From 6370067ac78446f25f12e59b3216fa2bcbfea150 Mon Sep 17 00:00:00 2001 From: Dan Hudlow Date: Wed, 2 Apr 2025 01:00:53 -0500 Subject: [PATCH] build: experimental script for adding a new rule Signed-off-by: Dan Hudlow --- package-lock.json | 451 +++++++++++++++++++++++- package.json | 4 +- packages/ruleset/src/functions/index.js | 4 +- packages/ruleset/src/ibm-oas.js | 3 +- packages/ruleset/src/rules/index.js | 8 +- scripts/add-rule.js | 223 ++++++++++++ 6 files changed, 684 insertions(+), 9 deletions(-) create mode 100755 scripts/add-rule.js diff --git a/package-lock.json b/package-lock.json index 1c980d509..c85269a5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "packages/validator" ], "devDependencies": { + "@inquirer/prompts": "^7.4.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^10.0.4", @@ -685,6 +686,352 @@ "resolved": "packages/utilities", "link": true }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.4.tgz", + "integrity": "sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.8.tgz", + "integrity": "sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.9.tgz", + "integrity": "sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.9.tgz", + "integrity": "sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.11.tgz", + "integrity": "sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", + "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.8.tgz", + "integrity": "sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.11.tgz", + "integrity": "sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.11.tgz", + "integrity": "sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.0.tgz", + "integrity": "sha512-EZiJidQOT4O5PYtqnu1JbF0clv36oW2CviR66c7ma4LsupmmQlUwmdReGKRp456OWPWMz3PdrPiYg3aCk3op2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.4", + "@inquirer/confirm": "^5.1.8", + "@inquirer/editor": "^4.2.9", + "@inquirer/expand": "^4.0.11", + "@inquirer/input": "^4.1.8", + "@inquirer/number": "^3.0.11", + "@inquirer/password": "^4.0.11", + "@inquirer/rawlist": "^4.0.11", + "@inquirer/search": "^3.0.11", + "@inquirer/select": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.11.tgz", + "integrity": "sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.11.tgz", + "integrity": "sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.0.tgz", + "integrity": "sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.9", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.5", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", + "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3742,6 +4089,13 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -3798,6 +4152,16 @@ "@colors/colors": "1.5.0" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -5073,6 +5437,21 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5863,6 +6242,19 @@ "resolved": "packages/validator", "link": true }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8236,6 +8628,16 @@ "mustache": "bin/mustache" } }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -11781,6 +12183,16 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -13166,6 +13578,13 @@ "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==", "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/semantic-release": { "version": "21.1.2", "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-21.1.2.tgz", @@ -14346,6 +14765,19 @@ "xtend": "~4.0.1" } }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -15038,9 +15470,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/ruleset": { "name": "@ibm-cloud/openapi-ruleset", - "version": "1.29.2", + "version": "1.29.4", "license": "Apache-2.0", "dependencies": { "@ibm-cloud/openapi-ruleset-utilities": "1.7.1", @@ -15101,10 +15546,10 @@ }, "packages/validator": { "name": "ibm-openapi-validator", - "version": "1.33.2", + "version": "1.33.4", "license": "Apache-2.0", "dependencies": { - "@ibm-cloud/openapi-ruleset": "1.29.2", + "@ibm-cloud/openapi-ruleset": "1.29.4", "@ibm-cloud/openapi-ruleset-utilities": "1.7.1", "@stoplight/spectral-cli": "^6.14.2", "@stoplight/spectral-core": "^1.19.4", diff --git a/package.json b/package.json index 36ba9fdf4..1696db1cb 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,11 @@ "unlink": "npm run unlink --workspace packages/validator", "all": "npm run test && npm run lint", "format-readme-toc": "npx -y markdown-toc --maxdepth 5 -i README.md", - "generate-utilities-docs": "scripts/generate-utilities-docs.js" + "generate-utilities-docs": "scripts/generate-utilities-docs.js", + "add-rule": "scripts/add-rule.js && npm run fix" }, "devDependencies": { + "@inquirer/prompts": "^7.4.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/npm": "^10.0.4", diff --git a/packages/ruleset/src/functions/index.js b/packages/ruleset/src/functions/index.js index 76634bae1..fd2ebe631 100644 --- a/packages/ruleset/src/functions/index.js +++ b/packages/ruleset/src/functions/index.js @@ -4,6 +4,7 @@ */ module.exports = { + // -- START INDEX -- acceptAndReturnModels: require('./accept-and-return-models'), allowedKeywords: require('./allowed-keywords'), anchoredPatterns: require('./anchored-patterns'), @@ -79,7 +80,8 @@ module.exports = { uniqueParameterRequestPropertyNames: require('./unique-parameter-request-property-names'), unusedTags: require('./unused-tags'), useDateBasedFormat: require('./use-date-based-format'), - validatePathSegments: require('./valid-path-segments'), validSchemaExample: require('./valid-schema-example'), + validatePathSegments: require('./valid-path-segments'), wellDefinedDictionaries: require('./well-defined-dictionaries'), + // -- END INDEX -- }; diff --git a/packages/ruleset/src/ibm-oas.js b/packages/ruleset/src/ibm-oas.js index bfa2b7487..3484fe5bd 100644 --- a/packages/ruleset/src/ibm-oas.js +++ b/packages/ruleset/src/ibm-oas.js @@ -104,7 +104,7 @@ module.exports = { // Turn off 'array-items' rule in favor of our 'ibm-array-attributes' rule 'array-items': 'off', - // IBM Custom Rules + // -- START INDEX -- 'ibm-accept-and-return-models': ibmRules.acceptAndReturnModels, 'ibm-anchored-patterns': ibmRules.anchoredPatterns, 'ibm-api-symmetry': ibmRules.apiSymmetry, @@ -202,5 +202,6 @@ module.exports = { 'ibm-valid-path-segments': ibmRules.validPathSegments, 'ibm-valid-schema-example': ibmRules.validSchemaExample, 'ibm-well-defined-dictionaries': ibmRules.wellDefinedDictionaries, + // -- END INDEX -- }, }; diff --git a/packages/ruleset/src/rules/index.js b/packages/ruleset/src/rules/index.js index c6ce4ea15..279ca9b7f 100644 --- a/packages/ruleset/src/rules/index.js +++ b/packages/ruleset/src/rules/index.js @@ -4,6 +4,7 @@ */ module.exports = { + // -- START INDEX -- acceptAndReturnModels: require('./accept-and-return-models'), acceptHeader: require('./accept-header'), anchoredPatterns: require('./anchored-patterns'), @@ -23,10 +24,10 @@ module.exports = { deleteBody: require('./delete-body'), discriminatorPropertyExists: require('./discriminator-property-exists'), duplicatePathParameter: require('./duplicate-path-parameter'), - etagHeaderExists: require('./etag-header-exists'), enumCasingConvention: require('./enum-casing-convention'), errorContentTypeIsJson: require('./error-content-type-is-json'), errorResponseSchemas: require('./error-response-schemas'), + etagHeaderExists: require('./etag-header-exists'), examplesNameContainsSpace: require('./examples-name-contains-space'), ibmSdkOperations: require('./ibm-sdk-operations'), ifModifiedSinceHeader: require('./if-modified-since-header'), @@ -82,17 +83,18 @@ module.exports = { schemaNamingConvention: require('./schema-naming-convention'), schemaTypeExists: require('./schema-type-exists'), schemaTypeFormat: require('./schema-type-format'), - securitySchemes: require('./securityschemes'), securitySchemeAttributes: require('./securityscheme-attributes'), + securitySchemes: require('./securityschemes'), serverVariableDefaultValue: require('./server-variable-default-value'), stringAttributes: require('./string-attributes'), summarySentenceStyle: require('./summary-sentence-style'), typedEnum: require('./typed-enum'), unevaluatedProperties: require('./unevaluated-properties'), - unusedTags: require('./unused-tags'), uniqueParameterRequestPropertyNames: require('./unique-parameter-request-property-names'), + unusedTags: require('./unused-tags'), useDateBasedFormat: require('./use-date-based-format'), validPathSegments: require('./valid-path-segments'), validSchemaExample: require('./valid-schema-example'), wellDefinedDictionaries: require('./well-defined-dictionaries'), + // -- END INDEX -- }; diff --git a/scripts/add-rule.js b/scripts/add-rule.js new file mode 100755 index 000000000..cd4420784 --- /dev/null +++ b/scripts/add-rule.js @@ -0,0 +1,223 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const { confirm, input } = require('@inquirer/prompts'); + +async function prompt() { + console.log(emitNamingRules()); + + const name = await input({ + message: 'Enter a name for the new rule', + validate: name => { + if (!name.startsWith('ibm-')) { + return 'Names must start with "ibm-"'; + } else if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) { + return 'Names must be in "lower-kebab-case"'; + } + + return true; + }, + }); + const camelCaseName = name + .slice(4) + .replace(/-[a-z]/g, m => m.slice(1).toUpperCase()); + const hasCustomFunction = await confirm({ + message: 'Do you need a custom function?', + }); + + if (hasCustomFunction) { + const customFunctionPath = `${__dirname}/../packages/ruleset/src/functions/${name.slice(4)}.js`; + fs.writeFileSync(customFunctionPath, emitCustomFunction(camelCaseName)); + + addToIndex( + `${__dirname}/../packages/ruleset/src/functions/index.js`, + `${camelCaseName}: require('./${name.slice(4)}')` + ); + } + + const rulePath = `${__dirname}/../packages/ruleset/src/rules/${name.slice(4)}.js`; + fs.writeFileSync(rulePath, emitRule(camelCaseName, hasCustomFunction)); + + addToIndex( + `${__dirname}/../packages/ruleset/src/rules/index.js`, + `${camelCaseName}: require('./${name.slice(4)}')` + ); + + addToIndex( + `${__dirname}/../packages/ruleset/src/ibm-oas.js`, + `'${name}': ibmRules.${camelCaseName}` + ); +} + +function emitRule(name, hasCustomFunction) { + return `/** + * Copyright 2017 - 2023 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { oas3 } = require('@stoplight/spectral-formats'); +const { + // See docs/openapi-ruleset-utilities.md + operations, + // parameters, + // patchOperations, + // paths, + // requestBodySchemas, + // responseSchemas, + // schemas, + // securitySchemes, + // unresolvedRequestBodySchemas, + // unresolvedResponseSchemas, + // unresolvedSchemas, +} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections'); +${hasCustomFunction ? `const { ${name} } = require('../functions');` : emitCoreFunctionsImport()} + +module.exports = { + description: 'X should be Y', + message: '{{error}}', + severity: 'error', + formats: [oas3], + resolved: true, + given: operations /* see other options above */, + then: { + function: ${hasCustomFunction ? name : 'truthy /* see other options above */'}, + }, +}; +`; +} + +function emitCustomFunction(name) { + return `/** + * Copyright 2025 IBM Corporation. + * SPDX-License-Identifier: Apache2.0 + */ + +const { + // See docs/openapi-ruleset-utilities.md + // collectFromComposedSchemas, + // getExamplesForSchema, + // getPropertyNamesForSchema, + // getSchemaType, + // isArraySchema, + // isBinarySchema, + // isBooleanSchema, + // isByteSchema, + // isDateSchema, + // isDateTimeSchema, + // isDoubleSchema, + // isEnumerationSchema, + // isFloatSchema, + // isInt32Schema, + // isInt64Schema, + // isIntegerSchema, + // isNumberSchema, + isObject, + // isObjectSchema, + // isPrimitiveSchema, + // isStringSchema, + // schemaHasConstraint, + // schemaHasProperty, + // schemaIsOfType, + // schemaLooselyHasConstraint, + // schemaRequiresProperty, + // SchemaType, + // validateComposedSchemas, + // validateNestedSchemas, + // validateSubschemas, +} = require('@ibm-cloud/openapi-ruleset-utilities'); + +const { LoggerFactory } = require('../utils'); + +let ruleId; +let logger; + +module.exports = function (schema, options, context) { + if (!logger) { + ruleId = context.rule.name; + logger = LoggerFactory.getInstance().getLogger(ruleId); + } + + // sometimes you'll instead want validateComposedSchemas(), validateNestedSchemas(), + // or validateSubschemas() called here with ${name}() as a validate callback + return ${name}(schema, context.path); +}; + +function ${name}(schema, path) { + const errors = []; + + // use throughout as needed + if (!isObject(schema)) { + logger.debug(\`\${ruleId}: some potential runtime problem identified\`); + } + + if (schema.x !== 'y') { + errors.push({ + message: 'X must be Y', + path, + }); + } + + return errors; +} +`; +} + +function emitCoreFunctionsImport() { + return `const { + // See https://docs.stoplight.io/docs/spectral/cb95cf0d26b83-core-functions + // + // alphabetical, + // enumeration, + // falsy, + // length, + // pattern, + // casing, + // schema, + // truthy, + // defined, + // "undefined" as notDefined, + // unreferencedReusableObject, + // or, + // xor, + // typedEnum, +} = require('@stoplight/spectral-functions'); +`; +} + +function emitNamingRules() { + return `Rule names must: +* Start with "ibm-" +* Be in "lower-kebab-case" +* Not be named for invalid behavior + +Examples: +\x1b[32m✔\x1b[0m ibm-consistent-schema-type + \x1b[31m✘\x1b[0m consistent-schema-type +\x1b[32m✔\x1b[0m ibm-anchored-patterns + \x1b[31m✘\x1b[0m ibmAnchoredPatterns +\x1b[32m✔\x1b[0m ibm-integer-attributes + \x1b[31m✘\x1b[0m ibm-missing-integer-attributes +\x1b[32m✔\x1b[0m ibm-integer-attributes + \x1b[31m✘\x1b[0m ibm-missing-integer-attributes +\x1b[32m✔\x1b[0m ibm-no-property-name-collisions + \x1b[31m✘\x1b[0m ibm-property-name-collision +`; +} + +function addToIndex(indexPath, line) { + const index = fs.readFileSync(indexPath, 'utf8'); + + const { prefix, linePrefix, list, suffix } = index.match( + /^(?(.|\n)*\/\/ -- START INDEX --)(?(?\s*)(.|\n)*[^\s,])(?[\s*,]*\/\/ -- END INDEX --(.|\n)*)$/ + ).groups; + + if (list.indexOf(line) === -1) { + const newList = [...list.split(','), `${linePrefix}${line}`] + .sort() + .join(','); + + fs.writeFileSync(indexPath, `${prefix}${newList}${suffix}`); + } +} + +prompt();