From 262e1e08aa19313ff70cf99936563ca724cc5835 Mon Sep 17 00:00:00 2001 From: dblock Date: Tue, 16 Jul 2024 17:17:52 -0400 Subject: [PATCH] Delete schema elements that don't match target OpenSearch version. Signed-off-by: dblock --- .github/workflows/test-spec.yml | 2 +- CHANGELOG.md | 1 + DEVELOPER_GUIDE.md | 7 + tools/src/linter/SchemasValidator.ts | 3 +- tools/src/merger/OpenApiMerger.ts | 131 ++++++++++++++---- tools/src/merger/merge.ts | 7 +- tools/src/tester/MergedOpenApiSpec.ts | 8 +- tools/src/tester/test.ts | 3 +- tools/tests/merger/OpenApiMerger.test.ts | 59 ++++++-- .../{expected.yaml => expected_1.3.yaml} | 5 + tools/tests/merger/fixtures/expected_2.0.yaml | 128 +++++++++++++++++ .../fixtures/spec/namespaces/indices.yaml | 15 ++ tools/tests/merger/opensearch-openapi.yaml | 118 ++++++++++++++++ tools/tests/tester/MergedOpenApiSpec.test.ts | 89 +++++++++--- .../specs/complete/namespaces/index.yaml | 21 +++ 15 files changed, 534 insertions(+), 63 deletions(-) rename tools/tests/merger/fixtures/{expected.yaml => expected_1.3.yaml} (96%) create mode 100644 tools/tests/merger/fixtures/expected_2.0.yaml create mode 100644 tools/tests/merger/opensearch-openapi.yaml diff --git a/.github/workflows/test-spec.yml b/.github/workflows/test-spec.yml index 8e290cdf2..586c9f847 100644 --- a/.github/workflows/test-spec.yml +++ b/.github/workflows/test-spec.yml @@ -54,4 +54,4 @@ jobs: run: docker-compose up -d - name: Run Tests - run: npm run test:spec -- --opensearch-insecure \ No newline at end of file + run: npm run test:spec -- --opensearch-insecure --opensearch-version=${{ matrix.entry.version }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d520d3f8c..811cb7b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added `is_hidden` to `/{index}/_alias/{name}` and `/{index}/_aliases/{name}` ([#429](https://github.com/opensearch-project/opensearch-api-specification/pull/429)) - Added `ignore_unmapped` to `GeoDistanceQuery` ([#427](https://github.com/opensearch-project/opensearch-api-specification/pull/427)) - Added missing variants of `indices.put_alias` ([#434](https://github.com/opensearch-project/opensearch-api-specification/pull/434)) +- Added `--opensearch-version` to `merger` that excludes schema elements per semver ([#428](https://github.com/opensearch-project/opensearch-api-specification/pull/428)) ### Changed diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index e941171c5..b965e50a1 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -173,6 +173,7 @@ The merger tool merges the multi-file OpenSearch spec into a single file for pro - `--source `: The path to the root folder of the multi-file spec, defaults to `/spec`. - `--output `: The path to write the final merged spec to, defaults to `/build/opensearch-openapi.yaml`. +- `--opensearch-version`: An optional target version of OpenSearch, checking values of `x-version-added` and `x-version-removed`. #### Example @@ -181,6 +182,12 @@ We can take advantage of the default values and simply merge the specification v npm run merge ``` +To generate a spec that does not contain any APIs or fields removed in version 2.0 (e.g. document `_type` fields). + +```bash +npm run merge -- --opensearch-version=2.0 +``` + ### [Spec Linter](tools/src/linter) ```bash diff --git a/tools/src/linter/SchemasValidator.ts b/tools/src/linter/SchemasValidator.ts index 5192595c7..6954d68ab 100644 --- a/tools/src/linter/SchemasValidator.ts +++ b/tools/src/linter/SchemasValidator.ts @@ -32,7 +32,8 @@ export default class SchemasValidator { } validate (): ValidationError[] { - this.spec = new OpenApiMerger(this.root_folder, new Logger(LogLevel.error)).merge().components as Record + const merger = new OpenApiMerger(this.root_folder, undefined, new Logger(LogLevel.error)) + this.spec = merger.merge().spec().components as Record const named_schemas_errors = this.validate_named_schemas() if (named_schemas_errors.length > 0) return named_schemas_errors return [ diff --git a/tools/src/merger/OpenApiMerger.ts b/tools/src/merger/OpenApiMerger.ts index 51907863d..a0f16a5b8 100644 --- a/tools/src/merger/OpenApiMerger.ts +++ b/tools/src/merger/OpenApiMerger.ts @@ -9,25 +9,30 @@ import { type OpenAPIV3 } from 'openapi-types' import fs from 'fs' -import _ from 'lodash' +import _, { isEmpty } from 'lodash' import { read_yaml, write_yaml } from '../helpers' import SupersededOpsGenerator from './SupersededOpsGenerator' import GlobalParamsGenerator from './GlobalParamsGenerator' import { Logger } from '../Logger' +import * as semver from 'semver' // Create a single-file OpenAPI spec from multiple files for OpenAPI validation and programmatic consumption export default class OpenApiMerger { root_folder: string - spec: Record logger: Logger + target_version?: string + + protected _spec: Record + protected _merged: boolean = false paths: Record> = {} // namespace -> path -> path_item_object schemas: Record> = {} // category -> schema -> schema_object - constructor (root_folder: string, logger: Logger = new Logger()) { + constructor (root_folder: string, target_version?: string, logger: Logger = new Logger()) { this.logger = logger this.root_folder = fs.realpathSync(root_folder) - this.spec = { + this.target_version = target_version === undefined ? undefined : semver.coerce(target_version)?.toString() + this._spec = { openapi: '3.1.0', info: read_yaml(`${this.root_folder}/_info.yaml`, true), paths: {}, @@ -40,17 +45,27 @@ export default class OpenApiMerger { } } - merge (output_path: string = ''): OpenAPIV3.Document { + write_to(output_path: string): OpenApiMerger { + if (!this._merged) { throw "Call merge() first." } + this.logger.info(`Writing ${output_path} ...`) + write_yaml(output_path, this._spec) + return this + } + + merge (): OpenApiMerger { + if (this._merged) { throw "Spec already merged." } this.#merge_schemas() this.#merge_namespaces() this.#sort_spec_keys() this.#generate_global_params() this.#generate_superseded_ops() + this._merged = true + return this + } - this.logger.info(`Writing ${output_path} ...`) - - if (output_path !== '') write_yaml(output_path, this.spec) - return this.spec as OpenAPIV3.Document + spec(): OpenAPIV3.Document { + if (!this._merged) { this.merge() } + return this._spec as OpenAPIV3.Document } // Merge files from /namespaces folder. @@ -59,21 +74,83 @@ export default class OpenApiMerger { fs.readdirSync(folder).forEach(file => { this.logger.info(`Merging namespaces in ${folder}/${file} ...`) const spec = read_yaml(`${folder}/${file}`) - this.redirect_refs_in_namespace(spec) - this.spec.paths = { ...this.spec.paths, ...spec.paths } - this.spec.components.parameters = { ...this.spec.components.parameters, ...spec.components.parameters } - this.spec.components.responses = { ...this.spec.components.responses, ...spec.components.responses } - this.spec.components.requestBodies = { ...this.spec.components.requestBodies, ...spec.components.requestBodies } + this.#redirect_refs_in_namespace(spec) + this._spec.paths = { ...this._spec.paths, ...spec.paths } + this._spec.components.parameters = { ...this._spec.components.parameters, ...spec.components.parameters } + this._spec.components.responses = { ...this._spec.components.responses, ...spec.components.responses } + this._spec.components.requestBodies = { ...this._spec.components.requestBodies, ...spec.components.requestBodies } + }) + + this.#remove_refs_per_semver() + } + + // 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) + + // parameters + const removed_params = this.#remove_per_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]) => { + method_item.parameters = _.filter(method_item.parameters, (param) => !_.includes(removed_parameter_refs, param.$ref)) + }) + }) + + // responses + const removed_responses = this.#remove_per_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]) => { + method_item.responses = _.omitBy(method_item.responses, (param) => _.includes(removed_response_refs, param.$ref)) + }) }) + + this._spec.paths = _.omitBy(this._spec.paths, isEmpty) + } + + #exclude_per_semver(obj: any): boolean { + if (this.target_version === undefined) return false + + const x_version_added = semver.coerce(obj['x-version-added'] as string) + const x_version_removed = semver.coerce(obj['x-version-removed'] as string) + + if (x_version_added && !semver.satisfies(this.target_version, `>=${x_version_added?.toString()}`)) { + return true + } else if (x_version_removed && !semver.satisfies(this.target_version, `<${x_version_removed?.toString()}`)) { + return true + } + + return false + } + + // 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 } // Redirect schema references in namespace files to local references in single-file spec. - redirect_refs_in_namespace (obj: any): void { + #redirect_refs_in_namespace (obj: any): void { const ref: string = obj?.$ref if (ref?.startsWith('../schemas/')) { obj.$ref = ref.replace('../schemas/', '#/components/schemas/').replace('.yaml#/components/schemas/', ':') } for (const key in obj) { - if (typeof obj[key] === 'object') { this.redirect_refs_in_namespace(obj[key]) } + if (typeof obj[key] === 'object') { this.#redirect_refs_in_namespace(obj[key]) } } } @@ -92,7 +169,7 @@ export default class OpenApiMerger { Object.entries(this.schemas).forEach(([category, schemas]) => { Object.entries(schemas).forEach(([name, schema_obj]) => { - this.spec.components.schemas[`${category}:${name}`] = schema_obj + this._spec.components.schemas[`${category}:${name}`] = schema_obj }) }) } @@ -115,26 +192,26 @@ export default class OpenApiMerger { // Sort keys in the spec to make it easier to read and compare. #sort_spec_keys (): void { - this.spec.components.schemas = _.fromPairs(Object.entries(this.spec.components.schemas as Document).sort((a, b) => a[0].localeCompare(b[0]))) - this.spec.components.parameters = _.fromPairs(Object.entries(this.spec.components.parameters as Document).sort((a, b) => a[0].localeCompare(b[0]))) - this.spec.components.responses = _.fromPairs(Object.entries(this.spec.components.responses as Document).sort((a, b) => a[0].localeCompare(b[0]))) - this.spec.components.requestBodies = _.fromPairs(Object.entries(this.spec.components.requestBodies as Document).sort((a, b) => a[0].localeCompare(b[0]))) - - this.spec.paths = _.fromPairs(Object.entries(this.spec.paths as Document).sort((a, b) => a[0].localeCompare(b[0]))) - Object.entries(this.spec.paths as Document).forEach(([path, path_item]) => { - this.spec.paths[path] = _.fromPairs(Object.entries(path_item as Document).sort((a, b) => a[0].localeCompare(b[0]))) + this._spec.components.schemas = _.fromPairs(Object.entries(this._spec.components.schemas as Document).sort((a, b) => a[0].localeCompare(b[0]))) + this._spec.components.parameters = _.fromPairs(Object.entries(this._spec.components.parameters as Document).sort((a, b) => a[0].localeCompare(b[0]))) + this._spec.components.responses = _.fromPairs(Object.entries(this._spec.components.responses as Document).sort((a, b) => a[0].localeCompare(b[0]))) + this._spec.components.requestBodies = _.fromPairs(Object.entries(this._spec.components.requestBodies as Document).sort((a, b) => a[0].localeCompare(b[0]))) + + this._spec.paths = _.fromPairs(Object.entries(this._spec.paths as Document).sort((a, b) => a[0].localeCompare(b[0]))) + Object.entries(this._spec.paths as Document).forEach(([path, path_item]) => { + this._spec.paths[path] = _.fromPairs(Object.entries(path_item as Document).sort((a, b) => a[0].localeCompare(b[0]))) }) } // Generate global parameters from _global_params.yaml file. #generate_global_params (): void { const gen = new GlobalParamsGenerator(this.root_folder) - gen.generate(this.spec) + gen.generate(this._spec) } // Generate superseded operations from _superseded_operations.yaml file. #generate_superseded_ops (): void { const gen = new SupersededOpsGenerator(this.root_folder, this.logger) - gen.generate(this.spec) + gen.generate(this._spec) } } diff --git a/tools/src/merger/merge.ts b/tools/src/merger/merge.ts index 50af3360b..0ad3d4eed 100644 --- a/tools/src/merger/merge.ts +++ b/tools/src/merger/merge.ts @@ -17,12 +17,13 @@ const command = new Command() .addOption(new Option('-s, --source ', 'path to the root folder of the multi-file spec').default(resolve(__dirname, '../../../spec'))) .addOption(new Option('-o, --output ', 'output file name').default(resolve(__dirname, '../../../build/opensearch-openapi.yaml'))) .addOption(new Option('--verbose', 'show merge details').default(false)) + .addOption(new Option('--opensearch-version ', 'target OpenSearch schema version').default(undefined)) .allowExcessArguments(false) .parse() const opts = command.opts() const logger = new Logger(opts.verbose ? LogLevel.info : LogLevel.warn) -const merger = new OpenApiMerger(opts.source, logger) -logger.log(`Merging ${opts.source} into ${opts.output} ...`) -merger.merge(opts.output) +const merger = new OpenApiMerger(opts.source, opts.opensearchVersion, logger) +logger.log(`Merging ${opts.source} into ${opts.output} (${opts.opensearchVersion}) ...`) +merger.merge().write_to(opts.output) logger.log('Done.') diff --git a/tools/src/tester/MergedOpenApiSpec.ts b/tools/src/tester/MergedOpenApiSpec.ts index 93a1e25dc..ddc157ece 100644 --- a/tools/src/tester/MergedOpenApiSpec.ts +++ b/tools/src/tester/MergedOpenApiSpec.ts @@ -17,16 +17,20 @@ import OpenApiMerger from '../merger/OpenApiMerger'; export default class MergedOpenApiSpec { logger: Logger file_path: string + target_version?: string + protected _spec: OpenAPIV3.Document | undefined - constructor (spec_path: string, logger: Logger = new Logger()) { + constructor (spec_path: string, target_version?: string, logger: Logger = new Logger()) { this.logger = logger this.file_path = spec_path + this.target_version = target_version } spec (): OpenAPIV3.Document { if (this._spec) return this._spec - const spec = (new OpenApiMerger(this.file_path, this.logger)).merge() + const merger = new OpenApiMerger(this.file_path, this.target_version, this.logger) + const spec = merger.merge().spec() const ctx = new SpecificationContext(this.file_path) this.inject_additional_properties(ctx, spec) this._spec = spec diff --git a/tools/src/tester/test.ts b/tools/src/tester/test.ts index 6fa7b5f68..6facfb4a3 100644 --- a/tools/src/tester/test.ts +++ b/tools/src/tester/test.ts @@ -40,6 +40,7 @@ const command = new Command() ) .addOption(new Option('--verbose', 'whether to print the full stack trace of errors').default(false)) .addOption(new Option('--dry-run', 'dry run only, do not make HTTP requests').default(false)) + .addOption(new Option('--opensearch-version ', 'target OpenSearch schema version').default(undefined)) .addOption(OPENSEARCH_URL_OPTION) .addOption(OPENSEARCH_USERNAME_OPTION) .addOption(OPENSEARCH_PASSWORD_OPTION) @@ -50,7 +51,7 @@ const command = new Command() const opts = command.opts() const logger = new Logger(opts.verbose ? LogLevel.info : LogLevel.warn) -const spec = new MergedOpenApiSpec(opts.specPath, new Logger(LogLevel.error)) +const spec = new MergedOpenApiSpec(opts.specPath, opts.opensearchVersion, new Logger(LogLevel.error)) const http_client = new OpenSearchHttpClient(get_opensearch_opts_from_cli({ opensearchResponseType: 'arraybuffer', ...opts })) const chapter_reader = new ChapterReader(http_client, logger) const chapter_evaluator = new ChapterEvaluator(new OperationLocator(spec.spec()), chapter_reader, new SchemaValidator(spec.spec(), logger), logger) diff --git a/tools/tests/merger/OpenApiMerger.test.ts b/tools/tests/merger/OpenApiMerger.test.ts index 72eeef042..1169b2101 100644 --- a/tools/tests/merger/OpenApiMerger.test.ts +++ b/tools/tests/merger/OpenApiMerger.test.ts @@ -9,12 +9,55 @@ import OpenApiMerger from 'merger/OpenApiMerger' import fs from 'fs' -import { Logger, LogLevel } from 'Logger' - -test('merge()', () => { - const merger = new OpenApiMerger('./tools/tests/merger/fixtures/spec/', new Logger(LogLevel.error)) - merger.merge('./tools/tests/merger/opensearch-openapi.yaml') - expect(fs.readFileSync('./tools/tests/merger/fixtures/expected.yaml', 'utf8')) - .toEqual(fs.readFileSync('./tools/tests/merger/opensearch-openapi.yaml', 'utf8')) - fs.unlinkSync('./tools/tests/merger/opensearch-openapi.yaml') + +describe('OpenApiMerger', () => { + var merger: OpenApiMerger + + describe('defaults', () => { + beforeEach(() => { + merger = new OpenApiMerger('./tools/tests/merger/fixtures/spec/') + }) + + describe('merge()', () => { + test('is not required', () => { + expect(merger.spec()).toBeDefined() + }) + + test('merges spec', () => { + merger.merge() + expect(merger.spec()).toBeDefined() + }) + + test('raises an error when called twice', () => { + merger.merge() + expect(() => { + merger.merge() + }).toThrow('Spec already merged.'); + }) + }) + + describe('write_to()', () => { + afterAll(() => { + fs.unlinkSync('./tools/tests/merger/opensearch-openapi.yaml') + }) + + test('writes a spec', () => { + merger.merge().write_to('./tools/tests/merger/opensearch-openapi.yaml') + expect(fs.readFileSync('./tools/tests/merger/fixtures/expected_2.0.yaml', 'utf8')) + .toEqual(fs.readFileSync('./tools/tests/merger/opensearch-openapi.yaml', 'utf8')) + }) + }) + }) + + describe('1.3', () => { + beforeEach(() => { + merger = new OpenApiMerger('./tools/tests/merger/fixtures/spec/', '1.3') + }) + + test('writes a spec', () => { + merger.merge().write_to('./tools/tests/merger/opensearch-openapi.yaml') + expect(fs.readFileSync('./tools/tests/merger/fixtures/expected_1.3.yaml', 'utf8')) + .toEqual(fs.readFileSync('./tools/tests/merger/opensearch-openapi.yaml', 'utf8')) + }) + }) }) diff --git a/tools/tests/merger/fixtures/expected.yaml b/tools/tests/merger/fixtures/expected_1.3.yaml similarity index 96% rename from tools/tests/merger/fixtures/expected.yaml rename to tools/tests/merger/fixtures/expected_1.3.yaml index 3c73f80a1..d75111c45 100644 --- a/tools/tests/merger/fixtures/expected.yaml +++ b/tools/tests/merger/fixtures/expected_1.3.yaml @@ -92,6 +92,11 @@ components: application/json: schema: type: object + indices.create@201: + description: Added in 2.0. + application/json: + schema: + type: object schemas: actions:Bark: type: string diff --git a/tools/tests/merger/fixtures/expected_2.0.yaml b/tools/tests/merger/fixtures/expected_2.0.yaml new file mode 100644 index 000000000..3b547b720 --- /dev/null +++ b/tools/tests/merger/fixtures/expected_2.0.yaml @@ -0,0 +1,128 @@ +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/indices.create::query.wait_for_active_shards' + - $ref: '#/components/parameters/_global::query.human' + requestBody: + $ref: '#/components/requestBodies/indices.create' + responses: + '200': + $ref: '#/components/responses/indices.create@200' + '201': + $ref: '#/components/responses/indices.create@201' + x-version-added: '2.0' + /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 + indices.create::query.wait_for_active_shards: + name: pretty + in: query + x-version-added: '2.0' + 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' diff --git a/tools/tests/merger/fixtures/spec/namespaces/indices.yaml b/tools/tests/merger/fixtures/spec/namespaces/indices.yaml index 966038f3f..0dafa08e7 100644 --- a/tools/tests/merger/fixtures/spec/namespaces/indices.yaml +++ b/tools/tests/merger/fixtures/spec/namespaces/indices.yaml @@ -8,11 +8,15 @@ paths: parameters: - $ref: '#/components/parameters/indices.create::path.index' - $ref: '#/components/parameters/indices.create::query.pretty' + - $ref: '#/components/parameters/indices.create::query.wait_for_active_shards' requestBody: $ref: '#/components/requestBodies/indices.create' responses: '200': $ref: '#/components/responses/indices.create@200' + '201': + $ref: '#/components/responses/indices.create@201' + x-version-added: '2.0' components: requestBodies: indices.create: {} @@ -27,9 +31,20 @@ components: in: query schema: type: boolean + indices.create::query.wait_for_active_shards: + name: pretty + in: query + x-version-added: '2.0' + schema: + type: boolean responses: indices.create@200: description: '' + application/json: + schema: + type: object + indices.create@201: + description: Added in 2.0. application/json: schema: type: object \ No newline at end of file diff --git a/tools/tests/merger/opensearch-openapi.yaml b/tools/tests/merger/opensearch-openapi.yaml new file mode 100644 index 000000000..d75111c45 --- /dev/null +++ b/tools/tests/merger/opensearch-openapi.yaml @@ -0,0 +1,118 @@ +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' diff --git a/tools/tests/tester/MergedOpenApiSpec.test.ts b/tools/tests/tester/MergedOpenApiSpec.test.ts index 2acda940d..0805add0c 100644 --- a/tools/tests/tester/MergedOpenApiSpec.test.ts +++ b/tools/tests/tester/MergedOpenApiSpec.test.ts @@ -8,38 +8,87 @@ */ import { Logger } from 'Logger' +import _ from 'lodash' import MergedOpenApiSpec from "tester/MergedOpenApiSpec" describe('unevaluatedProperties', () => { - const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', new Logger()) - const responses: any = spec.spec().components?.responses + describe('2.0', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', '2.0', new Logger()) + const responses: any = spec.spec().components?.responses - test('has an api version', () => { - expect(spec.api_version()).toEqual('1.2.3') - }) + test('has an api version', () => { + expect(spec.api_version()).toEqual('1.2.3') + }) + + test('is added with required fields', () => { + const schema = responses['info@200'].content['application/json'].schema + expect(schema.unevaluatedProperties).toEqual({ not: true, errorMessage: 'property is not defined in the spec' }) + }) + + test('is added when no required fields', () => { + const schema = responses['info@500'].content['application/json'].schema + expect(schema.unevaluatedProperties).toEqual({ not: true, errorMessage: 'property is not defined in the spec' }) + }) + + test('is not added to empty object schema', () => { + const schema = responses['info@503'].content['application/json'].schema + expect(schema.unevaluatedProperties).toBeUndefined() + }) + + test('is not added when true', () => { + const schema = responses['info@201'].content['application/json'].schema + expect(schema.unevaluatedProperties).toEqual(true) + }) - test('is added with required fields', () => { - const schema = responses['info@200'].content['application/json'].schema - expect(schema.unevaluatedProperties).toEqual({ not: true, errorMessage: 'property is not defined in the spec' }) + test('is not added when object', () => { + const schema = responses['info@404'].content['application/json'].schema + expect(schema.unevaluatedProperties).toEqual({ type: 'object' }) + }) + + test('has matching responses', () => { + expect(_.keys(spec.spec().paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', 'added-2.0' + ]) + }) }) - test('is added when no required fields', () => { - const schema = responses['info@500'].content['application/json'].schema - expect(schema.unevaluatedProperties).toEqual({ not: true, errorMessage: 'property is not defined in the spec' }) + describe('2.1', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', '2.1', new Logger()) + + test('has matching responses', () => { + expect(_.keys(spec.spec().paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', 'added-2.0', 'added-2.1' + ]) + }) }) - test('is not added to empty object schema', () => { - const schema = responses['info@503'].content['application/json'].schema - expect(schema.unevaluatedProperties).toBeUndefined() + describe('1.3', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', '1.3', new Logger()) + + test('has matching responses', () => { + expect(_.keys(spec.spec().paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', 'removed-2.0', 'added-1.3-removed-2.0' + ]) + }) }) - test('is not added when true', () => { - const schema = responses['info@201'].content['application/json'].schema - expect(schema.unevaluatedProperties).toEqual(true) + describe('1.2', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', '1.2', new Logger()) + + test('has matching responses', () => { + expect(_.keys(spec.spec().paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', 'removed-2.0' + ]) + }) }) - test('is not added when object', () => { - const schema = responses['info@404'].content['application/json'].schema - expect(schema.unevaluatedProperties).toEqual({ type: 'object' }) + describe('3.0', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', '3.0', new Logger()) + + test('has matching responses', () => { + expect(_.keys(spec.spec().paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', 'added-2.0', 'added-2.1' + ]) + }) }) }) diff --git a/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml b/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml index 5cf78308e..141ca5ac5 100644 --- a/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml +++ b/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml @@ -14,6 +14,16 @@ paths: $ref: '#/components/responses/info@201' '404': $ref: '#/components/responses/info@404' + added-2.0: + $ref: '#/components/responses/info@added-2.0' + x-version-added: '2.0' + removed-2.0: + $ref: '#/components/responses/info@removed-2.0' + x-version-removed: '2.0' + added-1.3-removed-2.0: + $ref: '#/components/responses/info@added-1.3-removed-2.0' + added-2.1: + $ref: '#/components/responses/info@added-2.1' '500': $ref: '#/components/responses/info@500' components: @@ -54,6 +64,17 @@ components: - tagline unevaluatedProperties: type: object + info@added-2.0: + description: Added in 2.0 via attribute next to ref. + info@removed-2.0: + description: Removed in 2.0 via attribute next to ref. + info@added-1.3-removed-2.0: + description: 'Added in 1.3, removed in 2.0 via attribute in response body.' + x-version-added: '1.3' + x-version-removed: '2.0' + info@added-2.1: + description: Added in 2.1 via attribute in response body. + x-version-added: '2.1' info@500: description: '' content: