diff --git a/package-lock.json b/package-lock.json index 9387b5d56..190f24271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@types/lodash": "^4.14.202", "@types/node": "^20.10.3", "@types/qs": "^6.9.15", + "@types/tmp": "^0.2.6", "@typescript-eslint/eslint-plugin": "^6.21.0", "ajv": "^8.13.0", "ajv-errors": "^3.0.0", @@ -41,6 +42,7 @@ "lodash": "^4.17.21", "qs": "^6.12.1", "smile-js": "^0.7.0", + "tmp": "^0.2.3", "ts-jest": "^29.1.2", "ts-node": "^10.9.1", "typescript": "<5.4.0", @@ -2296,6 +2298,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -7684,6 +7691,14 @@ "node": ">=0.12" } }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -10030,6 +10045,11 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" }, + "@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==" + }, "@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -13778,6 +13798,11 @@ "next-tick": "^1.1.0" } }, + "tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", diff --git a/package.json b/package.json index d433bb108..b574d42df 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,15 @@ "@types/lodash": "^4.14.202", "@types/node": "^20.10.3", "@types/qs": "^6.9.15", + "@types/tmp": "^0.2.6", "@typescript-eslint/eslint-plugin": "^6.21.0", + "ajv": "^8.13.0", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", - "ajv": "^8.13.0", "axios": "^1.7.1", "cbor": "^9.0.2", "commander": "^12.0.0", + "eslint": "^8.57.0", "eslint-config-standard-with-typescript": "^43.0.1", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.29.1", @@ -44,13 +46,13 @@ "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-yml": "^1.14.0", - "eslint": "^8.57.0", "globals": "^15.0.0", "json-diff-ts": "^4.0.1", "json-schema-to-typescript": "^14.0.4", "lodash": "^4.17.21", "qs": "^6.12.1", "smile-js": "^0.7.0", + "tmp": "^0.2.3", "ts-jest": "^29.1.2", "ts-node": "^10.9.1", "typescript": "<5.4.0", diff --git a/tools/src/helpers.ts b/tools/src/helpers.ts index 579f11388..3d5ef2e40 100644 --- a/tools/src/helpers.ts +++ b/tools/src/helpers.ts @@ -65,6 +65,23 @@ export function sort_array_by_keys (values: any[], priorities: string[] = []): s }) } +export function delete_matching_keys(obj: any, condition: (obj: any) => boolean): string[] { + let removed: string[] = [] + for (const key in obj) { + var item = obj[key] + if (_.isObject(item)) { + if (condition(item)) { + removed.push(key) + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete obj[key] + } else { + removed = _.concat(removed, delete_matching_keys(item, condition)) + } + } + } + return removed +} + export function ensure_parent_dir (file_path: string): void { fs.mkdirSync(path.dirname(file_path), { recursive: true }) } diff --git a/tools/src/merger/OpenApiMerger.ts b/tools/src/merger/OpenApiMerger.ts index b914c3be7..800c636c2 100644 --- a/tools/src/merger/OpenApiMerger.ts +++ b/tools/src/merger/OpenApiMerger.ts @@ -10,7 +10,7 @@ import { type OpenAPIV3 } from 'openapi-types' import fs from 'fs' import _, { isEmpty } from 'lodash' -import { read_yaml, write_yaml } from '../helpers' +import { delete_matching_keys, read_yaml, write_yaml } from '../helpers' import SupersededOpsGenerator from './SupersededOpsGenerator' import GlobalParamsGenerator from './GlobalParamsGenerator' import { Logger } from '../Logger' @@ -82,10 +82,10 @@ export default class OpenApiMerger { // Remove any refs that are x-version-added/removed incompatible with the target server version. #remove_refs_per_semver() : void { - this.#remove_per_semver(this._spec.paths) + this.#remove_keys_not_matching_semver(this._spec.paths) // parameters - const removed_params = this.#remove_per_semver(this._spec.components.parameters) + const removed_params = this.#remove_keys_not_matching_semver(this._spec.components.parameters) const removed_parameter_refs = _.map(removed_params, (ref) => `#/components/parameters/${ref}`) Object.entries(this._spec.paths as Document).forEach(([_path, path_item]) => { Object.entries(path_item as Document).forEach(([_method, method_item]) => { @@ -94,7 +94,7 @@ export default class OpenApiMerger { }) // responses - const removed_responses = this.#remove_per_semver(this._spec.components.responses) + const removed_responses = this.#remove_keys_not_matching_semver(this._spec.components.responses) const removed_response_refs = _.map(removed_responses, (ref) => `#/components/responses/${ref}`) Object.entries(this._spec.paths as Document).forEach(([_path, path_item]) => { Object.entries(path_item as Document).forEach(([_method, method_item]) => { @@ -121,23 +121,9 @@ export default class OpenApiMerger { } // Remove any elements that are x-version-added/removed incompatible with the target server version. - #remove_per_semver(obj: any): string[] { - let removed: string[] = [] - if (this.target_version === undefined) return removed - - for (const key in obj) { - if (_.isObject(obj[key])) { - if (this.#exclude_per_semver(obj[key])) { - removed.push(key) - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete obj[key] - } else { - removed = _.concat(removed, this.#remove_per_semver(obj[key])) - } - } - } - - return removed + #remove_keys_not_matching_semver(obj: any): string[] { + if (this.target_version === undefined) return [] + return delete_matching_keys(obj, this.#exclude_per_semver.bind(this)) } // Redirect schema references in namespace files to local references in single-file spec. diff --git a/tools/tests/helpers.test.ts b/tools/tests/helpers.test.ts index 60480af32..5268b55eb 100644 --- a/tools/tests/helpers.test.ts +++ b/tools/tests/helpers.test.ts @@ -7,7 +7,8 @@ * compatible open source license. */ -import { sort_array_by_keys, to_json, to_ndjson } from '../src/helpers' +import _ from 'lodash' +import { delete_matching_keys, sort_array_by_keys, to_json, to_ndjson } from '../src/helpers' describe('helpers', () => { describe('sort_array_by_keys', () => { @@ -36,4 +37,90 @@ describe('helpers', () => { expect(to_ndjson([{ x: 1 }])).toEqual("{\"x\":1}\n") expect(to_ndjson([{ x: 1 }, { y: 'z' }])).toEqual("{\"x\":1}\n{\"y\":\"z\"}\n") }) + + describe('delete_matching_keys', () => { + test('empty collection', () => { + var obj = {} + expect(delete_matching_keys(obj, (_obj) => false)).toEqual([]) + expect(obj).toEqual({}) + }) + + describe('an object', () => { + var obj: object + + beforeEach(() => { + obj = { + foo: { + bar1: { + x: 1 + } + }, + zar: { + bar2: { + y: 2 + } + } + } + }) + + test('removes all keys', () => { + expect(delete_matching_keys(obj, (_item) => true)).toEqual(['foo', 'zar']) + expect(obj).toStrictEqual({}) + }) + + test('removes no keys', () => { + const obj2 = _.cloneDeep(obj) + expect(delete_matching_keys(obj, (_item) => false)).toEqual([]) + expect(obj).toStrictEqual(obj2) + }) + + test('removes a value from a key', () => { + expect(delete_matching_keys(obj, (_item: any) => _item.x == 1)).toEqual(['bar1']) + expect(obj).toStrictEqual({ foo: {}, zar: { bar2: { y: 2 } } }) + }) + + test('removes multiple values from a key', () => { + expect(delete_matching_keys(obj, (_item: any) => _item.x == 1 || _item.y == 2)).toEqual(['bar1', 'bar2']) + expect(obj).toStrictEqual({ foo: {}, zar: {} }) + }) + }) + + describe('an object with arrays', () => { + var obj: object + + beforeEach(() => { + obj = { + foo: [{ + bar1: { + x: 1 + }, + bar2: { + y: 2 + } + }], + } + }) + + test('removes all keys', () => { + expect(delete_matching_keys(obj, (_item) => true)).toEqual(['foo']) + expect(obj).toStrictEqual({}) + }) + + test('removes no keys', () => { + const obj2 = _.cloneDeep(obj) + expect(delete_matching_keys(obj, (_item) => false)).toEqual([]) + expect(obj).toStrictEqual(obj2) + }) + + test('removes a value from a key', () => { + expect(delete_matching_keys(obj, (_item: any) => _item.x == 1)).toEqual(['bar1']) + expect(obj).toStrictEqual({ foo: [{ bar2: { y: 2 } }] }) + }) + + test('removes multiple values from a key', () => { + expect(delete_matching_keys(obj, (_item: any) => _item.x == 1 || _item.y == 2)).toEqual(['bar1', 'bar2']) + expect(obj).toStrictEqual({ foo: [{}] }) + }) + }) + }) }) diff --git a/tools/tests/merger/OpenApiMerger.test.ts b/tools/tests/merger/OpenApiMerger.test.ts index 2b5904ed6..60f5bcee5 100644 --- a/tools/tests/merger/OpenApiMerger.test.ts +++ b/tools/tests/merger/OpenApiMerger.test.ts @@ -9,6 +9,7 @@ import OpenApiMerger from 'merger/OpenApiMerger' import fs from 'fs' +import tmp, { file } from 'tmp' describe('OpenApiMerger', () => { var merger: OpenApiMerger @@ -29,27 +30,46 @@ describe('OpenApiMerger', () => { }) describe('write_to()', () => { - afterAll(() => { - fs.unlinkSync('./tools/tests/merger/opensearch-openapi.yaml') + var temp: tmp.DirResult + var filename: string + + beforeEach(() => { + temp = tmp.dirSync() + filename = `${temp.name}/opensearch-openapi.yaml` + }) + + afterEach(() => { + fs.unlinkSync(filename) + temp.removeCallback() }) test('writes a spec', () => { - merger.write_to('./tools/tests/merger/opensearch-openapi.yaml') + merger.write_to(filename) expect(fs.readFileSync('./tools/tests/merger/fixtures/expected_2.0.yaml', 'utf8')) - .toEqual(fs.readFileSync('./tools/tests/merger/opensearch-openapi.yaml', 'utf8')) + .toEqual(fs.readFileSync(filename, 'utf8')) }) }) }) describe('1.3', () => { + var temp: tmp.DirResult + var filename: string + beforeEach(() => { merger = new OpenApiMerger('./tools/tests/merger/fixtures/spec/', '1.3') + temp = tmp.dirSync() + filename = `${temp.name}/opensearch-openapi.yaml` + }) + + afterEach(() => { + fs.unlinkSync(filename) + temp.removeCallback() }) test('writes a spec', () => { - merger.write_to('./tools/tests/merger/opensearch-openapi.yaml') + merger.write_to(filename) expect(fs.readFileSync('./tools/tests/merger/fixtures/expected_1.3.yaml', 'utf8')) - .toEqual(fs.readFileSync('./tools/tests/merger/opensearch-openapi.yaml', 'utf8')) + .toEqual(fs.readFileSync(filename, 'utf8')) }) }) }) diff --git a/tools/tests/merger/opensearch-openapi.yaml b/tools/tests/merger/opensearch-openapi.yaml deleted file mode 100644 index d75111c45..000000000 --- a/tools/tests/merger/opensearch-openapi.yaml +++ /dev/null @@ -1,118 +0,0 @@ -openapi: 3.1.0 -info: - title: OpenSearch API - description: OpenSearch API - version: 1.0.0 -paths: - /{index}: - post: - parameters: - - $ref: '#/components/parameters/indices.create::path.index' - - $ref: '#/components/parameters/indices.create::query.pretty' - - $ref: '#/components/parameters/_global::query.human' - requestBody: - $ref: '#/components/requestBodies/indices.create' - responses: - '200': - $ref: '#/components/responses/indices.create@200' - /adopt/{animal}/dockets/{docket}: - get: - operationId: adopt.0 - parameters: - - $ref: '#/components/parameters/adopt::path.animal' - - $ref: '#/components/parameters/adopt::path.docket' - - $ref: '#/components/parameters/_global::query.human' - responses: - '200': - $ref: '#/components/responses/adopt@200' - post: - operationId: adopt.1 - parameters: - - $ref: '#/components/parameters/adopt::path.animal' - - $ref: '#/components/parameters/adopt::path.docket' - - $ref: '#/components/parameters/_global::query.human' - requestBody: - $ref: '#/components/requestBodies/adopt' - responses: - '200': - $ref: '#/components/responses/adopt@200' - /replaced/adopting/{animal}/something/{docket}: - get: - operationId: adopt.0_superseded - parameters: - - $ref: '#/components/parameters/adopt::path.animal' - - $ref: '#/components/parameters/adopt::path.docket' - - $ref: '#/components/parameters/_global::query.human' - responses: - '200': - $ref: '#/components/responses/adopt@200' - deprecated: true - x-ignorable: true -components: - parameters: - _global::query.human: - name: human - in: query - description: Whether to return human readable values for statistics. - schema: - type: boolean - default: true - x-global: true - adopt::path.animal: - name: animal - in: path - schema: - $ref: '#/components/schemas/animals:Animal' - adopt::path.docket: - name: docket - in: path - schema: - type: number - indices.create::path.index: - name: index - in: path - schema: - type: string - indices.create::query.pretty: - name: pretty - in: query - schema: - type: boolean - requestBodies: - adopt: {} - indices.create: {} - responses: - adopt@200: - description: '' - application/json: - schema: - type: object - indices.create@200: - description: '' - application/json: - schema: - type: object - indices.create@201: - description: Added in 2.0. - application/json: - schema: - type: object - schemas: - actions:Bark: - type: string - actions:Meow: - type: string - animals:Animal: - oneOf: - - $ref: '#/components/schemas/animals:Dog' - - $ref: '#/components/schemas/animals:Cat' - animals:Cat: - type: object - properties: - meow: - $ref: '#/components/schemas/actions:Meow' - animals:Dog: - type: object - properties: - bark: - $ref: '#/components/schemas/actions:Bark'