diff --git a/tools/helpers.ts b/tools/helpers.ts index 49b7218f..79040749 100644 --- a/tools/helpers.ts +++ b/tools/helpers.ts @@ -56,6 +56,18 @@ export function sort_by_keys (obj: Record, priorities: string[] = [ }) } +export function sort_array_by_keys (values: any[], priorities: string[] = []): string[] { + const orders = _.fromPairs(priorities.map((k, i) => [k, i + 1])) + return _.clone(values).sort((a, b) => { + const order_a = orders[a] + const order_b = orders[b] + if (order_a != null && order_b != null) return order_a - order_b + if (order_a != null) return 1 + if (order_b != null) return -1 + return a.localeCompare(b) + }) +} + export function ensure_parent_dir (file_path: string): void { fs.mkdirSync(path.dirname(file_path), { recursive: true }) } diff --git a/tools/src/linter/components/SupersededOperationsFile.ts b/tools/src/linter/components/SupersededOperationsFile.ts index 47b34fe9..7c0f5d3b 100644 --- a/tools/src/linter/components/SupersededOperationsFile.ts +++ b/tools/src/linter/components/SupersededOperationsFile.ts @@ -7,8 +7,40 @@ * compatible open source license. */ +import _ from 'lodash' +import { sort_array_by_keys } from '../../../helpers' import FileValidator from './base/FileValidator' +import { type ValidationError } from 'types' +import { type OpenAPIV3 } from 'openapi-types' + +const HTTP_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE'] export default class SupersededOperationsFile extends FileValidator { has_json_schema = true + protected _superseded_ops: OpenAPIV3.Document | undefined + + validate (): ValidationError[] { + const schema_validations = super.validate() + if (schema_validations.length > 0) return schema_validations + return this.validate_order_of_operations() + } + + superseded_ops (): OpenAPIV3.Document { + if (this._superseded_ops) return this._superseded_ops + this._superseded_ops = this.spec() + delete (this._superseded_ops as any).$schema + return this._superseded_ops + } + + validate_order_of_operations (): ValidationError[] { + return _.entries(this.superseded_ops()).map(([path, p]) => { + const current_keys = p.operations + const sorted_keys = sort_array_by_keys(p.operations as string[], HTTP_METHODS) + if(!_.isEqual(current_keys, sorted_keys)) { + return this.error( + `Operations must be sorted. Expected ${_.join(sorted_keys, ', ')}.`, + path) + } + }).filter((e) => e) as ValidationError[] + } } diff --git a/tools/tests/linter/SupersededOperationsFile.test.ts b/tools/tests/linter/SupersededOperationsFile.test.ts index 13102d45..696e7842 100644 --- a/tools/tests/linter/SupersededOperationsFile.test.ts +++ b/tools/tests/linter/SupersededOperationsFile.test.ts @@ -9,12 +9,45 @@ import SupersededOperationsFile from 'linter/components/SupersededOperationsFile' -test('validate()', () => { - const validator = new SupersededOperationsFile('./tools/tests/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.schema.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]" - } - ]) +describe('validate()', () => { + test('invalid schema', () => { + const validator = new SupersededOperationsFile('./tools/tests/linter/fixtures/superseded_operations/invalid_schema.yaml') + expect(validator.validate()).toEqual([ + { + file: 'superseded_operations/invalid_schema.yaml', + message: "File content does not match JSON schema found in './json_schemas/_superseded_operations.schema.yaml':\n " + + JSON.stringify([ + { + "instancePath": "/~1hello~1world/operations/1", + "schemaPath": "#/patternProperties/%5E~1/properties/operations/items/enum", + "keyword": "enum", + "params": { + "allowedValues": [ + "GET", + "POST", + "PUT", + "DELETE", + "HEAD", + "OPTIONS", + "PATCH" + ] + }, + "message": "must be equal to one of the allowed values" + } + ], null, 2), + }, + ]) + }) + + test('incorrect order of operations', () => { + const validator = new SupersededOperationsFile('./tools/tests/linter/fixtures/superseded_operations/incorrect_order_of_operations.yaml') + expect(validator.validate()).toEqual([ + { + file: 'superseded_operations/incorrect_order_of_operations.yaml', + location: '/world/hello', + message: "Operations must be sorted. Expected GET, HEAD, POST, PUT, PATCH, DELETE." + }, + ]) + }) }) + diff --git a/tools/tests/linter/fixtures/superseded_operations/incorrect_order_of_operations.yaml b/tools/tests/linter/fixtures/superseded_operations/incorrect_order_of_operations.yaml new file mode 100644 index 00000000..076e2268 --- /dev/null +++ b/tools/tests/linter/fixtures/superseded_operations/incorrect_order_of_operations.yaml @@ -0,0 +1,11 @@ +$schema: ./json_schemas/_superseded_operations.schema.yaml + +/world/hello: + superseded_by: /world/goodbye + operations: + - PATCH + - GET + - DELETE + - HEAD + - POST + - PUT diff --git a/tools/tests/linter/fixtures/_superseded_operations.yaml b/tools/tests/linter/fixtures/superseded_operations/invalid_schema.yaml similarity index 91% rename from tools/tests/linter/fixtures/_superseded_operations.yaml rename to tools/tests/linter/fixtures/superseded_operations/invalid_schema.yaml index 17341c41..34e2b3fe 100644 --- a/tools/tests/linter/fixtures/_superseded_operations.yaml +++ b/tools/tests/linter/fixtures/superseded_operations/invalid_schema.yaml @@ -4,4 +4,4 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml superseded_by: /goodbye/world operations: - GET - - CLEAN \ No newline at end of file + - CLEAN