Skip to content

Commit aec3145

Browse files
authored
Delete schema elements that don't match target OpenSearch version. (#428)
* Delete schema elements that don't match target OpenSearch version. Signed-off-by: dblock <[email protected]>
1 parent 6fd9afe commit aec3145

27 files changed

+1013
-86
lines changed

.github/workflows/test-spec.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ jobs:
5555

5656
- name: Run Tests
5757
run: |
58-
npm run test:spec -- --opensearch-insecure --coverage coverage/test-spec-coverage-${{ matrix.entry.version }}.json
58+
npm run test:spec -- \
59+
--opensearch-insecure \
60+
--opensearch-version=${{ matrix.entry.version }} \
61+
--coverage coverage/test-spec-coverage-${{ matrix.entry.version }}.json
5962
6063
- name: Upload Test Coverage Results
6164
uses: actions/upload-artifact@v4

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5454
- Added missing variants of `indices.put_alias` ([#434](https://github.com/opensearch-project/opensearch-api-specification/pull/434))
5555
- Added `plugins` to NodeInfoSettings ([#442](https://github.com/opensearch-project/opensearch-api-specification/pull/442))
5656
- Added test coverage ([#443](https://github.com/opensearch-project/opensearch-api-specification/pull/443))
57+
- Added `--opensearch-version` to `merger` that excludes schema elements per semver ([#428](https://github.com/opensearch-project/opensearch-api-specification/pull/428))
5758

5859
### Changed
5960

DEVELOPER_GUIDE.md

+7
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ The merger tool merges the multi-file OpenSearch spec into a single file for pro
173173

174174
- `--source <path>`: The path to the root folder of the multi-file spec, defaults to `<repository-root>/spec`.
175175
- `--output <path>`: The path to write the final merged spec to, defaults to `<repository-root>/build/opensearch-openapi.yaml`.
176+
- `--opensearch-version`: An optional target version of OpenSearch, checking values of `x-version-added` and `x-version-removed`.
176177

177178
#### Example
178179

@@ -181,6 +182,12 @@ We can take advantage of the default values and simply merge the specification v
181182
npm run merge
182183
```
183184

185+
To generate a spec that does not contain any APIs or fields removed in version 2.0 (e.g. document `_type` fields).
186+
187+
```bash
188+
npm run merge -- --opensearch-version=2.0
189+
```
190+
184191
### [Spec Linter](tools/src/linter)
185192

186193
```bash

eslint.config.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default [
5656
{ selector: 'typeProperty', format: null }
5757
],
5858
'@typescript-eslint/no-confusing-void-expression': 'error',
59-
'@typescript-eslint/no-dynamic-delete': 'error',
59+
'@typescript-eslint/no-dynamic-delete': 'off',
6060
'@typescript-eslint/no-invalid-void-type': 'error',
6161
'@typescript-eslint/no-non-null-assertion': 'error',
6262
'@typescript-eslint/no-unnecessary-type-assertion': 'error',

package-lock.json

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,29 @@
3030
"@types/lodash": "^4.14.202",
3131
"@types/node": "^20.10.3",
3232
"@types/qs": "^6.9.15",
33+
"@types/tmp": "^0.2.6",
3334
"@typescript-eslint/eslint-plugin": "^6.21.0",
35+
"ajv": "^8.13.0",
3436
"ajv-errors": "^3.0.0",
3537
"ajv-formats": "^3.0.1",
36-
"ajv": "^8.13.0",
3738
"axios": "^1.7.1",
3839
"cbor": "^9.0.2",
3940
"commander": "^12.0.0",
41+
"eslint": "^8.57.0",
4042
"eslint-config-standard-with-typescript": "^43.0.1",
4143
"eslint-plugin-eslint-comments": "^3.2.0",
4244
"eslint-plugin-import": "^2.29.1",
4345
"eslint-plugin-license-header": "^0.6.1",
4446
"eslint-plugin-n": "^16.6.2",
4547
"eslint-plugin-promise": "^6.1.1",
4648
"eslint-plugin-yml": "^1.14.0",
47-
"eslint": "^8.57.0",
4849
"globals": "^15.0.0",
4950
"json-diff-ts": "^4.0.1",
5051
"json-schema-to-typescript": "^14.0.4",
5152
"lodash": "^4.17.21",
5253
"qs": "^6.12.1",
5354
"smile-js": "^0.7.0",
55+
"tmp": "^0.2.3",
5456
"ts-jest": "^29.1.2",
5557
"ts-node": "^10.9.1",
5658
"typescript": "<5.4.0",

tools/src/helpers.ts

+40-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export function sort_by_keys (obj: Record<string, any>, priorities: string[] = [
4747
return a[0].localeCompare(b[0])
4848
})
4949
sorted.forEach(([k, v]) => {
50-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
5150
delete obj[k]
5251
obj[k] = v
5352
})
@@ -65,6 +64,46 @@ export function sort_array_by_keys (values: any[], priorities: string[] = []): s
6564
})
6665
}
6766

67+
export function delete_matching_keys(obj: any, condition: (obj: any) => boolean): void {
68+
for (const key in obj) {
69+
var item = obj[key]
70+
if (_.isObject(item)) {
71+
if (condition(item)) {
72+
delete obj[key]
73+
} else {
74+
delete_matching_keys(item, condition)
75+
}
76+
}
77+
}
78+
}
79+
80+
export function find_refs (current: Record<string, any>, root?: Record<string, any>, call_stack: string[] = []): Set<string> {
81+
var results = new Set<string>()
82+
83+
if (root === undefined) {
84+
root = current
85+
current = current.paths
86+
}
87+
88+
if (current?.$ref != null) {
89+
const ref = current.$ref as string
90+
results.add(ref)
91+
const ref_node = resolve_ref(ref, root)
92+
if (ref_node !== undefined && !call_stack.includes(ref)) {
93+
call_stack.push(ref)
94+
find_refs(ref_node, root, call_stack).forEach((ref) => results.add(ref))
95+
}
96+
}
97+
98+
if (_.isObject(current)) {
99+
_.forEach(current, (v) => {
100+
find_refs(v as Record<string, any>, root, call_stack).forEach((ref) => results.add(ref));
101+
})
102+
}
103+
104+
return results
105+
}
106+
68107
export function ensure_parent_dir (file_path: string): void {
69108
fs.mkdirSync(path.dirname(file_path), { recursive: true })
70109
}

tools/src/linter/SchemasValidator.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export default class SchemasValidator {
3232
}
3333

3434
validate (): ValidationError[] {
35-
this.spec = new OpenApiMerger(this.root_folder, new Logger(LogLevel.error)).merge().components as Record<string, any>
35+
const merger = new OpenApiMerger(this.root_folder, new Logger(LogLevel.error))
36+
this.spec = merger.spec().components as Record<string, any>
3637
const named_schemas_errors = this.validate_named_schemas()
3738
if (named_schemas_errors.length > 0) return named_schemas_errors
3839
return [

tools/src/merger/OpenApiMerger.ts

+37-29
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@ import { Logger } from '../Logger'
1818
// Create a single-file OpenAPI spec from multiple files for OpenAPI validation and programmatic consumption
1919
export default class OpenApiMerger {
2020
root_folder: string
21-
spec: Record<string, any>
2221
logger: Logger
2322

23+
protected _spec: Record<string, any>
24+
protected _merged: boolean = false
25+
2426
paths: Record<string, Record<string, OpenAPIV3.PathItemObject>> = {} // namespace -> path -> path_item_object
2527
schemas: Record<string, Record<string, OpenAPIV3.SchemaObject>> = {} // category -> schema -> schema_object
2628

2729
constructor (root_folder: string, logger: Logger = new Logger()) {
2830
this.logger = logger
2931
this.root_folder = fs.realpathSync(root_folder)
30-
this.spec = {
32+
this._spec = {
3133
openapi: '3.1.0',
3234
info: read_yaml(`${this.root_folder}/_info.yaml`, true),
3335
paths: {},
@@ -40,17 +42,23 @@ export default class OpenApiMerger {
4042
}
4143
}
4244

43-
merge (output_path: string = ''): OpenAPIV3.Document {
44-
this.#merge_schemas()
45-
this.#merge_namespaces()
46-
this.#sort_spec_keys()
47-
this.#generate_global_params()
48-
this.#generate_superseded_ops()
49-
45+
write_to(output_path: string): OpenApiMerger {
5046
this.logger.info(`Writing ${output_path} ...`)
47+
write_yaml(output_path, this.spec())
48+
return this
49+
}
50+
51+
spec(): OpenAPIV3.Document {
52+
if (!this._merged) {
53+
this.#merge_schemas()
54+
this.#merge_namespaces()
55+
this.#sort_spec_keys()
56+
this.#generate_global_params()
57+
this.#generate_superseded_ops()
58+
this._merged = true
59+
}
5160

52-
if (output_path !== '') write_yaml(output_path, this.spec)
53-
return this.spec as OpenAPIV3.Document
61+
return this._spec as OpenAPIV3.Document
5462
}
5563

5664
// Merge files from <spec_root>/namespaces folder.
@@ -59,21 +67,21 @@ export default class OpenApiMerger {
5967
fs.readdirSync(folder).forEach(file => {
6068
this.logger.info(`Merging namespaces in ${folder}/${file} ...`)
6169
const spec = read_yaml(`${folder}/${file}`)
62-
this.redirect_refs_in_namespace(spec)
63-
this.spec.paths = { ...this.spec.paths, ...spec.paths }
64-
this.spec.components.parameters = { ...this.spec.components.parameters, ...spec.components.parameters }
65-
this.spec.components.responses = { ...this.spec.components.responses, ...spec.components.responses }
66-
this.spec.components.requestBodies = { ...this.spec.components.requestBodies, ...spec.components.requestBodies }
70+
this.#redirect_refs_in_namespace(spec)
71+
this._spec.paths = { ...this._spec.paths, ...spec.paths }
72+
this._spec.components.parameters = { ...this._spec.components.parameters, ...spec.components.parameters }
73+
this._spec.components.responses = { ...this._spec.components.responses, ...spec.components.responses }
74+
this._spec.components.requestBodies = { ...this._spec.components.requestBodies, ...spec.components.requestBodies }
6775
})
6876
}
6977

7078
// Redirect schema references in namespace files to local references in single-file spec.
71-
redirect_refs_in_namespace (obj: any): void {
79+
#redirect_refs_in_namespace (obj: any): void {
7280
const ref: string = obj?.$ref
7381
if (ref?.startsWith('../schemas/')) { obj.$ref = ref.replace('../schemas/', '#/components/schemas/').replace('.yaml#/components/schemas/', ':') }
7482

7583
for (const key in obj) {
76-
if (typeof obj[key] === 'object') { this.redirect_refs_in_namespace(obj[key]) }
84+
if (typeof obj[key] === 'object') { this.#redirect_refs_in_namespace(obj[key]) }
7785
}
7886
}
7987

@@ -92,7 +100,7 @@ export default class OpenApiMerger {
92100

93101
Object.entries(this.schemas).forEach(([category, schemas]) => {
94102
Object.entries(schemas).forEach(([name, schema_obj]) => {
95-
this.spec.components.schemas[`${category}:${name}`] = schema_obj
103+
this._spec.components.schemas[`${category}:${name}`] = schema_obj
96104
})
97105
})
98106
}
@@ -115,26 +123,26 @@ export default class OpenApiMerger {
115123

116124
// Sort keys in the spec to make it easier to read and compare.
117125
#sort_spec_keys (): void {
118-
this.spec.components.schemas = _.fromPairs(Object.entries(this.spec.components.schemas as Document).sort((a, b) => a[0].localeCompare(b[0])))
119-
this.spec.components.parameters = _.fromPairs(Object.entries(this.spec.components.parameters as Document).sort((a, b) => a[0].localeCompare(b[0])))
120-
this.spec.components.responses = _.fromPairs(Object.entries(this.spec.components.responses as Document).sort((a, b) => a[0].localeCompare(b[0])))
121-
this.spec.components.requestBodies = _.fromPairs(Object.entries(this.spec.components.requestBodies as Document).sort((a, b) => a[0].localeCompare(b[0])))
122-
123-
this.spec.paths = _.fromPairs(Object.entries(this.spec.paths as Document).sort((a, b) => a[0].localeCompare(b[0])))
124-
Object.entries(this.spec.paths as Document).forEach(([path, path_item]) => {
125-
this.spec.paths[path] = _.fromPairs(Object.entries(path_item as Document).sort((a, b) => a[0].localeCompare(b[0])))
126+
this._spec.components.schemas = _.fromPairs(Object.entries(this._spec.components.schemas as Document).sort((a, b) => a[0].localeCompare(b[0])))
127+
this._spec.components.parameters = _.fromPairs(Object.entries(this._spec.components.parameters as Document).sort((a, b) => a[0].localeCompare(b[0])))
128+
this._spec.components.responses = _.fromPairs(Object.entries(this._spec.components.responses as Document).sort((a, b) => a[0].localeCompare(b[0])))
129+
this._spec.components.requestBodies = _.fromPairs(Object.entries(this._spec.components.requestBodies as Document).sort((a, b) => a[0].localeCompare(b[0])))
130+
131+
this._spec.paths = _.fromPairs(Object.entries(this._spec.paths as Document).sort((a, b) => a[0].localeCompare(b[0])))
132+
Object.entries(this._spec.paths as Document).forEach(([path, path_item]) => {
133+
this._spec.paths[path] = _.fromPairs(Object.entries(path_item as Document).sort((a, b) => a[0].localeCompare(b[0])))
126134
})
127135
}
128136

129137
// Generate global parameters from _global_params.yaml file.
130138
#generate_global_params (): void {
131139
const gen = new GlobalParamsGenerator(this.root_folder)
132-
gen.generate(this.spec)
140+
gen.generate(this._spec)
133141
}
134142

135143
// Generate superseded operations from _superseded_operations.yaml file.
136144
#generate_superseded_ops (): void {
137145
const gen = new SupersededOpsGenerator(this.root_folder, this.logger)
138-
gen.generate(this.spec)
146+
gen.generate(this._spec)
139147
}
140148
}

0 commit comments

Comments
 (0)