diff --git a/.github/workflows/test-spec.yml b/.github/workflows/test-spec.yml index 809c1a63..80640fe0 100644 --- a/.github/workflows/test-spec.yml +++ b/.github/workflows/test-spec.yml @@ -55,7 +55,10 @@ jobs: - name: Run Tests run: | - npm run test:spec -- --opensearch-insecure --coverage coverage/test-spec-coverage-${{ matrix.entry.version }}.json + npm run test:spec -- \ + --opensearch-insecure \ + --opensearch-version=${{ matrix.entry.version }} \ + --coverage coverage/test-spec-coverage-${{ matrix.entry.version }}.json - name: Upload Test Coverage Results uses: actions/upload-artifact@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index d6563e39..19de5fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added missing variants of `indices.put_alias` ([#434](https://github.com/opensearch-project/opensearch-api-specification/pull/434)) - Added `plugins` to NodeInfoSettings ([#442](https://github.com/opensearch-project/opensearch-api-specification/pull/442)) - Added test coverage ([#443](https://github.com/opensearch-project/opensearch-api-specification/pull/443)) +- 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 e941171c..b965e50a 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/eslint.config.mjs b/eslint.config.mjs index f8b14531..337f1472 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -56,7 +56,7 @@ export default [ { selector: 'typeProperty', format: null } ], '@typescript-eslint/no-confusing-void-expression': 'error', - '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-dynamic-delete': 'off', '@typescript-eslint/no-invalid-void-type': 'error', '@typescript-eslint/no-non-null-assertion': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', diff --git a/package-lock.json b/package-lock.json index 9387b5d5..190f2427 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 d433bb10..b574d42d 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 579f1138..97cee98f 100644 --- a/tools/src/helpers.ts +++ b/tools/src/helpers.ts @@ -47,7 +47,6 @@ export function sort_by_keys (obj: Record, priorities: string[] = [ return a[0].localeCompare(b[0]) }) sorted.forEach(([k, v]) => { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete obj[k] obj[k] = v }) @@ -65,6 +64,46 @@ export function sort_array_by_keys (values: any[], priorities: string[] = []): s }) } +export function delete_matching_keys(obj: any, condition: (obj: any) => boolean): void { + for (const key in obj) { + var item = obj[key] + if (_.isObject(item)) { + if (condition(item)) { + delete obj[key] + } else { + delete_matching_keys(item, condition) + } + } + } +} + +export function find_refs (current: Record, root?: Record, call_stack: string[] = []): Set { + var results = new Set() + + if (root === undefined) { + root = current + current = current.paths + } + + if (current?.$ref != null) { + const ref = current.$ref as string + results.add(ref) + const ref_node = resolve_ref(ref, root) + if (ref_node !== undefined && !call_stack.includes(ref)) { + call_stack.push(ref) + find_refs(ref_node, root, call_stack).forEach((ref) => results.add(ref)) + } + } + + if (_.isObject(current)) { + _.forEach(current, (v) => { + find_refs(v as Record, root, call_stack).forEach((ref) => results.add(ref)); + }) + } + + return results +} + export function ensure_parent_dir (file_path: string): void { fs.mkdirSync(path.dirname(file_path), { recursive: true }) } diff --git a/tools/src/linter/SchemasValidator.ts b/tools/src/linter/SchemasValidator.ts index 5192595c..d22c0bea 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, new Logger(LogLevel.error)) + this.spec = merger.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 51907863..ff8cd7d0 100644 --- a/tools/src/merger/OpenApiMerger.ts +++ b/tools/src/merger/OpenApiMerger.ts @@ -18,16 +18,18 @@ import { Logger } from '../Logger' // 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 + 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()) { this.logger = logger this.root_folder = fs.realpathSync(root_folder) - this.spec = { + this._spec = { openapi: '3.1.0', info: read_yaml(`${this.root_folder}/_info.yaml`, true), paths: {}, @@ -40,17 +42,23 @@ export default class OpenApiMerger { } } - merge (output_path: string = ''): OpenAPIV3.Document { - this.#merge_schemas() - this.#merge_namespaces() - this.#sort_spec_keys() - this.#generate_global_params() - this.#generate_superseded_ops() - + write_to(output_path: string): OpenApiMerger { this.logger.info(`Writing ${output_path} ...`) + write_yaml(output_path, this.spec()) + return this + } + + spec(): OpenAPIV3.Document { + if (!this._merged) { + this.#merge_schemas() + this.#merge_namespaces() + this.#sort_spec_keys() + this.#generate_global_params() + this.#generate_superseded_ops() + this._merged = true + } - if (output_path !== '') write_yaml(output_path, this.spec) - return this.spec as OpenAPIV3.Document + return this._spec as OpenAPIV3.Document } // Merge files from /namespaces folder. @@ -59,21 +67,21 @@ 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 } }) } // 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 +100,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 +123,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/OpenApiVersionExtractor.ts b/tools/src/merger/OpenApiVersionExtractor.ts new file mode 100644 index 00000000..5d4164b1 --- /dev/null +++ b/tools/src/merger/OpenApiVersionExtractor.ts @@ -0,0 +1,97 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import _, { extend, isEmpty } from 'lodash' +import { delete_matching_keys, find_refs, write_yaml } from '../helpers' +import { Logger } from '../Logger' +import { type OpenAPIV3 } from 'openapi-types' +import semver from 'semver' + +// Extract a versioned API +export default class OpenApiVersionExtractor { + private _spec?: Record + private _source_spec: OpenAPIV3.Document + private _target_version: string + private _logger: Logger + + constructor(source_spec: OpenAPIV3.Document, target_version: string, logger: Logger = new Logger()) { + this._source_spec = source_spec + this._target_version = semver.coerce(target_version)?.toString() ?? target_version + this._logger = logger + this._spec = undefined + } + + extract(): OpenAPIV3.Document { + if (this._spec) return this._spec as OpenAPIV3.Document + this._spec = _.cloneDeep(this._source_spec) + this.#extract() + return this._spec as OpenAPIV3.Document + } + + write_to(output_path: string): OpenApiVersionExtractor { + this._logger.info(`Writing ${output_path} ...`) + write_yaml(output_path, this.extract()) + return this + } + + // Remove any refs and objects that reference them that are x-version-added/removed + // incompatible with the target server version. + #extract() : void { + this._logger.info(`Extracting version ${this._target_version} ...`) + this.#remove_keys_not_matching_semver() + this.#remove_unused() + } + + #exclude_per_semver(obj: any): boolean { + 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_keys_not_matching_semver(): void { + delete_matching_keys(this._spec, this.#exclude_per_semver.bind(this)) + } + + #remove_unused(): void { + if (this._spec === undefined) return + + // remove anything that's not referenced + var references = find_refs(this._spec) + + this._spec.components = _.reduce(_.map(['parameters', 'requestBodies', 'responses', 'schemas'], (p) => + { + return { + [p]: _.pickBy( + this._spec?.components?.[p], (_value, key) => + references.has(`#/components/${p}/${key}`) + ) + } + } + ), extend) + + // collect what's left + var remaining = _.flatMap(['schemas', 'parameters', 'responses', 'requestBodies'], (key) => + _.keys(this._spec?.components?.[key]).map((ref) => `#/components/${key}/${ref}`) + ) + + delete_matching_keys(this._spec, (obj) => + obj.$ref !== undefined && !_.includes(remaining, obj.$ref) + ) + + this._spec.paths = _.omitBy(this._spec.paths, isEmpty) + } +} diff --git a/tools/src/merger/merge.ts b/tools/src/merger/merge.ts index 50af3360..b2dd16d8 100644 --- a/tools/src/merger/merge.ts +++ b/tools/src/merger/merge.ts @@ -8,21 +8,29 @@ */ import { Command, Option } from '@commander-js/extra-typings' -import OpenApiMerger from './OpenApiMerger' -import { resolve } from 'path' import { Logger, LogLevel } from '../Logger' +import { resolve } from 'path' +import OpenApiMerger from './OpenApiMerger' +import OpenApiVersionExtractor from './OpenApiVersionExtractor' const command = new Command() .description('Merges the multi-file OpenSearch spec into a single file for programmatic use.') .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) +if (opts.opensearchVersion === undefined) { + logger.log(`Merging ${opts.source} into ${opts.output} ...`) + merger.write_to(opts.output) +} else { + logger.log(`Merging ${opts.source} into ${opts.output} (${opts.opensearchVersion}) ...`) + const extractor = new OpenApiVersionExtractor(merger.spec(), opts.opensearchVersion) + extractor.write_to(opts.output) +} logger.log('Done.') diff --git a/tools/src/tester/MergedOpenApiSpec.ts b/tools/src/tester/MergedOpenApiSpec.ts index 56ee8b7a..ee21f55a 100644 --- a/tools/src/tester/MergedOpenApiSpec.ts +++ b/tools/src/tester/MergedOpenApiSpec.ts @@ -13,21 +13,30 @@ import { determine_possible_schema_types, HTTP_METHODS, SpecificationContext } f import { SchemaVisitor } from '../_utils/SpecificationVisitor'; import OpenApiMerger from '../merger/OpenApiMerger'; import _ from 'lodash'; +import OpenApiVersionExtractor from '../merger/OpenApiVersionExtractor'; // An augmented spec with additionalProperties: false. 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.logger) + var spec = merger.spec() + if (this.target_version !== undefined) { + const version_extractor = new OpenApiVersionExtractor(spec, this.target_version) + spec = version_extractor.extract() + } 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 5c55a544..98a875e0 100644 --- a/tools/src/tester/test.ts +++ b/tools/src/tester/test.ts @@ -41,6 +41,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) @@ -52,7 +53,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/helpers.test.ts b/tools/tests/helpers.test.ts index 60480af3..54773d22 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, find_refs, sort_array_by_keys, to_json, to_ndjson } from '../src/helpers' describe('helpers', () => { describe('sort_array_by_keys', () => { @@ -36,4 +37,143 @@ 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 = {} + delete_matching_keys(obj, (_obj) => false) + expect(obj).toEqual({}) + }) + + describe('an object', () => { + var obj: object + + beforeEach(() => { + obj = { + foo: { + bar1: { + x: 1 + } + }, + zar: { + bar2: { + y: 2 + } + } + } + }) + + test('removes all keys', () => { + delete_matching_keys(obj, (_item) => true) + expect(obj).toStrictEqual({}) + }) + + test('removes no keys', () => { + const obj2 = _.cloneDeep(obj) + delete_matching_keys(obj, (_item) => false) + expect(obj).toStrictEqual(obj2) + }) + + test('removes a value from a key', () => { + delete_matching_keys(obj, (_item: any) => _item.x == 1) + expect(obj).toStrictEqual({ foo: {}, zar: { bar2: { y: 2 } } }) + }) + + test('removes multiple values from a key', () => { + delete_matching_keys(obj, (_item: any) => _item.x == 1 || _item.y == 2) + 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', () => { + delete_matching_keys(obj, (_item) => true) + expect(obj).toStrictEqual({}) + }) + + test('removes no keys', () => { + const obj2 = _.cloneDeep(obj) + delete_matching_keys(obj, (_item) => false) + expect(obj).toStrictEqual(obj2) + }) + + test('removes a value from a key', () => { + delete_matching_keys(obj, (_item: any) => _item.x == 1) + expect(obj).toStrictEqual({ foo: [{ bar2: { y: 2 } }] }) + }) + + test('removes multiple values from a key', () => { + delete_matching_keys(obj, (_item: any) => _item.x == 1 || _item.y == 2) + expect(obj).toStrictEqual({ foo: [{}] }) + }) + }) + }) + + describe('find_refs', () => { + test('empty collection', () => { + expect(find_refs({})).toEqual(new Set()) + }) + + test('with refs', () => { + expect(find_refs({ + paths: { + $ref: '#1', + null: null, + undefined, + obj: { + $ref: '#2', + obj: { + $ref: '#3' + }, + schema_obj: { + $ref: '#/schemas/schema1' + } + }, + arr: [{ + obj1: { + $ref: '#dup', + }, + obj2: { + $ref: '#dup', + }, + }] + }, + schemas: { + schema1: { + items: { + $ref: '#/schemas/schema2' + } + }, + schema2: { + x: 1 + } + }, + unused1: { + $ref: '#unused2', + obj: { + $ref: '#unused3' + } + }, + '#1': {}, + '#2': {}, + '#3': {}, + '#dup': {} + })).toEqual(new Set(['#1', '#2', '#3', '#dup', '#/schemas/schema1', '#/schemas/schema2'])) + }) + }) }) diff --git a/tools/tests/merger/OpenApiMerger.test.ts b/tools/tests/merger/OpenApiMerger.test.ts index 72eeef04..057866dd 100644 --- a/tools/tests/merger/OpenApiMerger.test.ts +++ b/tools/tests/merger/OpenApiMerger.test.ts @@ -9,12 +9,45 @@ 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') +import tmp from 'tmp' + +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', () => { + expect(merger.spec()).toBeDefined() + }) + }) + + describe('write_to()', () => { + 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(filename) + expect(fs.readFileSync('./tools/tests/merger/fixtures/merger/expected.yaml', 'utf8')) + .toEqual(fs.readFileSync(filename, 'utf8')) + }) + }) + }) }) diff --git a/tools/tests/merger/OpenApiVersionExtractor.test.ts b/tools/tests/merger/OpenApiVersionExtractor.test.ts new file mode 100644 index 00000000..61bc4262 --- /dev/null +++ b/tools/tests/merger/OpenApiVersionExtractor.test.ts @@ -0,0 +1,93 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import _ from 'lodash' +import OpenApiMerger from 'merger/OpenApiMerger' +import OpenApiVersionExtractor from 'merger/OpenApiVersionExtractor' +import fs from 'fs' +import tmp from 'tmp' + +describe('extract() from a merged API spec', () => { + const merger = new OpenApiMerger('tools/tests/tester/fixtures/specs/complete') + + describe('1.3', () => { + const extractor = new OpenApiVersionExtractor(merger.spec(), '1.3') + + describe('write_to', () => { + 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', () => { + extractor.write_to(filename) + expect(fs.readFileSync('./tools/tests/merger/fixtures/extractor/expected_1.3.yaml', 'utf8')) + .toEqual(fs.readFileSync(filename, 'utf8')) + }) + }) + + test('has matching responses', () => { + const spec = extractor.extract() + expect(_.keys(spec.paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', '503', 'removed-2.0', 'added-1.3-removed-2.0' + ]) + }) + }) + + describe('2.0', () => { + const extractor = new OpenApiVersionExtractor(merger.spec(), '2.0') + + test('has matching responses', () => { + const spec = extractor.extract() + expect(_.keys(spec.paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', '503', 'added-2.0' + ]) + }) + + describe('write_to()', () => { + 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', () => { + extractor.write_to(filename) + expect(fs.readFileSync('./tools/tests/merger/fixtures/extractor/expected_2.0.yaml', 'utf8')) + .toEqual(fs.readFileSync(filename, 'utf8')) + }) + }) + }) + + describe('2.1', () => { + const extractor = new OpenApiVersionExtractor(merger.spec(), '2.1') + + test('has matching responses', () => { + const spec = extractor.extract() + expect(_.keys(spec.paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', '503', 'added-2.0', 'added-2.1' + ]) + }) + }) +}) diff --git a/tools/tests/merger/fixtures/extractor/expected_1.3.yaml b/tools/tests/merger/fixtures/extractor/expected_1.3.yaml new file mode 100644 index 00000000..81eecb28 --- /dev/null +++ b/tools/tests/merger/fixtures/extractor/expected_1.3.yaml @@ -0,0 +1,125 @@ +openapi: 3.1.0 +info: + title: OpenSearch API + description: OpenSearch API + version: 1.0.0 + x-api-version: 1.2.3 +paths: + /_nodes/{id}: + get: + operationId: nodes.info.1 + x-operation-group: nodes.info + x-version-added: '1.0' + description: Returns information about nodes in the cluster. + parameters: + - $ref: '#/components/parameters/nodes.info::path.id' + responses: + '200': + $ref: '#/components/responses/nodes.info@200' + /index: + get: + operationId: get.0 + responses: + '200': + $ref: '#/components/responses/info@200' + '201': + $ref: '#/components/responses/info@201' + '404': + $ref: '#/components/responses/info@404' + '500': + $ref: '#/components/responses/info@500' + '503': + $ref: '#/components/responses/info@503' + 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' + parameters: [] + /nodes: + get: + operationId: nodes.0 + responses: + '200': + $ref: '#/components/responses/nodes.info@200' + parameters: [] +components: + parameters: + nodes.info::path.id: + in: path + name: id + description: Node ID. + required: true + schema: + type: string + requestBodies: {} + responses: + info@200: + description: '' + content: + application/json: + schema: + type: object + properties: + _type: + $ref: '#/components/schemas/_common:Type' + tagline: + type: string + required: + - tagline + info@201: + description: '' + content: + application/json: + schema: + type: object + properties: + tagline: + type: string + required: + - tagline + unevaluatedProperties: true + info@404: + description: '' + content: + application/json: + schema: + type: object + properties: + tagline: + type: string + required: + - tagline + unevaluatedProperties: + type: object + info@500: + description: '' + content: + application/json: + schema: + type: object + properties: + tagline: + type: string + info@503: + description: '' + content: + application/json: + schema: + type: object + 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@removed-2.0: + description: Removed in 2.0 via attribute next to ref. + nodes.info@200: + description: All nodes. + content: + application/json: + schema: + type: object + schemas: + _common:Type: + type: string + x-version-removed: '2.0' diff --git a/tools/tests/merger/fixtures/extractor/expected_2.0.yaml b/tools/tests/merger/fixtures/extractor/expected_2.0.yaml new file mode 100644 index 00000000..95e11986 --- /dev/null +++ b/tools/tests/merger/fixtures/extractor/expected_2.0.yaml @@ -0,0 +1,172 @@ +openapi: 3.1.0 +info: + title: OpenSearch API + description: OpenSearch API + version: 1.0.0 + x-api-version: 1.2.3 +paths: + /_nodes/{id}: + get: + operationId: nodes.info.1 + x-operation-group: nodes.info + x-version-added: '1.0' + description: Returns information about nodes in the cluster. + parameters: + - $ref: '#/components/parameters/nodes.info::path.id' + responses: + '200': + $ref: '#/components/responses/nodes.info@200' + post: + operationId: nodes.info.1 + x-operation-group: nodes.info + x-version-added: '2.0' + description: Returns information about nodes in the cluster. + parameters: + - $ref: '#/components/parameters/nodes.info::path.id' + - $ref: '#/components/parameters/nodes.info::query.flag' + requestBody: + $ref: '#/components/requestBodies/nodes.info' + responses: + '200': + $ref: '#/components/responses/nodes.info@200' + '201': + $ref: '#/components/responses/nodes.info@201' + /cluster_manager: + get: + operationId: cluster_manager.0 + x-version-added: '2.0' + parameters: [] + post: + operationId: cluster_manager.0 + x-version-added: '2.0' + parameters: [] + /index: + get: + operationId: get.0 + responses: + '200': + $ref: '#/components/responses/info@200' + '201': + $ref: '#/components/responses/info@201' + '404': + $ref: '#/components/responses/info@404' + '500': + $ref: '#/components/responses/info@500' + '503': + $ref: '#/components/responses/info@503' + added-2.0: + $ref: '#/components/responses/info@added-2.0' + x-version-added: '2.0' + parameters: [] + /nodes: + get: + operationId: nodes.0 + responses: + '200': + $ref: '#/components/responses/nodes.info@200' + parameters: [] +components: + parameters: + nodes.info::path.id: + in: path + name: id + description: Node ID. + required: true + schema: + type: string + nodes.info::query.flag: + in: query + name: flag + description: Flag. + required: false + schema: + type: boolean + default: false + requestBodies: + nodes.info: + content: + application/json: + schema: + type: object + properties: + _all: + type: boolean + ids: + $ref: '#/components/schemas/_common:Ids' + x-version-added: '2.0' + description: Nodes options. + responses: + info@200: + description: '' + content: + application/json: + schema: + type: object + properties: + tagline: + type: string + required: + - tagline + info@201: + description: '' + content: + application/json: + schema: + type: object + properties: + tagline: + type: string + required: + - tagline + unevaluatedProperties: true + info@404: + description: '' + content: + application/json: + schema: + type: object + properties: + tagline: + type: string + required: + - tagline + unevaluatedProperties: + type: object + info@500: + description: '' + content: + application/json: + schema: + type: object + properties: + tagline: + type: string + info@503: + description: '' + content: + application/json: + schema: + type: object + info@added-2.0: + description: Added in 2.0 via attribute next to ref. + nodes.info@200: + description: All nodes. + content: + application/json: + schema: + type: object + nodes.info@201: + description: All nodes. + content: + application/json: + schema: + type: object + schemas: + _common:Ids: + oneOf: + - $ref: '#/components/schemas/_common:OldId' + - type: array + items: + $ref: '#/components/schemas/_common:Ids' + _common:OldId: + type: string diff --git a/tools/tests/merger/fixtures/expected.yaml b/tools/tests/merger/fixtures/merger/expected.yaml similarity index 85% rename from tools/tests/merger/fixtures/expected.yaml rename to tools/tests/merger/fixtures/merger/expected.yaml index 3c73f80a..ef0560ad 100644 --- a/tools/tests/merger/fixtures/expected.yaml +++ b/tools/tests/merger/fixtures/merger/expected.yaml @@ -9,12 +9,16 @@ 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' - $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 @@ -78,9 +82,17 @@ 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 requestBodies: adopt: {} - indices.create: {} + indices.create: + name: + type: string responses: adopt@200: description: '' @@ -92,6 +104,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/spec/namespaces/indices.yaml b/tools/tests/merger/fixtures/spec/namespaces/indices.yaml index 966038f3..d5f1fcb9 100644 --- a/tools/tests/merger/fixtures/spec/namespaces/indices.yaml +++ b/tools/tests/merger/fixtures/spec/namespaces/indices.yaml @@ -8,14 +8,20 @@ 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: {} + indices.create: + name: + type: string parameters: indices.create::path.index: name: index @@ -27,9 +33,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/tester/MergedOpenApiSpec.test.ts b/tools/tests/tester/MergedOpenApiSpec.test.ts index 2c656082..7e6c6464 100644 --- a/tools/tests/tester/MergedOpenApiSpec.test.ts +++ b/tools/tests/tester/MergedOpenApiSpec.test.ts @@ -8,49 +8,89 @@ */ import { Logger } from 'Logger' +import _ from 'lodash' import MergedOpenApiSpec from "tester/MergedOpenApiSpec" describe('merged API spec', () => { - const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', new Logger()) + describe('defaults', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', undefined, new Logger()) - test('has an api version', () => { - expect(spec.api_version()).toEqual('1.2.3') - }) - - test('paths', () => { - expect(spec.paths()).toEqual({ - '/_nodes/{id}': ['get', 'post'], - '/index': ['get'], - '/nodes': ['get'] + test('has an api version', () => { + expect(spec.api_version()).toEqual('1.2.3') }) - }) - describe('unevaluatedProperties', () => { - const responses: any = spec.spec().components?.responses + test('paths', () => { + expect(spec.paths()).toEqual({ + '/_nodes/{id}': ['get', 'post'], + '/cluster_manager': ['get', 'post'], + '/index': ['get'], + '/nodes': ['get'] + }) + }) - 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('has all responses', () => { + expect(_.keys(spec.spec().paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500','503', 'added-2.0', 'removed-2.0', 'added-1.3-removed-2.0', 'added-2.1' + ]) }) - 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('unevaluatedProperties', () => { + const responses: any = spec.spec().components?.responses + + 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 not added when object', () => { + const schema = responses['info@404'].content['application/json'].schema + expect(schema.unevaluatedProperties).toEqual({ type: 'object' }) + }) }) + }) - 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', '503', '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('2.0', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', '2.0', new Logger()) + + test('has matching responses', () => { + expect(_.keys(spec.spec().paths['/index']?.get?.responses)).toEqual([ + '200', '201', '404', '500', '503', 'added-2.0' + ]) }) + }) + + describe('2.1', () => { + const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete', '2.1', new Logger()) - 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', '503', 'added-2.0', 'added-2.1' + ]) }) }) }) diff --git a/tools/tests/tester/ResultLogger.test.ts b/tools/tests/tester/ResultLogger.test.ts index 2eea82b2..1d2392bb 100644 --- a/tools/tests/tester/ResultLogger.test.ts +++ b/tools/tests/tester/ResultLogger.test.ts @@ -75,7 +75,7 @@ describe('ConsoleResultLogger', () => { expect(log.mock.calls).toEqual([ [], - ['Tested 1/4 paths.'] + ['Tested 1/6 paths.'] ]) }) }) diff --git a/tools/tests/tester/TestResults.test.ts b/tools/tests/tester/TestResults.test.ts index fb8b2585..ac06a635 100644 --- a/tools/tests/tester/TestResults.test.ts +++ b/tools/tests/tester/TestResults.test.ts @@ -39,7 +39,7 @@ describe('TestResults', () => { }) test('spec_paths_count', () => { - expect(test_results.spec_paths_count()).toEqual(4) + expect(test_results.spec_paths_count()).toEqual(6) }) test('write_coverage', () => { @@ -47,8 +47,8 @@ describe('TestResults', () => { test_results.write_coverage(filename) expect(JSON.parse(fs.readFileSync(filename, 'utf8'))).toEqual({ evaluated_paths_count: 1, - evaluated_paths_pct: 25, - paths_count: 4 + evaluated_paths_pct: 16.67, + paths_count: 6 }) fs.unlinkSync(filename) }) diff --git a/tools/tests/tester/fixtures/specs/complete/namespaces/cluster_manager.yaml b/tools/tests/tester/fixtures/specs/complete/namespaces/cluster_manager.yaml new file mode 100644 index 00000000..680169ad --- /dev/null +++ b/tools/tests/tester/fixtures/specs/complete/namespaces/cluster_manager.yaml @@ -0,0 +1,17 @@ +openapi: 3.1.0 +info: + title: OpenSearch API + description: OpenSearch API + version: 1.0.0 +paths: + /cluster_manager: + get: + operationId: cluster_manager.0 + x-version-added: '2.0' + post: + operationId: cluster_manager.0 + x-version-added: '2.0' +components: + requestBodies: [] + parameters: [] + responses: [] diff --git a/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml b/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml index 5cf78308..a2402b35 100644 --- a/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml +++ b/tools/tests/tester/fixtures/specs/complete/namespaces/index.yaml @@ -14,8 +14,20 @@ 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' + '503': + $ref: '#/components/responses/info@503' components: responses: info@200: @@ -25,6 +37,8 @@ components: schema: type: object properties: + _type: + $ref: '../schemas/_common.yaml#/components/schemas/Type' tagline: type: string required: @@ -54,6 +68,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: diff --git a/tools/tests/tester/fixtures/specs/complete/namespaces/nodes.yaml b/tools/tests/tester/fixtures/specs/complete/namespaces/nodes.yaml index 87abb9db..2b39054e 100644 --- a/tools/tests/tester/fixtures/specs/complete/namespaces/nodes.yaml +++ b/tools/tests/tester/fixtures/specs/complete/namespaces/nodes.yaml @@ -24,13 +24,16 @@ paths: post: operationId: nodes.info.1 x-operation-group: nodes.info - x-version-added: '1.0' + x-version-added: '2.0' description: Returns information about nodes in the cluster. parameters: - $ref: '#/components/parameters/nodes.info::path.id' + - $ref: '#/components/parameters/nodes.info::query.flag' requestBody: $ref: '#/components/requestBodies/nodes.info' responses: + '201': + $ref: '#/components/responses/nodes.info@201' '200': $ref: '#/components/responses/nodes.info@200' components: @@ -43,6 +46,9 @@ components: properties: _all: type: boolean + ids: + $ref: '../schemas/_common.yaml#/components/schemas/Ids' + x-version-added: '2.0' description: Nodes options. parameters: nodes.info::path.id: @@ -52,6 +58,23 @@ components: required: true schema: type: string + nodes.info::query.flag: + in: query + name: flag + description: Flag. + required: false + schema: + type: boolean + default: false + nodes.info::query.new: + in: query + name: new + description: New option added in 2.1. + required: false + x-version-added: '2.1' + schema: + type: array + default: false responses: nodes.info@200: description: All nodes. @@ -59,3 +82,9 @@ components: application/json: schema: type: object + nodes.info@201: + description: All nodes. + content: + application/json: + schema: + type: object diff --git a/tools/tests/tester/fixtures/specs/complete/schemas/_common.yaml b/tools/tests/tester/fixtures/specs/complete/schemas/_common.yaml new file mode 100644 index 00000000..c3a4e41f --- /dev/null +++ b/tools/tests/tester/fixtures/specs/complete/schemas/_common.yaml @@ -0,0 +1,18 @@ +openapi: 3.1.0 +info: + title: Schemas of _common category + description: Schemas of _common category + version: 1.0.0 +components: + schemas: + Type: + type: string + x-version-removed: '2.0' + OldId: + type: string + Ids: + oneOf: + - $ref: '#/components/schemas/OldId' + - type: array + items: + $ref: '#/components/schemas/Ids'