From d3783f1200fdc5799eba861842ee611f2c7e30e7 Mon Sep 17 00:00:00 2001 From: Theo Nam Truong Date: Tue, 30 Apr 2024 18:58:33 -0600 Subject: [PATCH] Validate _superseded_operations.yaml against its JSON schema (#276) * Validate _superseded_operations.yaml against its JSON schema Signed-off-by: Theo Truong * # lint Signed-off-by: Theo Truong * # lint Signed-off-by: Theo Truong --------- Signed-off-by: Theo Truong --- json_schemas/_superseded_operations.yaml | 20 ++++++++++++++++ spec/_superseded_operations.yaml | 2 ++ tools/linter/SpecValidator.ts | 8 +++++-- .../components/SupersededOperationsFile.ts | 23 +++++++++++++++++++ tools/merger/SupersededOpsGenerator.ts | 1 + tools/package-lock.json | 15 ++++++------ tools/package.json | 5 ++-- .../linter/SupersededOperationsFile.test.ts | 11 +++++++++ .../fixtures/_superseded_operations.yaml | 7 ++++++ .../empty/_superseded_operations.yaml | 1 + 10 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 json_schemas/_superseded_operations.yaml create mode 100644 tools/linter/components/SupersededOperationsFile.ts create mode 100644 tools/test/linter/SupersededOperationsFile.test.ts create mode 100644 tools/test/linter/fixtures/_superseded_operations.yaml create mode 100644 tools/test/linter/fixtures/empty/_superseded_operations.yaml diff --git a/json_schemas/_superseded_operations.yaml b/json_schemas/_superseded_operations.yaml new file mode 100644 index 000000000..ed298d7f3 --- /dev/null +++ b/json_schemas/_superseded_operations.yaml @@ -0,0 +1,20 @@ +$schema: http://json-schema.org/draft-07/schema# +type: object +patternProperties: + ^\$schema$: + type: string + ^/: + type: object + properties: + superseded_by: + type: string + pattern: ^/ + operations: + type: array + items: + type: string + enum: [GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH] + required: [superseded_by, operations] + additionalProperties: false +required: [$schema] +additionalProperties: false \ No newline at end of file diff --git a/spec/_superseded_operations.yaml b/spec/_superseded_operations.yaml index 5191a5b6f..3996da336 100644 --- a/spec/_superseded_operations.yaml +++ b/spec/_superseded_operations.yaml @@ -1,3 +1,5 @@ +$schema: ../json_schemas/_superseded_operations.yaml + /_opendistro/_alerting/destinations: superseded_by: /_plugins/_alerting/destinations operations: diff --git a/tools/linter/SpecValidator.ts b/tools/linter/SpecValidator.ts index d7cd3ada5..b227c4fec 100644 --- a/tools/linter/SpecValidator.ts +++ b/tools/linter/SpecValidator.ts @@ -4,9 +4,11 @@ import RootFile from './components/RootFile' import { type ValidationError } from '../types' import PathRefsValidator from './PathRefsValidator' import SchemaRefsValidator from './SchemaRefsValidator' +import SupersededOperationsFile from './components/SupersededOperationsFile' export default class SpecValidator { root_file: RootFile + superseded_ops_files: SupersededOperationsFile namespaces_folder: NamespacesFolder schemas_folder: SchemasFolder path_refs_validator: PathRefsValidator @@ -14,6 +16,7 @@ export default class SpecValidator { constructor (root_folder: string) { this.root_file = new RootFile(`${root_folder}/opensearch-openapi.yaml`) + this.superseded_ops_files = new SupersededOperationsFile(`${root_folder}/_superseded_operations.yaml`) this.namespaces_folder = new NamespacesFolder(`${root_folder}/namespaces`) this.schemas_folder = new SchemasFolder(`${root_folder}/schemas`) this.path_refs_validator = new PathRefsValidator(this.root_file, this.namespaces_folder) @@ -26,11 +29,12 @@ export default class SpecValidator { ...this.namespaces_folder.validate(), ...this.schemas_folder.validate() ] - if (component_errors.length) return component_errors + if (component_errors.length > 0) return component_errors return [ ...this.path_refs_validator.validate(), - ...this.schema_refs_validator.validate() + ...this.schema_refs_validator.validate(), + ...this.superseded_ops_files.validate() ] } } diff --git a/tools/linter/components/SupersededOperationsFile.ts b/tools/linter/components/SupersededOperationsFile.ts new file mode 100644 index 000000000..208279c14 --- /dev/null +++ b/tools/linter/components/SupersededOperationsFile.ts @@ -0,0 +1,23 @@ +import FileValidator from './base/FileValidator' +import ajv from 'ajv' +import fs from 'fs' +import YAML from 'yaml' +import { type ValidationError } from '../../types' + +export default class SupersededOperationsFile extends FileValidator { + JSON_SCHEMA_PATH = '../json_schemas/_superseded_operations.yaml' + + validate (): ValidationError[] { + return [ + this.validate_json_schema() + ].filter(e => e) as ValidationError[] + } + + validate_json_schema (): ValidationError | undefined { + const schema = YAML.parse(fs.readFileSync(this.JSON_SCHEMA_PATH, 'utf8')) + const validator = (new ajv()).compile(schema) + if (!validator(this.spec())) { + return this.error(`File content does not match JSON schema found in '${this.JSON_SCHEMA_PATH}':\n ${JSON.stringify(validator.errors, null, 2)}`) + } + } +} diff --git a/tools/merger/SupersededOpsGenerator.ts b/tools/merger/SupersededOpsGenerator.ts index b278786a8..1d37ff99f 100644 --- a/tools/merger/SupersededOpsGenerator.ts +++ b/tools/merger/SupersededOpsGenerator.ts @@ -9,6 +9,7 @@ export default class SupersededOpsGenerator { constructor (root_path: string) { const file_path = root_path + '/_superseded_operations.yaml' this.superseded_ops = YAML.parse(fs.readFileSync(file_path, 'utf8')) + delete this.superseded_ops.$schema } generate (spec: Record): void { diff --git a/tools/package-lock.json b/tools/package-lock.json index e1faac991..baf75ba26 100644 --- a/tools/package-lock.json +++ b/tools/package-lock.json @@ -12,8 +12,10 @@ "@apidevtools/swagger-parser": "^10.1.0", "@types/lodash": "^4.14.202", "@types/node": "^20.10.3", + "ajv": "^8.13.0", "lodash": "^4.17.21", "ts-node": "^10.9.1", + "typescript": "^5.4.5", "yaml": "^2.3.4" }, "devDependencies": { @@ -28,8 +30,7 @@ "eslint-plugin-promise": "^6.1.1", "globals": "^15.0.0", "jest": "^29.7.0", - "ts-jest": "^29.1.2", - "typescript": "^5.4.5" + "ts-jest": "^29.1.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1803,14 +1804,14 @@ } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", diff --git a/tools/package.json b/tools/package.json index b1e67dc6c..a29beee9e 100644 --- a/tools/package.json +++ b/tools/package.json @@ -11,10 +11,12 @@ "test": "jest" }, "dependencies": { + "ajv": "^8.13.0", "@apidevtools/swagger-parser": "^10.1.0", "@types/lodash": "^4.14.202", "@types/node": "^20.10.3", "lodash": "^4.17.21", + "typescript": "^5.4.5", "ts-node": "^10.9.1", "yaml": "^2.3.4" }, @@ -30,7 +32,6 @@ "eslint-plugin-promise": "^6.1.1", "globals": "^15.0.0", "jest": "^29.7.0", - "ts-jest": "^29.1.2", - "typescript": "^5.4.5" + "ts-jest": "^29.1.2" } } diff --git a/tools/test/linter/SupersededOperationsFile.test.ts b/tools/test/linter/SupersededOperationsFile.test.ts new file mode 100644 index 000000000..be93ea7a0 --- /dev/null +++ b/tools/test/linter/SupersededOperationsFile.test.ts @@ -0,0 +1,11 @@ +import SupersededOperationsFile from '../../linter/components/SupersededOperationsFile' + +test('validate()', () => { + const validator = new SupersededOperationsFile('./test/linter/fixtures/_superseded_operations.yaml') + expect(validator.validate()).toEqual([ + { + file: 'fixtures/_superseded_operations.yaml', + message: "File content does not match JSON schema found in '../json_schemas/_superseded_operations.yaml':\n [\n {\n \"instancePath\": \"/~1hello~1world/operations/1\",\n \"schemaPath\": \"#/patternProperties/%5E~1/properties/operations/items/enum\",\n \"keyword\": \"enum\",\n \"params\": {\n \"allowedValues\": [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"DELETE\",\n \"HEAD\",\n \"OPTIONS\",\n \"PATCH\"\n ]\n },\n \"message\": \"must be equal to one of the allowed values\"\n }\n]" + } + ]) +}) diff --git a/tools/test/linter/fixtures/_superseded_operations.yaml b/tools/test/linter/fixtures/_superseded_operations.yaml new file mode 100644 index 000000000..d264c0681 --- /dev/null +++ b/tools/test/linter/fixtures/_superseded_operations.yaml @@ -0,0 +1,7 @@ +$schema: ../../../../json_schemas/_superseded_operations.yaml + +/hello/world: + superseded_by: /goodbye/world + operations: + - GET + - CLEAN \ No newline at end of file diff --git a/tools/test/linter/fixtures/empty/_superseded_operations.yaml b/tools/test/linter/fixtures/empty/_superseded_operations.yaml new file mode 100644 index 000000000..ac2175ea5 --- /dev/null +++ b/tools/test/linter/fixtures/empty/_superseded_operations.yaml @@ -0,0 +1 @@ +$schema: ../../../../../json_schemas/_superseded_operations.yaml \ No newline at end of file