diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ceb549a69..a325840b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: run: |- mkdir -p ../build npm install - export ROOT_PATH=../spec/opensearch-openapi.yaml + export ROOT_PATH=../spec export OUTPUT_PATH=../build/opensearch-openapi.yaml npm run merge -- $ROOT_PATH $OUTPUT_PATH diff --git a/.github/workflows/coverage-gather.yml b/.github/workflows/coverage-gather.yml index 703c0c6ab..d3b378b90 100644 --- a/.github/workflows/coverage-gather.yml +++ b/.github/workflows/coverage-gather.yml @@ -19,7 +19,7 @@ jobs: run: |- mkdir -p ../build npm install - export ROOT_PATH=../spec/opensearch-openapi.yaml + export ROOT_PATH=../spec export OUTPUT_PATH=../build/opensearch-openapi.yaml npm run merge -- $ROOT_PATH $OUTPUT_PATH - name: Build and Run Docker Container diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 37274d43e..145d9c2e1 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -41,14 +41,15 @@ spec │ ├── cat.aliases.yaml │ └── ... │ -└── opensearch-openapi.yaml +├── _info.yaml +├── _global_parameters.yaml +└── _superseded_operations.yaml ``` - The API Operations are grouped by namespaces in [spec/namespaces/](spec/namespaces) directory. Each file in this directory represents a namespace and holds all paths and operations of the namespace. - The data schemas are grouped by categories in [spec/schemas/](spec/schemas) directory. Each file in this directory represents a category. -- The [spec/opensearch-openapi.yaml](spec/opensearch-openapi.yaml) file is the OpenAPI root file that ties everything together. -Every `.yaml` file is a OpenAPI 3 document. This means that you can use any OpenAPI 3 compatible tool to view and edit the files, and IDEs with OpenAPI support will also offer autocomplete and validation in realtime. +Every `.yaml` file in the namespaces and schemas folders is a OpenAPI 3 document. This means that you can use any OpenAPI 3 compatible tool to view and edit the files, and IDEs with OpenAPI support will also offer autocomplete and validation in realtime. ## Grouping Operations @@ -100,7 +101,7 @@ if and only if the superseding operations exist in the spec. A warning will be p Note that the path parameter names do not need to match. So, if the actual superseding operations have path of `/_plugins/_anomaly_detection/{node_id}/stats/{stat_id}`, the merger tool will recognize that it is the same as `/_plugins/_anomaly_detection/{nodeId}/stats/{stat}` and generate the superseded operations accordingly with the correct path parameter names. ## Global Parameters -Certain query parameters are global, and they are accepted by every operation. These parameters are listed in the [root file](spec/opensearch-openapi.yaml) under the `parameters` section with `x-global` set to true. The merger tool will automatically add these parameters to all operations. +Certain query parameters are global, and they are accepted by every operation. These parameters are listed in the [spec/_global_parameters.yaml](spec/_global_parameters.yaml). The merger tool will automatically add these parameters to all operations. ## OpenAPI Extensions @@ -112,7 +113,7 @@ This repository includes several OpenAPI Specification Extensions to fill in any - `x-version-removed`: OpenSearch version when the operation/parameter was removed. - `x-deprecation-message`: Reason for deprecation and guidance on how to prepare for the next major version. - `x-ignorable`: Denotes that the operation should be ignored by the client generator. This is used in operation groups where some operations have been replaced by newer ones, but we still keep them in the specs because the server still supports them. -- `x-global`: Denotes that the parameter is a global parameter that is included in every operation. These parameters are listed in the [root file](spec/opensearch-openapi.yaml). +- `x-global`: Denotes that the parameter is a global parameter that is included in every operation. These parameters are listed in the [spec/_global_parameters.yaml](spec/_global_parameters.yaml). - `x-default`: Contains the default value of a parameter. This is often used to override the default value specified in the schema, or to avoid accidentally changing the default value when updating a shared schema. ## Tools diff --git a/_plugins/openapi.rb b/_plugins/openapi.rb index 82bae554f..23a612b27 100644 --- a/_plugins/openapi.rb +++ b/_plugins/openapi.rb @@ -4,7 +4,7 @@ def self.generate(_site, _payload) Dir.chdir('tools') do system 'npm install' - system 'npm run merge -- ../spec/opensearch-openapi.yaml ../_site/opensearch-openapi.yaml' + system 'npm run merge -- ../spec ../_site/opensearch-openapi.yaml' end @generated = true diff --git a/spec/_global_parameters.yaml b/spec/_global_parameters.yaml new file mode 100644 index 000000000..9a2cd00e9 --- /dev/null +++ b/spec/_global_parameters.yaml @@ -0,0 +1,43 @@ +openapi: 3.1.0 +info: + title: '' + version: '' +components: + parameters: + _global::query.pretty: + name: pretty + in: query + description: Whether to pretty format the returned JSON response. + schema: + type: boolean + default: false + _global::query.human: + name: human + in: query + description: Whether to return human readable values for statistics. + schema: + type: boolean + default: true + _global::query.error_trace: + name: error_trace + in: query + description: Whether to include the stack trace of returned errors. + schema: + type: boolean + default: false + _global::query.source: + name: source + in: query + description: The URL-encoded request definition. Useful for libraries that do not accept a request body for non-POST requests. + schema: + type: string + _global::query.filter_path: + name: filter_path + in: query + description: Comma-separated list of filters used to reduce the response. + schema: + oneOf: + - type: string + - type: array + items: + type: string \ No newline at end of file diff --git a/spec/_info.yaml b/spec/_info.yaml new file mode 100644 index 000000000..9da26bfa0 --- /dev/null +++ b/spec/_info.yaml @@ -0,0 +1,3 @@ +title: OpenSearch API +description: OpenSearch API +version: 1.0.0 \ No newline at end of file diff --git a/spec/opensearch-openapi.yaml b/spec/opensearch-openapi.yaml deleted file mode 100644 index ce83c5b45..000000000 --- a/spec/opensearch-openapi.yaml +++ /dev/null @@ -1,584 +0,0 @@ -openapi: 3.1.0 -info: - title: OpenSearch API - description: OpenSearch API - version: 1.0.0 -paths: - /: - $ref: 'namespaces/_core.yaml#/paths/~1' - /_bulk: - $ref: 'namespaces/_core.yaml#/paths/~1_bulk' - /_count: - $ref: 'namespaces/_core.yaml#/paths/~1_count' - /_delete_by_query/{task_id}/_rethrottle: - $ref: 'namespaces/_core.yaml#/paths/~1_delete_by_query~1{task_id}~1_rethrottle' - /_field_caps: - $ref: 'namespaces/_core.yaml#/paths/~1_field_caps' - /_mget: - $ref: 'namespaces/_core.yaml#/paths/~1_mget' - /_msearch: - $ref: 'namespaces/_core.yaml#/paths/~1_msearch' - /_msearch/template: - $ref: 'namespaces/_core.yaml#/paths/~1_msearch~1template' - /_mtermvectors: - $ref: 'namespaces/_core.yaml#/paths/~1_mtermvectors' - /_rank_eval: - $ref: 'namespaces/_core.yaml#/paths/~1_rank_eval' - /_reindex: - $ref: 'namespaces/_core.yaml#/paths/~1_reindex' - /_reindex/{task_id}/_rethrottle: - $ref: 'namespaces/_core.yaml#/paths/~1_reindex~1{task_id}~1_rethrottle' - /_render/template: - $ref: 'namespaces/_core.yaml#/paths/~1_render~1template' - /_render/template/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1_render~1template~1{id}' - /_script_context: - $ref: 'namespaces/_core.yaml#/paths/~1_script_context' - /_script_language: - $ref: 'namespaces/_core.yaml#/paths/~1_script_language' - /_scripts/painless/_execute: - $ref: 'namespaces/_core.yaml#/paths/~1_scripts~1painless~1_execute' - /_scripts/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1_scripts~1{id}' - /_scripts/{id}/{context}: - $ref: 'namespaces/_core.yaml#/paths/~1_scripts~1{id}~1{context}' - /_search: - $ref: 'namespaces/_core.yaml#/paths/~1_search' - /_search/point_in_time: - $ref: 'namespaces/_core.yaml#/paths/~1_search~1point_in_time' - /_search/point_in_time/_all: - $ref: 'namespaces/_core.yaml#/paths/~1_search~1point_in_time~1_all' - /_search/scroll: - $ref: 'namespaces/_core.yaml#/paths/~1_search~1scroll' - /_search/scroll/{scroll_id}: - $ref: 'namespaces/_core.yaml#/paths/~1_search~1scroll~1{scroll_id}' - /_search/template: - $ref: 'namespaces/_core.yaml#/paths/~1_search~1template' - /_search_shards: - $ref: 'namespaces/_core.yaml#/paths/~1_search_shards' - /_update_by_query/{task_id}/_rethrottle: - $ref: 'namespaces/_core.yaml#/paths/~1_update_by_query~1{task_id}~1_rethrottle' - /{index}/_bulk: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_bulk' - /{index}/_count: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_count' - /{index}/_create/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_create~1{id}' - /{index}/_delete_by_query: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_delete_by_query' - /{index}/_doc: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_doc' - /{index}/_doc/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_doc~1{id}' - /{index}/_explain/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_explain~1{id}' - /{index}/_field_caps: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_field_caps' - /{index}/_mget: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_mget' - /{index}/_msearch: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_msearch' - /{index}/_msearch/template: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_msearch~1template' - /{index}/_mtermvectors: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_mtermvectors' - /{index}/_rank_eval: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_rank_eval' - /{index}/_search: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_search' - /{index}/_search/point_in_time: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_search~1point_in_time' - /{index}/_search/template: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_search~1template' - /{index}/_search_shards: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_search_shards' - /{index}/_source/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_source~1{id}' - /{index}/_termvectors: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_termvectors' - /{index}/_termvectors/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_termvectors~1{id}' - /{index}/_update/{id}: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_update~1{id}' - /{index}/_update_by_query: - $ref: 'namespaces/_core.yaml#/paths/~1{index}~1_update_by_query' - /_alias: - $ref: 'namespaces/indices.yaml#/paths/~1_alias' - /_alias/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_alias~1{name}' - /_aliases: - $ref: 'namespaces/indices.yaml#/paths/~1_aliases' - /_analyze: - $ref: 'namespaces/indices.yaml#/paths/~1_analyze' - /_cache/clear: - $ref: 'namespaces/indices.yaml#/paths/~1_cache~1clear' - /_data_stream: - $ref: 'namespaces/indices.yaml#/paths/~1_data_stream' - /_data_stream/_stats: - $ref: 'namespaces/indices.yaml#/paths/~1_data_stream~1_stats' - /_data_stream/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_data_stream~1{name}' - /_data_stream/{name}/_stats: - $ref: 'namespaces/indices.yaml#/paths/~1_data_stream~1{name}~1_stats' - /_flush: - $ref: 'namespaces/indices.yaml#/paths/~1_flush' - /_forcemerge: - $ref: 'namespaces/indices.yaml#/paths/~1_forcemerge' - /_index_template: - $ref: 'namespaces/indices.yaml#/paths/~1_index_template' - /_index_template/_simulate: - $ref: 'namespaces/indices.yaml#/paths/~1_index_template~1_simulate' - /_index_template/_simulate/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_index_template~1_simulate~1{name}' - /_index_template/_simulate_index/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_index_template~1_simulate_index~1{name}' - /_index_template/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_index_template~1{name}' - /_mapping: - $ref: 'namespaces/indices.yaml#/paths/~1_mapping' - /_mapping/field/{fields}: - $ref: 'namespaces/indices.yaml#/paths/~1_mapping~1field~1{fields}' - /_recovery: - $ref: 'namespaces/indices.yaml#/paths/~1_recovery' - /_refresh: - $ref: 'namespaces/indices.yaml#/paths/~1_refresh' - /_resolve/index/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_resolve~1index~1{name}' - /_segments: - $ref: 'namespaces/indices.yaml#/paths/~1_segments' - /_settings: - $ref: 'namespaces/indices.yaml#/paths/~1_settings' - /_settings/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_settings~1{name}' - /_shard_stores: - $ref: 'namespaces/indices.yaml#/paths/~1_shard_stores' - /_stats: - $ref: 'namespaces/indices.yaml#/paths/~1_stats' - /_stats/{metric}: - $ref: 'namespaces/indices.yaml#/paths/~1_stats~1{metric}' - /_template: - $ref: 'namespaces/indices.yaml#/paths/~1_template' - /_template/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1_template~1{name}' - /_upgrade: - $ref: 'namespaces/indices.yaml#/paths/~1_upgrade' - /_validate/query: - $ref: 'namespaces/indices.yaml#/paths/~1_validate~1query' - /{alias}/_rollover: - $ref: 'namespaces/indices.yaml#/paths/~1{alias}~1_rollover' - /{alias}/_rollover/{new_index}: - $ref: 'namespaces/indices.yaml#/paths/~1{alias}~1_rollover~1{new_index}' - /{index}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}' - /{index}/_alias: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_alias' - /{index}/_alias/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_alias~1{name}' - /{index}/_aliases/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_aliases~1{name}' - /{index}/_analyze: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_analyze' - /{index}/_block/{block}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_block~1{block}' - /{index}/_cache/clear: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_cache~1clear' - /{index}/_clone/{target}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_clone~1{target}' - /{index}/_close: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_close' - /{index}/_flush: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_flush' - /{index}/_forcemerge: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_forcemerge' - /{index}/_mapping: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_mapping' - /{index}/_mapping/field/{fields}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_mapping~1field~1{fields}' - /{index}/_open: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_open' - /{index}/_recovery: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_recovery' - /{index}/_refresh: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_refresh' - /{index}/_segments: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_segments' - /{index}/_settings: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_settings' - /{index}/_settings/{name}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_settings~1{name}' - /{index}/_shard_stores: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_shard_stores' - /{index}/_shrink/{target}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_shrink~1{target}' - /{index}/_split/{target}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_split~1{target}' - /{index}/_stats: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_stats' - /{index}/_stats/{metric}: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_stats~1{metric}' - /{index}/_upgrade: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_upgrade' - /{index}/_validate/query: - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_validate~1query' - /_cat: - $ref: 'namespaces/cat.yaml#/paths/~1_cat' - /_cat/aliases: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1aliases' - /_cat/aliases/{name}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1aliases~1{name}' - /_cat/allocation: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1allocation' - /_cat/allocation/{node_id}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1allocation~1{node_id}' - /_cat/cluster_manager: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1cluster_manager' - /_cat/count: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1count' - /_cat/count/{index}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1count~1{index}' - /_cat/fielddata: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1fielddata' - /_cat/fielddata/{fields}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1fielddata~1{fields}' - /_cat/health: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1health' - /_cat/indices: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1indices' - /_cat/indices/{index}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1indices~1{index}' - /_cat/master: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1master' - /_cat/nodeattrs: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1nodeattrs' - /_cat/nodes: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1nodes' - /_cat/pending_tasks: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1pending_tasks' - /_cat/pit_segments: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1pit_segments' - /_cat/pit_segments/_all: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1pit_segments~1_all' - /_cat/plugins: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1plugins' - /_cat/recovery: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1recovery' - /_cat/recovery/{index}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1recovery~1{index}' - /_cat/repositories: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1repositories' - /_cat/segment_replication: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1segment_replication' - /_cat/segment_replication/{index}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1segment_replication~1{index}' - /_cat/segments: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1segments' - /_cat/segments/{index}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1segments~1{index}' - /_cat/shards: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1shards' - /_cat/shards/{index}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1shards~1{index}' - /_cat/snapshots: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1snapshots' - /_cat/snapshots/{repository}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1snapshots~1{repository}' - /_cat/tasks: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1tasks' - /_cat/templates: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1templates' - /_cat/templates/{name}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1templates~1{name}' - /_cat/thread_pool: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1thread_pool' - /_cat/thread_pool/{thread_pool_patterns}: - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1thread_pool~1{thread_pool_patterns}' - /_cluster/allocation/explain: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1allocation~1explain' - /_cluster/decommission/awareness: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1decommission~1awareness' - /_cluster/decommission/awareness/{awareness_attribute_name}/_status: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1decommission~1awareness~1{awareness_attribute_name}~1_status' - /_cluster/decommission/awareness/{awareness_attribute_name}/{awareness_attribute_value}: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1decommission~1awareness~1{awareness_attribute_name}~1{awareness_attribute_value}' - /_cluster/health: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1health' - /_cluster/health/{index}: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1health~1{index}' - /_cluster/pending_tasks: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1pending_tasks' - /_cluster/reroute: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1reroute' - /_cluster/routing/awareness/weights: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1routing~1awareness~1weights' - /_cluster/routing/awareness/{attribute}/weights: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1routing~1awareness~1{attribute}~1weights' - /_cluster/settings: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1settings' - /_cluster/state: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1state' - /_cluster/state/{metric}: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1state~1{metric}' - /_cluster/state/{metric}/{index}: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1state~1{metric}~1{index}' - /_cluster/stats: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1stats' - /_cluster/stats/nodes/{node_id}: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1stats~1nodes~1{node_id}' - /_cluster/voting_config_exclusions: - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1voting_config_exclusions' - /_component_template: - $ref: 'namespaces/cluster.yaml#/paths/~1_component_template' - /_component_template/{name}: - $ref: 'namespaces/cluster.yaml#/paths/~1_component_template~1{name}' - /_remote/info: - $ref: 'namespaces/cluster.yaml#/paths/~1_remote~1info' - /_cluster/nodes/hot_threads: - $ref: 'namespaces/nodes.yaml#/paths/~1_cluster~1nodes~1hot_threads' - /_cluster/nodes/hotthreads: - $ref: 'namespaces/nodes.yaml#/paths/~1_cluster~1nodes~1hotthreads' - /_cluster/nodes/{node_id}/hot_threads: - $ref: 'namespaces/nodes.yaml#/paths/~1_cluster~1nodes~1{node_id}~1hot_threads' - /_cluster/nodes/{node_id}/hotthreads: - $ref: 'namespaces/nodes.yaml#/paths/~1_cluster~1nodes~1{node_id}~1hotthreads' - /_nodes: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes' - /_nodes/hot_threads: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1hot_threads' - /_nodes/hotthreads: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1hotthreads' - /_nodes/reload_secure_settings: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1reload_secure_settings' - /_nodes/stats: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1stats' - /_nodes/stats/{metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1stats~1{metric}' - /_nodes/stats/{metric}/{index_metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1stats~1{metric}~1{index_metric}' - /_nodes/usage: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1usage' - /_nodes/usage/{metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1usage~1{metric}' - /_nodes/{metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{metric}' - /_nodes/{node_id}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}' - /_nodes/{node_id}/hot_threads: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1hot_threads' - /_nodes/{node_id}/hotthreads: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1hotthreads' - /_nodes/{node_id}/reload_secure_settings: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1reload_secure_settings' - /_nodes/{node_id}/stats: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1stats' - /_nodes/{node_id}/stats/{metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1stats~1{metric}' - /_nodes/{node_id}/stats/{metric}/{index_metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1stats~1{metric}~1{index_metric}' - /_nodes/{node_id}/usage: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1usage' - /_nodes/{node_id}/usage/{metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1usage~1{metric}' - /_nodes/{node_id}/{metric}: - $ref: 'namespaces/nodes.yaml#/paths/~1_nodes~1{node_id}~1{metric}' - /_dangling: - $ref: 'namespaces/dangling_indices.yaml#/paths/~1_dangling' - /_dangling/{index_uuid}: - $ref: 'namespaces/dangling_indices.yaml#/paths/~1_dangling~1{index_uuid}' - /_ingest/pipeline: - $ref: 'namespaces/ingest.yaml#/paths/~1_ingest~1pipeline' - /_ingest/pipeline/_simulate: - $ref: 'namespaces/ingest.yaml#/paths/~1_ingest~1pipeline~1_simulate' - /_ingest/pipeline/{id}: - $ref: 'namespaces/ingest.yaml#/paths/~1_ingest~1pipeline~1{id}' - /_ingest/pipeline/{id}/_simulate: - $ref: 'namespaces/ingest.yaml#/paths/~1_ingest~1pipeline~1{id}~1_simulate' - /_ingest/processor/grok: - $ref: 'namespaces/ingest.yaml#/paths/~1_ingest~1processor~1grok' - /_plugins/_knn/models/_search: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1models~1_search' - /_plugins/_knn/models/_train: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1models~1_train' - /_plugins/_knn/models/{model_id}: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1models~1{model_id}' - /_plugins/_knn/models/{model_id}/_train: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1models~1{model_id}~1_train' - /_plugins/_knn/stats: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1stats' - /_plugins/_knn/stats/{stat}: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1stats~1{stat}' - /_plugins/_knn/warmup/{index}: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1warmup~1{index}' - /_plugins/_knn/{node_id}/stats: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1{node_id}~1stats' - /_plugins/_knn/{node_id}/stats/{stat}: - $ref: 'namespaces/knn.yaml#/paths/~1_plugins~1_knn~1{node_id}~1stats~1{stat}' - /_plugins/_notifications/channels: - $ref: 'namespaces/notifications.yaml#/paths/~1_plugins~1_notifications~1channels' - /_plugins/_notifications/configs: - $ref: 'namespaces/notifications.yaml#/paths/~1_plugins~1_notifications~1configs' - /_plugins/_notifications/configs/{config_id}: - $ref: 'namespaces/notifications.yaml#/paths/~1_plugins~1_notifications~1configs~1{config_id}' - /_plugins/_notifications/feature/test/{config_id}: - $ref: 'namespaces/notifications.yaml#/paths/~1_plugins~1_notifications~1feature~1test~1{config_id}' - /_plugins/_notifications/features: - $ref: 'namespaces/notifications.yaml#/paths/~1_plugins~1_notifications~1features' - /_opendistro/_security/sslinfo: - $ref: 'namespaces/security.yaml#/paths/~_opendistro~1_security~1sslinfo' - /_plugins/_security/authinfo: - $ref: 'namespaces/security.yaml#/paths/~_plugins~1_security~1authinfo' - /_plugins/_security/dashboardsinfo: - $ref: 'namespaces/security.yaml#/paths/~_plugins~1_security~1dashboardsinfo' - /_plugins/_security/health: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1health' - /_plugins/_security/tenantinfo: - $ref: 'namespaces/security.yaml#/paths/~_plugins~1_security~1tenantinfo' - /_plugins/_security/_upgrade_check: - $ref: 'namespaces/security.yaml#/paths/~_plugins~1_security~1_upgrade_check' - /_plugins/_security/_upgrade_perform: - $ref: 'namespaces/security.yaml#/paths/~_plugins~1_security~1_upgrade_perform' - /_plugins/_security/whoami: - $ref: 'namespaces/security.yaml#/paths/~_plugins~1_security~1whoami' - /_plugins/_security/whoamiprotected: - $ref: 'namespaces/security.yaml#/paths/~_plugins~1_security~1whoamiprotected' - /_plugins/_security/api/account: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1account' - /_plugins/_security/api/actiongroups: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1actiongroups' - /_plugins/_security/api/actiongroups/{action_group}: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1actiongroups~1{action_group}' - /_plugins/_security/api/allowlist: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1allowlist' - /_plugins/_security/api/audit: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1audit' - /_plugins/_security/api/audit/config: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1audit~1config' - /_plugins/_security/api/authtoken: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1authtoken' - /_plugins/_security/api/cache: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1cache' - /_plugins/_security/api/generateonbehalfoftoken: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1generateonbehalfoftoken' - /_plugins/_security/api/internalusers: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1internalusers' - /_plugins/_security/api/internalusers/{username}: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1internalusers~1{username}' - /_plugins/_security/api/internalusers/{username}/authtoken: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1internalusers~1{username}~1authtoken' - /_plugins/_security/api/migrate: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1migrate' - /_plugins/_security/api/nodesdn: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1nodesdn' - /_plugins/_security/api/nodesdn/{cluster_name}: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1nodesdn~1{cluster_name}' - /_plugins/_security/api/permissionsinfo: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1permissionsinfo' - /_plugins/_security/api/roles: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1roles' - /_plugins/_security/api/roles/{role}: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1roles~1{role}' - /_plugins/_security/api/rolesmapping: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1rolesmapping' - /_plugins/_security/api/rolesmapping/{role}: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1rolesmapping~1{role}' - /_plugins/_security/api/securityconfig: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1securityconfig' - /_plugins/_security/api/securityconfig/config: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1securityconfig~1config' - /_plugins/_security/api/ssl/certs: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1ssl~1certs' - /_plugins/_security/api/ssl/http/reloadcerts: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1ssl~1http~1reloadcerts' - /_plugins/_security/api/ssl/transport/reloadcerts: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1ssl~1transport~1reloadcerts' - /_plugins/_security/api/tenancy/config: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1tenancy~1config' - /_plugins/_security/api/tenants: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1tenants' - /_plugins/_security/api/tenants/{tenant}: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1tenants~1{tenant}' - /_plugins/_security/api/user: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1internalusers' - /_plugins/_security/api/user/{username}: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1internalusers~1{username}' - /_plugins/_security/api/user/{username}/authtoken: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1internalusers~1{username}~1authtoken' - /_plugins/_security/api/validate: - $ref: 'namespaces/security.yaml#/paths/~1_plugins~1_security~1api~1validate' - /_remotestore/_restore: - $ref: 'namespaces/remote_store.yaml#/paths/~1_remotestore~1_restore' - /_search/pipeline: - $ref: 'namespaces/search_pipeline.yaml#/paths/~1_search~1pipeline' - /_search/pipeline/{id}: - $ref: 'namespaces/search_pipeline.yaml#/paths/~1_search~1pipeline~1{id}' - /_snapshot: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot' - /_snapshot/_status: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1_status' - /_snapshot/{repository}: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}' - /_snapshot/{repository}/_cleanup: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}~1_cleanup' - /_snapshot/{repository}/_status: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}~1_status' - /_snapshot/{repository}/_verify: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}~1_verify' - /_snapshot/{repository}/{snapshot}: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}~1{snapshot}' - /_snapshot/{repository}/{snapshot}/_clone/{target_snapshot}: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}~1{snapshot}~1_clone~1{target_snapshot}' - /_snapshot/{repository}/{snapshot}/_restore: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}~1{snapshot}~1_restore' - /_snapshot/{repository}/{snapshot}/_status: - $ref: 'namespaces/snapshot.yaml#/paths/~1_snapshot~1{repository}~1{snapshot}~1_status' - /_tasks: - $ref: 'namespaces/tasks.yaml#/paths/~1_tasks' - /_tasks/_cancel: - $ref: 'namespaces/tasks.yaml#/paths/~1_tasks~1_cancel' - /_tasks/{task_id}: - $ref: 'namespaces/tasks.yaml#/paths/~1_tasks~1{task_id}' - /_tasks/{task_id}/_cancel: - $ref: 'namespaces/tasks.yaml#/paths/~1_tasks~1{task_id}~1_cancel' -components: - parameters: - _global::query.pretty: - x-global: true - name: pretty - in: query - description: Whether to pretty format the returned JSON response. - schema: - type: boolean - default: false - _global::query.human: - x-global: true - name: human - in: query - description: Whether to return human readable values for statistics. - schema: - type: boolean - default: true - _global::query.error_trace: - x-global: true - name: error_trace - in: query - description: Whether to include the stack trace of returned errors. - schema: - type: boolean - default: false - _global::query.source: - x-global: true - name: source - in: query - description: The URL-encoded request definition. Useful for libraries that do not accept a request body for non-POST requests. - schema: - type: string - _global::query.filter_path: - x-global: true - name: filter_path - in: query - description: Comma-separated list of filters used to reduce the response. - schema: - oneOf: - - type: string - - type: array - items: - type: string diff --git a/tools/README.md b/tools/README.md index 008449bd8..a9bffabab 100644 --- a/tools/README.md +++ b/tools/README.md @@ -21,13 +21,13 @@ Example: ```bash mkdir -p ../build -export ROOT_PATH=../spec/opensearch-openapi.yaml +export ROOT_PATH=../spec export OUTPUT_PATH=../build/opensearch-openapi.yaml npm run merge -- $ROOT_PATH $OUTPUT_PATH ``` As a shortcut, if those parameters are not provided, the tool will use the default values: -- `../spec/opensearch-openapi.yaml` as the root path (i.e. the root file of the repo's [spec folder](../spec)) +- `../spec` as the root path (i.e. the repo's [spec folder](../spec)) - `../opensearch-openapi.yaml` as the output path ```bash diff --git a/tools/eslint.config.mjs b/tools/eslint.config.mjs index a5c9d3fd8..1a6740241 100644 --- a/tools/eslint.config.mjs +++ b/tools/eslint.config.mjs @@ -13,26 +13,45 @@ export default [ ...compat.extends('standard-with-typescript'), { files: ['**/*.{js,ts}'], - // to auto-fix disable all rules except the one you want to fix with '@rule': 'warn', then run `npm run lint -- --fix` rules: { - '@typescript-eslint/consistent-indexed-object-style': 'warn', - '@typescript-eslint/consistent-type-assertions': 'warn', - '@typescript-eslint/dot-notation': 'warn', - '@typescript-eslint/explicit-function-return-type': 'warn', - '@typescript-eslint/naming-convention': 'warn', - '@typescript-eslint/no-confusing-void-expression': 'warn', - '@typescript-eslint/no-dynamic-delete': 'warn', - '@typescript-eslint/no-invalid-void-type': 'warn', - '@typescript-eslint/no-non-null-assertion': 'warn', - '@typescript-eslint/no-unnecessary-type-assertion': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', - '@typescript-eslint/prefer-nullish-coalescing': 'warn', - '@typescript-eslint/require-array-sort-compare': 'warn', - '@typescript-eslint/strict-boolean-expressions': 'warn', - 'array-callback-return': 'warn', - 'new-cap': 'warn', - 'no-return-assign': 'warn', - 'object-shorthand': 'warn' + '@typescript-eslint/consistent-indexed-object-style': 'error', + '@typescript-eslint/consistent-type-assertions': 'error', + '@typescript-eslint/dot-notation': 'error', + '@typescript-eslint/explicit-function-return-type': 'error', + '@typescript-eslint/naming-convention': ['error', + { selector: 'classProperty', modifiers: ['readonly'], format: ['UPPER_CASE'], leadingUnderscore: 'allow' }, + { selector: 'memberLike', modifiers: ['public'], format: ['snake_case'], leadingUnderscore: 'forbid' }, + { selector: 'memberLike', modifiers: ['private', 'protected'], format: ['snake_case'], leadingUnderscore: 'require' }, + { selector: 'variableLike', format: ['snake_case', 'UPPER_CASE'], leadingUnderscore: 'allow' }, + { selector: 'typeLike', format: ['PascalCase'] }, + { selector: 'objectLiteralProperty', format: null }, + { selector: 'typeProperty', format: null } + ], + '@typescript-eslint/no-confusing-void-expression': 'error', + '@typescript-eslint/no-dynamic-delete': 'error', + '@typescript-eslint/no-invalid-void-type': 'error', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/prefer-nullish-coalescing': 'error', + '@typescript-eslint/require-array-sort-compare': 'error', + '@typescript-eslint/strict-boolean-expressions': ['error', + { + allowString: true, + allowNumber: true, + allowNullableObject: true, + allowNullableBoolean: true, + allowNullableString: false, + allowNullableNumber: false, + allowNullableEnum: false, + allowAny: false, + allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false + } + ], + 'array-callback-return': 'off', + 'new-cap': 'off', + 'no-return-assign': 'error', + 'object-shorthand': 'error' } } ] diff --git a/tools/helpers.ts b/tools/helpers.ts index dab79201b..09e6fcb6f 100644 --- a/tools/helpers.ts +++ b/tools/helpers.ts @@ -2,7 +2,7 @@ import fs from 'fs' import YAML from 'yaml' import _ from 'lodash' -export function resolveRef (ref: string, root: Record): Record | undefined { +export function resolve_ref (ref: string, root: Record): Record | undefined { const paths = ref.replace('#/', '').split('/') for (const p of paths) { root = root[p] @@ -11,42 +11,47 @@ export function resolveRef (ref: string, root: Record): Record | undefined, root: Record) { +export function resolve_obj (obj: Record | undefined, root: Record): Record | undefined { if (obj === undefined) return undefined - if (obj.$ref) return resolveRef(obj.$ref, root) + if (obj.$ref !== null) return resolve_ref(obj.$ref as string, root) return obj } export function dig (obj: Record, path: string[], root: Record): any { let value = obj for (const p of path) { - value = resolveObj(value, root)?.[p] + value = resolve_obj(value, root)?.[p] if (value === undefined) break } return value } -export function sortByKey (obj: Record, priorities: string[] = []) { +export function sort_by_keys (obj: Record, priorities: string[] = []): void { const orders = _.fromPairs(priorities.map((k, i) => [k, i + 1])) const sorted = _.entries(obj).sort((a, b) => { const order_a = orders[a[0]] const order_b = orders[b[0]] - if (order_a && order_b) return order_a - order_b - if (order_a) return 1 - if (order_b) return -1 + if (order_a != null && order_b != null) return order_a - order_b + if (order_a != null) return 1 + if (order_b != null) return -1 return a[0].localeCompare(b[0]) }) sorted.forEach(([k, v]) => { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete obj[k] obj[k] = v }) } -export function write2file (file_path: string, content: Record): void { - fs.writeFileSync(file_path, quoteRefs(YAML.stringify(removeAnchors(content), { lineWidth: 0, singleQuote: true }))) +export function read_yaml (file_path: string): Record { + return YAML.parse(fs.readFileSync(file_path, 'utf8')) } -function quoteRefs (str: string): string { +export function write_yaml (file_path: string, content: Record): void { + fs.writeFileSync(file_path, quote_refs(YAML.stringify(remove_anchors(content), { lineWidth: 0, singleQuote: true }))) +} + +function quote_refs (str: string): string { return str.split('\n').map((line) => { if (line.includes('$ref')) { const [key, value] = line.split(': ') @@ -56,7 +61,7 @@ function quoteRefs (str: string): string { }).join('\n') } -function removeAnchors (content: Record): Record { - const replacer = (key: string, value: any) => key === '$anchor' ? undefined : value +function remove_anchors (content: Record): Record { + const replacer = (key: string, value: any): any => key === '$anchor' ? undefined : value return JSON.parse(JSON.stringify(content, replacer)) } diff --git a/tools/linter/PathRefsValidator.ts b/tools/linter/PathRefsValidator.ts deleted file mode 100644 index e8d464b40..000000000 --- a/tools/linter/PathRefsValidator.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { type ValidationError } from '../types' -import type RootFile from './components/RootFile' -import type NamespacesFolder from './components/NamespacesFolder' - -export default class PathRefsValidator { - root_file: RootFile - namespaces_folder: NamespacesFolder - - referenced_paths: Record> = {} // file -> paths - available_paths: Record> = {} // file -> paths - - constructor (root_file: RootFile, namespaces_folder: NamespacesFolder) { - this.root_file = root_file - this.namespaces_folder = namespaces_folder - this.#build_referenced_paths() - this.#build_available_paths() - } - - #build_referenced_paths () { - for (const [path, spec] of Object.entries(this.root_file.spec().paths)) { - const ref = spec!.$ref! - const file = ref.split('#')[0] - if (!this.referenced_paths[file]) this.referenced_paths[file] = new Set() - this.referenced_paths[file].add(path) - } - } - - #build_available_paths () { - for (const file of this.namespaces_folder.files) { - this.available_paths[file.file] = new Set(Object.keys(file.spec().paths || {})) - } - } - - validate (): ValidationError[] { - return [ - ...this.validate_unresolved_refs(), - ...this.validate_unreferenced_paths() - ] - } - - validate_unresolved_refs (): ValidationError[] { - return Object.entries(this.referenced_paths).flatMap(([ref_file, ref_paths]) => { - const available = this.available_paths[ref_file] - if (!available) { - return { - file: this.root_file.file, - location: `Paths: ${[...ref_paths].join(' , ')}`, - message: `Unresolved path reference: Namespace file ${ref_file} does not exist.` - } - } - - return Array.from(ref_paths).map((path) => { - if (!available.has(path)) { - return { - file: this.root_file.file, - location: `Path: ${path}`, - message: `Unresolved path reference: Path ${path} does not exist in namespace file ${ref_file}.` - } - } - }).filter((e) => e) as ValidationError[] - }) - } - - validate_unreferenced_paths (): ValidationError[] { - return Object.entries(this.available_paths).flatMap(([ns_file, ns_paths]) => { - const referenced = this.referenced_paths[ns_file] - if (!referenced) { - return { - file: ns_file, - message: 'Unreferenced paths: No paths are referenced in the root file.' - } - } - return Array.from(ns_paths).map((path) => { - if (!referenced || !referenced.has(path)) { - return { - file: ns_file, - location: `Path: ${path}`, - message: `Unreferenced path: Path ${path} is not referenced in the root file.` - } - } - }).filter((e) => e) as ValidationError[] - }) - } -} diff --git a/tools/linter/SchemaRefsValidator.ts b/tools/linter/SchemaRefsValidator.ts index 9dc184a7b..149d40f34 100644 --- a/tools/linter/SchemaRefsValidator.ts +++ b/tools/linter/SchemaRefsValidator.ts @@ -17,39 +17,41 @@ export default class SchemaRefsValidator { this.#build_available_schemas() } - #find_refs_in_namespaces_folder () { - const search = (obj: Record) => { - const ref = obj.$ref - if (ref) { + #find_refs_in_namespaces_folder (): void { + const search = (obj: any): void => { + const ref: string = obj.$ref ?? '' + if (ref !== '') { const file = ref.split('#')[0].replace('../', '') - const name = ref.split('/').pop() - if (!this.referenced_schemas[file]) this.referenced_schemas[file] = new Set() + const name = ref.split('/').pop() ?? '' + if (name === '') throw new Error(`Invalid schema reference: ${ref}`) + if (this.referenced_schemas[file] == null) this.referenced_schemas[file] = new Set() this.referenced_schemas[file].add(name) } for (const key in obj) { if (typeof obj[key] === 'object') search(obj[key]) } } - this.namespaces_folder.files.forEach((file) => { search(file.spec().components || {}) }) + this.namespaces_folder.files.forEach((file) => { search(file.spec().components ?? {}) }) } - #find_refs_in_schemas_folder () { - const search = (obj: Record, ref_file: string) => { - const ref = obj.$ref - if (ref) { + #find_refs_in_schemas_folder (): void { + const search = (obj: any, ref_file: string): void => { + const ref = obj.$ref as string ?? '' + if (ref !== '') { const file = ref.startsWith('#') ? ref_file : `schemas/${ref.split('#')[0]}` - const name = ref.split('/').pop() - if (!this.referenced_schemas[file]) this.referenced_schemas[file] = new Set() + const name = ref.split('/').pop() ?? '' + if (name === '') throw new Error(`Invalid schema reference: ${ref}`) + if (this.referenced_schemas[file] == null) this.referenced_schemas[file] = new Set() this.referenced_schemas[file].add(name) } for (const key in obj) { if (typeof obj[key] === 'object') search(obj[key], ref_file) } } - this.schemas_folder.files.forEach((file) => { search(file.spec().components?.schemas || {}, file.file) }) + this.schemas_folder.files.forEach((file) => { search(file.spec().components?.schemas ?? {}, file.file) }) } - #build_available_schemas () { + #build_available_schemas (): void { this.schemas_folder.files.forEach((file) => { - this.available_schemas[file.file] = new Set(Object.keys(file.spec().components?.schemas || {})) + this.available_schemas[file.file] = new Set(Object.keys(file.spec().components?.schemas ?? {})) }) } @@ -63,7 +65,7 @@ export default class SchemaRefsValidator { validate_unresolved_refs (): ValidationError[] { return Object.entries(this.referenced_schemas).flatMap(([ref_file, ref_schemas]) => { const available = this.available_schemas[ref_file] - if (!available) { + if (available == null) { return { file: this.namespaces_folder.file, message: `Unresolved schema reference: Schema file ${ref_file} is referenced but does not exist.` @@ -85,9 +87,9 @@ export default class SchemaRefsValidator { validate_unreferenced_schemas (): ValidationError[] { return Object.entries(this.available_schemas).flatMap(([file, schemas]) => { const referenced = this.referenced_schemas[file] - if (!referenced) { + if (referenced == null) { return { - file: file, + file, message: `Unreferenced schema: Schema file ${file} is not referenced anywhere.` } } @@ -95,7 +97,7 @@ export default class SchemaRefsValidator { return Array.from(schemas).map((schema) => { if (!referenced.has(schema)) { return { - file: file, + file, location: `#/components/schemas/${schema}`, message: `Unreferenced schema: Schema ${schema} is not referenced anywhere.` } diff --git a/tools/linter/SpecValidator.ts b/tools/linter/SpecValidator.ts index b227c4fec..a9ea98f14 100644 --- a/tools/linter/SpecValidator.ts +++ b/tools/linter/SpecValidator.ts @@ -1,38 +1,30 @@ import SchemasFolder from './components/SchemasFolder' import NamespacesFolder from './components/NamespacesFolder' -import RootFile from './components/RootFile' import { type ValidationError } from '../types' -import PathRefsValidator from './PathRefsValidator' import SchemaRefsValidator from './SchemaRefsValidator' import SupersededOperationsFile from './components/SupersededOperationsFile' export default class SpecValidator { - root_file: RootFile superseded_ops_files: SupersededOperationsFile namespaces_folder: NamespacesFolder schemas_folder: SchemasFolder - path_refs_validator: PathRefsValidator schema_refs_validator: SchemaRefsValidator constructor (root_folder: string) { - this.root_file = new RootFile(`${root_folder}/opensearch-openapi.yaml`) this.superseded_ops_files = new SupersededOperationsFile(`${root_folder}/_superseded_operations.yaml`) this.namespaces_folder = new NamespacesFolder(`${root_folder}/namespaces`) this.schemas_folder = new SchemasFolder(`${root_folder}/schemas`) - this.path_refs_validator = new PathRefsValidator(this.root_file, this.namespaces_folder) this.schema_refs_validator = new SchemaRefsValidator(this.namespaces_folder, this.schemas_folder) } validate (): ValidationError[] { const component_errors = [ - ...this.root_file.validate(), ...this.namespaces_folder.validate(), ...this.schemas_folder.validate() ] if (component_errors.length > 0) return component_errors return [ - ...this.path_refs_validator.validate(), ...this.schema_refs_validator.validate(), ...this.superseded_ops_files.validate() ] diff --git a/tools/linter/components/NamespaceFile.ts b/tools/linter/components/NamespaceFile.ts index 0f995e67e..f163c0c28 100644 --- a/tools/linter/components/NamespaceFile.ts +++ b/tools/linter/components/NamespaceFile.ts @@ -3,7 +3,7 @@ import { type OperationSpec, type ValidationError } from '../../types' import OperationGroup from './OperationGroup' import _ from 'lodash' import Operation from './Operation' -import { resolveRef } from '../../helpers' +import { resolve_ref } from '../../helpers' import FileValidator from './base/FileValidator' const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] @@ -11,8 +11,8 @@ const NAME_REGEX = /^[a-z]+[a-z_]*[a-z]+$/ export default class NamespaceFile extends FileValidator { namespace: string - _operation_groups: OperationGroup[] | undefined - _refs: Set | undefined + private _operation_groups: OperationGroup[] | undefined + private _refs: Set | undefined constructor (file_path: string) { super(file_path) @@ -41,39 +41,41 @@ export default class NamespaceFile extends FileValidator { }) }) - return this._operation_groups = _.entries(_.groupBy(ops, (op) => op.group)).map(([group, ops]) => { + this._operation_groups = _.entries(_.groupBy(ops, (op) => op.group)).map(([group, ops]) => { return new OperationGroup(this.file, group, ops) }) + return this._operation_groups } refs (): Set { if (this._refs) return this._refs this._refs = new Set() - const find_refs = (obj: Record) => { - if (obj.$ref) this._refs!.add(obj.$ref) - _.values(obj).forEach((value) => { if (typeof value === 'object') find_refs(value) }) + const find_refs = (obj: Record): void => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (obj.$ref != null) this._refs!.add(obj.$ref as string) + _.values(obj).forEach((value) => { if (typeof value === 'object') find_refs(value as Record) }) } - find_refs(this.spec().paths || {}) + find_refs(this.spec().paths ?? {}) return this._refs } - validate_name (name = this.namespace): ValidationError | void { + validate_name (name = this.namespace): ValidationError | undefined { if (name === '_core') return if (!name.match(NAME_REGEX)) { return this.error(`Invalid namespace name '${name}'. Must match regex: /${NAME_REGEX.source}/.`, 'File Name') } } - validate_schemas (): ValidationError | void { + validate_schemas (): ValidationError | undefined { if (this.spec().components?.schemas) { return this.error('components/schemas is not allowed in namespace files', '#/components/schemas') } } validate_unresolved_refs (): ValidationError[] { return Array.from(this.refs()).map((ref) => { - if (resolveRef(ref, this.spec()) === undefined) return this.error(`Unresolved reference: ${ref}`, ref) + if (resolve_ref(ref, this.spec()) === undefined) return this.error(`Unresolved reference: ${ref}`, ref) }).filter((e) => e) as ValidationError[] } validate_unused_refs (): ValidationError[] { - return _.entries(this.spec().components || {}).flatMap(([type, collection]) => { + return _.entries(this.spec().components ?? {}).flatMap(([type, collection]) => { return _.keys(collection).map((name) => { if (!this.refs().has(`#/components/${type}/${name}`)) { return this.error(`Unused ${type} component: ${name}`, `#/components/${type}/${name}`) } }) @@ -81,7 +83,7 @@ export default class NamespaceFile extends FileValidator { } validate_parameter_refs (): ValidationError[] { - const parameters = this.spec().components?.parameters as Record + const parameters = this.spec().components?.parameters as Record | undefined if (!parameters) return [] return _.entries(parameters).map(([name, p]) => { const group = name.split('::')[0] diff --git a/tools/linter/components/NamespacesFolder.ts b/tools/linter/components/NamespacesFolder.ts index b1be1faa2..86c544fb3 100644 --- a/tools/linter/components/NamespacesFolder.ts +++ b/tools/linter/components/NamespacesFolder.ts @@ -12,12 +12,12 @@ export default class NamespacesFolder extends FolderValidator { } validate_duplicate_paths (): ValidationError[] { - const paths: { [path: string]: string[] } = {} + const paths: Record = {} for (const file of this.files) { - if (!file._spec?.paths) continue + if (file.spec().paths == null) continue Object.keys(file.spec().paths).sort().forEach((path) => { - if (paths[path]) paths[path].push(file.namespace) - else paths[path] = [file.namespace] + if (paths[path] == null) paths[path] = [file.namespace] + else paths[path].push(file.namespace) }) } return Object.entries(paths).map(([path, namespaces]) => { diff --git a/tools/linter/components/Operation.ts b/tools/linter/components/Operation.ts index e7f5adc60..5714473b1 100644 --- a/tools/linter/components/Operation.ts +++ b/tools/linter/components/Operation.ts @@ -27,22 +27,22 @@ export default class Operation extends ValidatorBase { const namespace_error = this.validate_namespace() if (namespace_error) return [namespace_error] return [ - this.validate_operationId(), + this.validate_operation_id(), this.validate_description(), - this.validate_requestBody(), + this.validate_request_body(), this.validate_parameters(), this.validate_path_parameters(), ...this.validate_responses() ].filter((e) => e) as ValidationError[] } - validate_group (): ValidationError | void { + validate_group (): ValidationError | undefined { if (!this.group || this.group === '') { return this.error('Missing x-operation-group property') } - if (!this.group.match(GROUP_REGEX)) { return this.error(`Invalid x-operation-group '${this.group}'. Must match regex: /${GROUP_REGEX.source}/.`) } + if (!GROUP_REGEX.test(this.group)) { return this.error(`Invalid x-operation-group '${this.group}'. Must match regex: /${GROUP_REGEX.source}/.`) } } - validate_namespace (): ValidationError | void { - const expected_namespace = this.file.match(/namespaces\/(.*)\.yaml/)![1] + validate_namespace (): ValidationError | undefined { + const expected_namespace = this.file.match(/\/(.*)\.yaml/)?.[1] if (expected_namespace === '_core' && this.namespace === undefined) return if (expected_namespace === '_core' && this.namespace === '_core') { return this.error(`Invalid x-operation-group '${this.group}'. '_core' namespace must be omitted in x-operation-group.`) } @@ -52,19 +52,20 @@ export default class Operation extends ValidatorBase { `Only '${expected_namespace}' namespace is allowed in this file.`) } - validate_description (): ValidationError | void { - const description = this.spec.description - if (!description || description === '') { return this.error('Missing description property.') } + validate_description (): ValidationError | undefined { + const description = this.spec.description ?? '' + if (description === '') { return this.error('Missing description property.') } if (!description.endsWith('.')) { return this.error('Description must end with a period.') } } - validate_operationId (): ValidationError | void { - const id = this.spec.operationId - if (!id || id === '') { return this.error('Missing operationId property.') } - if (!id.match(new RegExp(`^${this.group_regex}\\.[0-9]+$`))) { return this.error(`Invalid operationId '${id}'. Must be in {x-operation-group}.{number} format.`) } + validate_operation_id (): ValidationError | undefined { + const id = this.spec.operationId ?? '' + if (id === '') { return this.error('Missing operationId property.') } + const regex = new RegExp(`^${this.group_regex}\\.[0-9]+$`) + if (!regex.test(id)) { return this.error(`Invalid operationId '${id}'. Must be in {x-operation-group}.{number} format.`) } } - validate_requestBody (): ValidationError | void { + validate_request_body (): ValidationError | undefined { const body = this.spec.requestBody if (!body) return const expected = `#/components/requestBodies/${this.group}` @@ -72,36 +73,36 @@ export default class Operation extends ValidatorBase { } validate_responses (): ValidationError[] { - const responses = this.spec.responses - if (!responses || _.keys(responses).length === 0) return [this.error('Missing responses property.')] + const responses = this.spec.responses ?? {} + if (_.keys(responses).length === 0) return [this.error('Missing responses property.')] return _.entries(responses).map(([code, response]) => { const expected = `#/components/responses/${this.group}@${code}` - if (response.$ref && response.$ref !== expected) { return this.error(`The ${code} response must be a reference object to '${expected}'.`) } + if (response.$ref !== expected) { return this.error(`The ${code} response must be a reference object to '${expected}'.`) } }).filter((error) => error) as ValidationError[] } - validate_parameters (): ValidationError | void { + validate_parameters (): ValidationError | undefined { const parameters = this.spec.parameters if (!parameters) return const regex = new RegExp(`^#/components/parameters/${this.group_regex}::((path)|(query))\\.[a-z0-9_.]+$`) for (const parameter of parameters) { - if (!parameter.$ref.match(regex)) { return this.error('Every parameter must be a reference object to \'#/components/parameters/{x-operation-group}::{path|query}.{parameter_name}\'.') } + if (!regex.test(parameter.$ref)) { return this.error('Every parameter must be a reference object to \'#/components/parameters/{x-operation-group}::{path|query}.{parameter_name}\'.') } } } - validate_path_parameters (): ValidationError | void { + validate_path_parameters (): ValidationError | undefined { const path_params = this.path_params() - const expected = this.path.match(/{[a-z0-9_]+}/g)?.map(p => p.slice(1, -1)) || [] + const expected = this.path.match(/{[a-z0-9_]+}/g)?.map(p => p.slice(1, -1)) ?? [] if (path_params.sort().join(', ') !== expected.sort().join(', ')) { return this.error(`Path parameters must match the parameters in the path: {${expected.join('}, {')}}.`) } } path_params (): string[] { return this.spec.parameters?.map(p => p.$ref?.match(/::path\.(.+)/)?.[1]) - .filter((p): p is string => p !== undefined) || [] + .filter((p): p is string => p !== undefined) ?? [] } query_params (): string[] { return this.spec.parameters?.map(p => p.$ref?.match(/::query\.(.+)/)?.[1]) - .filter((p): p is string => p !== undefined) || [] + .filter((p): p is string => p !== undefined) ?? [] } } diff --git a/tools/linter/components/OperationGroup.ts b/tools/linter/components/OperationGroup.ts index 9b88b9bb8..db4a06544 100644 --- a/tools/linter/components/OperationGroup.ts +++ b/tools/linter/components/OperationGroup.ts @@ -22,35 +22,35 @@ export default class OperationGroup extends ValidatorBase { if (this.operations.length === 1) return [] return [ this.validate_description(), - this.validate_externalDocs(), - this.validate_requestBody(), + this.validate_external_docs(), + this.validate_request_body(), this.validate_responses(), this.validate_query_parameters() ].filter((e) => e) as ValidationError[] } - validate_description (): ValidationError | void { + validate_description (): ValidationError | undefined { const uniq_descriptions = new Set(this.operations.map((op) => op.spec.description)) if (uniq_descriptions.size > 1) { return this.error(`${this.operations.length} '${this.name}' operations must have identical description property.`) } } - validate_externalDocs (): ValidationError | void { - const uniq_externalDocs = new Set(this.operations.map((op) => op.spec.externalDocs?.url)) - if (uniq_externalDocs.size > 1) { return this.error(`${this.operations.length} '${this.name}' operations must have identical externalDocs property.`) } + validate_external_docs (): ValidationError | undefined { + const uniq_external_docs = new Set(this.operations.map((op) => op.spec.externalDocs?.url)) + if (uniq_external_docs.size > 1) { return this.error(`${this.operations.length} '${this.name}' operations must have identical externalDocs property.`) } } - validate_requestBody (): ValidationError | void { - const uniq_requestBodies = new Set(this.operations.map((op) => op.spec.requestBody?.$ref)) - if (uniq_requestBodies.size > 1) { return this.error(`${this.operations.length} '${this.name}' operations must have identical requestBody property.`) } + validate_request_body (): ValidationError | undefined { + const uniq_request_bodies = new Set(this.operations.map((op) => op.spec.requestBody?.$ref)) + if (uniq_request_bodies.size > 1) { return this.error(`${this.operations.length} '${this.name}' operations must have identical requestBody property.`) } } - validate_responses (): ValidationError | void { + validate_responses (): ValidationError | undefined { const key_signatures = this.operations.map((op) => Object.keys(op.spec.responses).sort().join('#$@')) const uniq_signatures = new Set(key_signatures) if (uniq_signatures.size > 1) { return this.error(`${this.operations.length} '${this.name}' operations must have an identical set of responses.`) } } - validate_query_parameters (): ValidationError | void { + validate_query_parameters (): ValidationError | undefined { const query_signatures = this.operations.map((op) => op.query_params().sort().join('#$@')) const uniq_signatures = new Set(query_signatures) if (uniq_signatures.size > 1) { return this.error(`${this.operations.length} '${this.name}' operations must have an identical set of query parameters.`) } diff --git a/tools/linter/components/RootFile.ts b/tools/linter/components/RootFile.ts deleted file mode 100644 index 3cfd9b7ae..000000000 --- a/tools/linter/components/RootFile.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type ParameterSpec, type ValidationError } from '../../types' -import FileValidator from './base/FileValidator' - -export default class RootFile extends FileValidator { - constructor (file_path: string) { - super(file_path) - this.file = file_path.split('/').pop()! - } - - validate_file (): ValidationError[] { - return [ - this.validate_paths(), - this.validate_params() - ].flat() - } - - validate_paths (): ValidationError[] { - return Object.entries(this.spec().paths).map(([path, spec]) => { - if (!spec?.$ref) { return this.error('Every path must be a reference object to a path in a namespace file.', `Path: ${path}`) } - }).filter((e) => e) as ValidationError[] - } - - validate_params (): ValidationError[] { - const params = (this.spec().components?.parameters || {}) as Record - return Object.entries(params).map(([name, param]) => { - const expected = `_global::${param.in}.${param.name}` - if (name !== expected) { return this.error(`Parameters in root file must be in the format '_global::{in}.{name}'. Expected '${expected}'.`, `#/components/parameters/${name}`) } - if (!param['x-global']) { return this.error('Parameters in root file must have \'x-global\' extension set to true.', `#/components/parameters/${name}`) } - }).filter((e) => e) as ValidationError[] - } -} diff --git a/tools/linter/components/SchemaFile.ts b/tools/linter/components/SchemaFile.ts index 65284b68f..074b76892 100644 --- a/tools/linter/components/SchemaFile.ts +++ b/tools/linter/components/SchemaFile.ts @@ -8,7 +8,7 @@ const NAME_REGEX = /^[a-z]+[a-z_]*[a-z]+$/ export default class SchemaFile extends FileValidator { category: string - _schemas: Schema[] | undefined + private _schemas: Schema[] | undefined constructor (file_path: string) { super(file_path) @@ -26,15 +26,16 @@ export default class SchemaFile extends FileValidator { schemas (): Schema[] { if (this._schemas) return this._schemas - return Object.entries(this.spec().components?.schemas || {}).map(([name, spec]) => { + this._schemas = Object.entries(this.spec().components?.schemas ?? {}).map(([name, spec]) => { return new Schema(this.file, name, spec as OpenAPIV3.SchemaObject) }) + return this._schemas } - validate_category (category = this.category): ValidationError | void { + validate_category (category = this.category): ValidationError | undefined { if (category === '_common') return - if (!category.match(CATEGORY_REGEX)) { return this.error(`Invalid category name '${category}'. Must match regex: /${CATEGORY_REGEX.source}/.`, 'File Name') } + if (!CATEGORY_REGEX.test(category)) { return this.error(`Invalid category name '${category}'. Must match regex: /${CATEGORY_REGEX.source}/.`, 'File Name') } const name = category.split('.')[1] - if (name !== '_common' && !name.match(NAME_REGEX)) { return this.error(`Invalid category name '${category}'. '${name}' does not match regex: /${NAME_REGEX.source}/.`, 'File Name') } + if (name !== '_common' && !NAME_REGEX.test(name)) { return this.error(`Invalid category name '${category}'. '${name}' does not match regex: /${NAME_REGEX.source}/.`, 'File Name') } } } diff --git a/tools/linter/components/SupersededOperationsFile.ts b/tools/linter/components/SupersededOperationsFile.ts index 208279c14..335a897ff 100644 --- a/tools/linter/components/SupersededOperationsFile.ts +++ b/tools/linter/components/SupersededOperationsFile.ts @@ -1,11 +1,10 @@ import FileValidator from './base/FileValidator' import ajv from 'ajv' -import fs from 'fs' -import YAML from 'yaml' import { type ValidationError } from '../../types' +import { read_yaml } from '../../helpers' export default class SupersededOperationsFile extends FileValidator { - JSON_SCHEMA_PATH = '../json_schemas/_superseded_operations.yaml' + readonly JSON_SCHEMA_PATH = '../json_schemas/_superseded_operations.yaml' validate (): ValidationError[] { return [ @@ -14,7 +13,7 @@ export default class SupersededOperationsFile extends FileValidator { } validate_json_schema (): ValidationError | undefined { - const schema = YAML.parse(fs.readFileSync(this.JSON_SCHEMA_PATH, 'utf8')) + const schema = read_yaml(this.JSON_SCHEMA_PATH) const validator = (new ajv()).compile(schema) if (!validator(this.spec())) { return this.error(`File content does not match JSON schema found in '${this.JSON_SCHEMA_PATH}':\n ${JSON.stringify(validator.errors, null, 2)}`) diff --git a/tools/linter/components/base/FileValidator.ts b/tools/linter/components/base/FileValidator.ts index 6f5d234aa..f2d777216 100644 --- a/tools/linter/components/base/FileValidator.ts +++ b/tools/linter/components/base/FileValidator.ts @@ -1,12 +1,11 @@ import ValidatorBase from './ValidatorBase' import { type ValidationError } from '../../../types' -import fs from 'fs' -import YAML from 'yaml' import { type OpenAPIV3 } from 'openapi-types' +import { read_yaml } from '../../../helpers' export default class FileValidator extends ValidatorBase { file_path: string - _spec: OpenAPIV3.Document | undefined + protected _spec: OpenAPIV3.Document | undefined constructor (file_path: string) { super(file_path.split('/').slice(-2).join('/')) @@ -15,10 +14,11 @@ export default class FileValidator extends ValidatorBase { spec (): OpenAPIV3.Document { if (this._spec) return this._spec - return this._spec = YAML.parse(fs.readFileSync(this.file_path, 'utf8')) || {} + this._spec = read_yaml(this.file_path) as OpenAPIV3.Document + return this._spec } - validate (...args: any[]): ValidationError[] { + validate (): ValidationError[] { const extension_error = this.validate_extension() if (extension_error) return [extension_error] const yaml_error = this.validate_yaml() @@ -26,7 +26,7 @@ export default class FileValidator extends ValidatorBase { return this.validate_file() } - validate_file (...args: any[]): ValidationError[] { + validate_file (): ValidationError[] { throw new Error('Method not implemented.') } diff --git a/tools/linter/components/base/FolderValidator.ts b/tools/linter/components/base/FolderValidator.ts index 2543b6f82..30eafbfac 100644 --- a/tools/linter/components/base/FolderValidator.ts +++ b/tools/linter/components/base/FolderValidator.ts @@ -9,19 +9,18 @@ export default class FolderValidator extends ValidatorB constructor (folder_path: string, file_type: new (file_path: string) => F) { const parts = folder_path.split('/').reverse() - const folder_name = (parts[0] === undefined ? parts[1] : parts[0]) + '/' + const folder_name = (parts[0] ?? parts[1]) + '/' super(folder_name, 'Folder') this.folder_path = folder_path this.files = fs.readdirSync(this.folder_path).sort() .filter((file) => file !== '.gitkeep') - .map((file) => { return new file_type(`${this.folder_path}/${file}`) as F }) + .map((file) => { return new file_type(`${this.folder_path}/${file}`) }) } validate (): ValidationError[] { - return [ - ...this.files.flatMap((file) => file.validate()), - ...this.validate_folder() - ] + const file_errors = this.files.flatMap((file) => file.validate()) + if (file_errors.length > 0) return file_errors + return this.validate_folder() } validate_folder (): ValidationError[] { diff --git a/tools/linter/lint.ts b/tools/linter/lint.ts index af9a25394..9b8d05abe 100644 --- a/tools/linter/lint.ts +++ b/tools/linter/lint.ts @@ -1,6 +1,6 @@ import SpecValidator from './SpecValidator' -const root_folder = process.argv[2] || '../spec' +const root_folder = process.argv[2] ?? '../spec' const validator = new SpecValidator(root_folder) const errors = validator.validate() @@ -9,7 +9,7 @@ if (errors.length === 0) { process.exit(0) } else { console.log('Errors found:\n') - errors.forEach(e => console.error(e)) + errors.forEach(e => { console.error(e) }) console.log('\nTotal errors:', errors.length) process.exit(1) } diff --git a/tools/merger/GlobalParamsGenerator.ts b/tools/merger/GlobalParamsGenerator.ts new file mode 100644 index 000000000..df84ff8c3 --- /dev/null +++ b/tools/merger/GlobalParamsGenerator.ts @@ -0,0 +1,36 @@ +import { type OpenAPIV3 } from 'openapi-types' +import _ from 'lodash' +import { read_yaml } from '../helpers' + +export default class GlobalParamsGenerator { + global_params: Record + + constructor (root_path: string) { + const file_path = root_path + '/_global_parameters.yaml' + const spec = read_yaml(file_path) as OpenAPIV3.Document + this.global_params = this.create_global_params(spec) + } + + generate (spec: Record): void { + spec.components.parameters = { ...this.global_params, ...spec.components.parameters } + + const global_param_refs = Object.keys(this.global_params).map(param => ({ $ref: `#/components/parameters/${param}` })) + Object.entries(spec.paths as Document).forEach(([path, path_item]) => { + Object.entries(path_item as Document).forEach(([method, operation]) => { + const params = operation.parameters ?? [] + operation.parameters = [...params, ...Object.values(global_param_refs)] + }) + }) + } + + create_global_params (spec: OpenAPIV3.Document): Record { + const params = (spec.components?.parameters ?? {}) as Record + _.entries(params).forEach(([original_key, param]) => { + const global_key = `_global::${param.in}.${param.name}` + params[global_key] = param + _.set(param, 'x-global', true) + _.unset(params, original_key) + }) + return params + } +} diff --git a/tools/merger/OpenApiMerger.ts b/tools/merger/OpenApiMerger.ts index f76731aae..7054eac12 100644 --- a/tools/merger/OpenApiMerger.ts +++ b/tools/merger/OpenApiMerger.ts @@ -1,78 +1,60 @@ import { type OpenAPIV3 } from 'openapi-types' import fs from 'fs' import _ from 'lodash' -import yaml from 'yaml' -import { write2file } from '../helpers' +import { read_yaml, write_yaml } from '../helpers' import SupersededOpsGenerator from './SupersededOpsGenerator' +import GlobalParamsGenerator from './GlobalParamsGenerator' // Create a single-file OpenAPI spec from multiple files for OpenAPI validation and programmatic consumption export default class OpenApiMerger { - root_path: string root_folder: string spec: Record - global_param_refs: OpenAPIV3.ReferenceObject[] paths: Record> = {} // namespace -> path -> path_item_object schemas: Record> = {} // category -> schema -> schema_object - constructor (root_path: string) { - this.root_path = fs.realpathSync(root_path) - this.root_folder = this.root_path.split('/').slice(0, -1).join('/') - this.spec = yaml.parse(fs.readFileSync(this.root_path, 'utf8')) - const global_params: OpenAPIV3.ParameterObject = this.spec.components?.parameters || {} - this.global_param_refs = Object.keys(global_params).map(param => ({ $ref: `#/components/parameters/${param}` })) - this.spec.components = { - parameters: global_params, - requestBodies: {}, - responses: {}, - schemas: {} + constructor (root_folder: string) { + this.root_folder = fs.realpathSync(root_folder) + this.spec = { + openapi: '3.1.0', + info: read_yaml(`${this.root_folder}/_info.yaml`), + paths: {}, + components: { + parameters: {}, + requestBodies: {}, + responses: {}, + schemas: {} + } } } - merge (output_path?: string): OpenAPIV3.Document { + merge (output_path: string = ''): OpenAPIV3.Document { this.#merge_schemas() this.#merge_namespaces() - this.#apply_global_params() this.#sort_spec_keys() - this.#generate_replaced_ops() + this.#generate_global_params() + this.#generate_superseded_ops() - if (output_path) write2file(output_path, this.spec) + if (output_path !== '') write_yaml(output_path, this.spec) return this.spec as OpenAPIV3.Document } - // Apply global parameters to all operations in the spec. - #apply_global_params (): void { - Object.entries(this.spec.paths).forEach(([path, pathItem]) => { - Object.entries(pathItem!).forEach(([method, operation]) => { - const params = operation.parameters || [] - operation.parameters = [...params, ...Object.values(this.global_param_refs)] - }) - }) - } - // Merge files from /namespaces folder. #merge_namespaces (): void { const folder = `${this.root_folder}/namespaces` fs.readdirSync(folder).forEach(file => { - const spec = yaml.parse(fs.readFileSync(`${folder}/${file}`, 'utf8')) - const namespace = file.split('.yaml')[0] + const spec = read_yaml(`${folder}/${file}`) this.redirect_refs_in_namespace(spec) - this.paths[namespace] = 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'] } - }) - - Object.entries(this.spec.paths).forEach(([path, refObj]) => { - const ref = (refObj as Record).$ref! - const namespace = ref.match(/namespaces\/(.*)\.yaml/)![1] - this.spec.paths[path] = this.paths[namespace][path] + 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: Record): void { - const ref = obj.$ref + 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) { @@ -84,25 +66,26 @@ export default class OpenApiMerger { #merge_schemas (): void { const folder = `${this.root_folder}/schemas` fs.readdirSync(folder).forEach(file => { - const spec = yaml.parse(fs.readFileSync(`${folder}/${file}`, 'utf8')) + const spec = read_yaml(`${folder}/${file}`) const category = file.split('.yaml')[0] this.redirect_refs_in_schema(category, spec) - this.schemas[category] = spec['components']['schemas'] as Record + this.schemas[category] = spec.components.schemas as Record }) Object.entries(this.schemas).forEach(([category, schemas]) => { - Object.entries(schemas).forEach(([name, schemaObj]) => { - this.spec.components.schemas[`${category}:${name}`] = schemaObj + Object.entries(schemas).forEach(([name, schema_obj]) => { + this.spec.components.schemas[`${category}:${name}`] = schema_obj }) }) } // Redirect schema references in schema files to local references in single-file spec. - redirect_refs_in_schema (category: string, obj: Record): void { - const ref = obj.$ref - if (ref) { + redirect_refs_in_schema (category: string, obj: any): void { + const ref: string = obj.$ref ?? '' + if (ref !== '') { if (ref.startsWith('#/components/schemas')) { obj.$ref = `#/components/schemas/${category}:${ref.split('/').pop()}` } else { - const other_category = ref.match(/(.*)\.yaml/)![1] + const other_category = ref.match(/(.*)\.yaml/)?.[1] ?? '' + if (other_category === '') throw new Error(`Invalid schema reference: ${ref}`) obj.$ref = `#/components/schemas/${other_category}:${ref.split('/').pop()}` } } @@ -114,18 +97,25 @@ 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).sort()) - this.spec.components.parameters = _.fromPairs(Object.entries(this.spec.components.parameters).sort()) - this.spec.components.responses = _.fromPairs(Object.entries(this.spec.components.responses).sort()) - this.spec.components.requestBodies = _.fromPairs(Object.entries(this.spec.components.requestBodies).sort()) - - this.spec.paths = _.fromPairs(Object.entries(this.spec.paths).sort()) - Object.entries(this.spec.paths).forEach(([path, pathItem]) => { - this.spec.paths[path] = _.fromPairs(Object.entries(pathItem!).sort()) + 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_replaced_ops (): void { + // Generate global parameters from _global_params.yaml file. + #generate_global_params (): void { + const gen = new GlobalParamsGenerator(this.root_folder) + gen.generate(this.spec) + } + + // Generate superseded operations from _superseded_operations.yaml file. + #generate_superseded_ops (): void { const gen = new SupersededOpsGenerator(this.root_folder) gen.generate(this.spec) } diff --git a/tools/merger/OpenDistro.ts b/tools/merger/OpenDistro.ts index 4014c3ccc..e35cace6e 100644 --- a/tools/merger/OpenDistro.ts +++ b/tools/merger/OpenDistro.ts @@ -1,7 +1,5 @@ -import fs from 'fs' -import YAML from 'yaml' import { type HttpVerb, type OperationPath, type SupersededOperationMap } from '../types' -import { write2file } from '../helpers' +import { read_yaml, write_yaml } from '../helpers' // One-time script to generate _superseded_operations.yaml file for OpenDistro // Keeping this for now in case we need to update the file in the near future. Can be removed after a few months. @@ -11,12 +9,12 @@ export default class OpenDistro { output: SupersededOperationMap = {} constructor (file_path: string) { - this.input = YAML.parse(fs.readFileSync(file_path, 'utf8')) + this.input = read_yaml(file_path) this.build_output() - write2file(file_path, this.output) + write_yaml(file_path, this.output) } - build_output () { + build_output (): void { for (const [path, operations] of Object.entries(this.input)) { const replaced_by = path.replace('_opendistro', '_plugins') this.output[path] = { superseded_by: replaced_by, operations } diff --git a/tools/merger/SupersededOpsGenerator.ts b/tools/merger/SupersededOpsGenerator.ts index 1d37ff99f..747204c61 100644 --- a/tools/merger/SupersededOpsGenerator.ts +++ b/tools/merger/SupersededOpsGenerator.ts @@ -1,14 +1,13 @@ import { type OperationSpec, type SupersededOperationMap } from '../types' -import YAML from 'yaml' -import fs from 'fs' import _ from 'lodash' +import { read_yaml } from '../helpers' export default class SupersededOpsGenerator { superseded_ops: SupersededOperationMap constructor (root_path: string) { const file_path = root_path + '/_superseded_operations.yaml' - this.superseded_ops = YAML.parse(fs.readFileSync(file_path, 'utf8')) + this.superseded_ops = read_yaml(file_path) as SupersededOperationMap delete this.superseded_ops.$schema } @@ -17,13 +16,13 @@ export default class SupersededOpsGenerator { const regex = this.path_to_regex(superseded_by) const operation_keys = operations.map(op => op.toLowerCase()) const superseded_path = this.copy_params(superseded_by, path) - const path_entry = _.entries(spec.paths).find(([path, _]) => regex.test(path)) + const path_entry = _.entries(spec.paths as Document).find(([path, _]) => regex.test(path)) if (!path_entry) console.log(`Path not found: ${superseded_by}`) - else spec.paths[superseded_path] = this.path_object(path_entry[1] as any, operation_keys) + else spec.paths[superseded_path] = this.path_object(path_entry[1], operation_keys) } } - path_object (obj: Record, keys: string[]): Record { + path_object (obj: any, keys: string[]): Record { const cloned_obj = _.cloneDeep(_.pick(obj, keys)) for (const key in cloned_obj) { const operation = cloned_obj[key] as OperationSpec @@ -44,6 +43,6 @@ export default class SupersededOpsGenerator { const target_params = target_parts.filter(part => part.startsWith('{')) const source_params = source.split('/').filter(part => part.startsWith('{')).reverse() if (target_params.length !== source_params.length) { throw new Error('Mismatched parameters in source and target paths: ' + source + ' -> ' + target) } - return target_parts.map((part) => part.startsWith('{') ? source_params.pop()! : part).join('/') + return target_parts.map((part) => part.startsWith('{') ? source_params.pop() : part).join('/') } } diff --git a/tools/merger/merge.ts b/tools/merger/merge.ts index b4531b4b4..a32d3059b 100644 --- a/tools/merger/merge.ts +++ b/tools/merger/merge.ts @@ -1,6 +1,6 @@ import OpenApiMerger from './OpenApiMerger' -const root_path: string = process.argv[2] || '../spec/opensearch-openapi.yaml' +const root_path: string = process.argv[2] || '../spec' const output_path: string = process.argv[3] || '../opensearch-openapi.yaml' const merger = new OpenApiMerger(root_path) merger.merge(output_path) diff --git a/tools/package.json b/tools/package.json index a29beee9e..0e7bbbfde 100644 --- a/tools/package.json +++ b/tools/package.json @@ -11,13 +11,13 @@ "test": "jest" }, "dependencies": { - "ajv": "^8.13.0", "@apidevtools/swagger-parser": "^10.1.0", "@types/lodash": "^4.14.202", "@types/node": "^20.10.3", + "ajv": "^8.13.0", "lodash": "^4.17.21", - "typescript": "^5.4.5", "ts-node": "^10.9.1", + "typescript": "^5.4.5", "yaml": "^2.3.4" }, "devDependencies": { diff --git a/tools/test/linter/NamespacesFolder.test.ts b/tools/test/linter/NamespacesFolder.test.ts index 2ac2ccba6..9f2f6b839 100644 --- a/tools/test/linter/NamespacesFolder.test.ts +++ b/tools/test/linter/NamespacesFolder.test.ts @@ -1,45 +1,51 @@ import NamespacesFolder from '../../linter/components/NamespacesFolder' -test('validate()', () => { - const validator = new NamespacesFolder('./test/linter/fixtures/folder_validators/namespaces') +test('validate() - When there invalid files', () => { + const validator = new NamespacesFolder('./test/linter/fixtures/folder_validators/namespaces/invalid_files') expect(validator.validate()).toEqual([ { - file: 'namespaces/indices.txt', + file: 'invalid_files/indices.txt', location: 'File Extension', message: "Invalid file extension. Only '.yaml' files are allowed." }, { - file: 'namespaces/invalid_spec.yaml', + file: 'invalid_files/invalid_spec.yaml', location: 'Operation: GET /{index}/_doc/{id}', message: 'Missing description property.' }, { - file: 'namespaces/invalid_spec.yaml', + file: 'invalid_files/invalid_spec.yaml', location: 'Operation: GET /{index}/_doc/{id}', message: "Every parameter must be a reference object to '#/components/parameters/{x-operation-group}::{path|query}.{parameter_name}'." }, { - file: 'namespaces/invalid_spec.yaml', + file: 'invalid_files/invalid_spec.yaml', location: 'Operation: GET /{index}/_doc/{id}', message: 'Path parameters must match the parameters in the path: {id}, {index}.' }, { - file: 'namespaces/invalid_spec.yaml', + file: 'invalid_files/invalid_spec.yaml', location: 'Operation: GET /{index}/_doc/{id}', message: "The 200 response must be a reference object to '#/components/responses/invalid_spec.fetch@200'." }, { - file: 'namespaces/invalid_yaml.yaml', + file: 'invalid_files/invalid_yaml.yaml', location: 'File Content', message: 'Unable to read or parse YAML.' - }, + } + ]) +}) + +test('validate() - When the files are valid but the folder is not', () => { + const validator = new NamespacesFolder('./test/linter/fixtures/folder_validators/namespaces/invalid_folder') + expect(validator.validate()).toEqual([ { - file: 'namespaces/', + file: 'invalid_folder/', location: 'Folder', message: "Duplicate path '/{index}' found in namespaces: dup_path_a, dup_path_c." }, { - file: 'namespaces/', + file: 'invalid_folder/', location: 'Folder', message: "Duplicate path '/{index}/_rollover' found in namespaces: dup_path_a, dup_path_b, dup_path_c." } diff --git a/tools/test/linter/Operation.test.ts b/tools/test/linter/Operation.test.ts index 48173deed..f7426a29d 100644 --- a/tools/test/linter/Operation.test.ts +++ b/tools/test/linter/Operation.test.ts @@ -41,15 +41,15 @@ test('validate_namespace()', () => { test('validate_operationId()', () => { const no_id = operation({ 'x-operation-group': 'indices.create' }) - expect(no_id.validate_operationId()) + expect(no_id.validate_operation_id()) .toEqual(no_id.error('Missing operationId property.')) const invalid_id = operation({ 'x-operation-group': 'indices.create', operationId: 'create_index' }) - expect(invalid_id.validate_operationId()) + expect(invalid_id.validate_operation_id()) .toEqual(invalid_id.error('Invalid operationId \'create_index\'. Must be in {x-operation-group}.{number} format.')) const valid_id = operation({ 'x-operation-group': 'indices.create', operationId: 'indices.create.1' }) - expect(valid_id.validate_operationId()) + expect(valid_id.validate_operation_id()) .toBeUndefined() }) @@ -69,15 +69,15 @@ test('validate_description()', () => { test('validate_requestBody()', () => { const no_body = operation({ 'x-operation-group': 'indices.create' }) - expect(no_body.validate_requestBody()) + expect(no_body.validate_request_body()) .toBeUndefined() const valid_body = operation({ 'x-operation-group': 'indices.create', requestBody: { $ref: '#/components/requestBodies/indices.create' } }) - expect(valid_body.validate_requestBody()) + expect(valid_body.validate_request_body()) .toBeUndefined() const invalid_body = operation({ 'x-operation-group': 'indices.create', requestBody: { $ref: '#/components/requestBodies/indices.create.1' } }) - expect(invalid_body.validate_requestBody()) + expect(invalid_body.validate_request_body()) .toEqual(invalid_body.error('The requestBody must be a reference object to \'#/components/requestBodies/indices.create\'.')) }) diff --git a/tools/test/linter/OperationGroup.test.ts b/tools/test/linter/OperationGroup.test.ts index 9dcf8ae4d..564cab7b5 100644 --- a/tools/test/linter/OperationGroup.test.ts +++ b/tools/test/linter/OperationGroup.test.ts @@ -18,32 +18,32 @@ test('validate_description()', () => { }) test('validate_externalDocs()', () => { - const valid_externalDocs = operation_group([ + const valid_external_docs = operation_group([ { externalDocs: { url: 'https://example.com' } }, { externalDocs: { url: 'https://example.com' } }]) - expect(valid_externalDocs.validate_externalDocs()) + expect(valid_external_docs.validate_external_docs()) .toBeUndefined() - const invalid_externalDocs = operation_group([ + const invalid_external_docs = operation_group([ { externalDocs: { url: 'https://example.com' } }, { externalDocs: { url: 'https://example.com' } }, {}]) - expect(invalid_externalDocs.validate_externalDocs()) - .toEqual(invalid_externalDocs.error('3 \'indices.create\' operations must have identical externalDocs property.')) + expect(invalid_external_docs.validate_external_docs()) + .toEqual(invalid_external_docs.error('3 \'indices.create\' operations must have identical externalDocs property.')) }) test('validate_requestBody()', () => { - const valid_requestBodies = operation_group([ + const valid_request_bodies = operation_group([ { requestBody: { $ref: '#/components/requestBodies/indices.create' } }, { requestBody: { $ref: '#/components/requestBodies/indices.create' } }]) - expect(valid_requestBodies.validate_requestBody()) + expect(valid_request_bodies.validate_request_body()) .toBeUndefined() - const invalid_requestBodies = operation_group([ + const invalid_request_bodies = operation_group([ { requestBody: { $ref: '#/components/requestBodies/indices.create' } }, {}]) - expect(invalid_requestBodies.validate_requestBody()) - .toEqual(invalid_requestBodies.error('2 \'indices.create\' operations must have identical requestBody property.')) + expect(invalid_request_bodies.validate_request_body()) + .toEqual(invalid_request_bodies.error('2 \'indices.create\' operations must have identical requestBody property.')) }) test('validate_responses()', () => { diff --git a/tools/test/linter/PathRefsValidator.test.ts b/tools/test/linter/PathRefsValidator.test.ts deleted file mode 100644 index eaf8c2826..000000000 --- a/tools/test/linter/PathRefsValidator.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import PathRefsValidator from '../../linter/PathRefsValidator' -import RootFile from '../../linter/components/RootFile' -import NamespacesFolder from '../../linter/components/NamespacesFolder' - -test('validate()', () => { - const root_folder = './test/linter/fixtures/path_refs_validator' - const root_file = new RootFile(`${root_folder}/opensearch-openapi.yaml`) - const namespaces_folder = new NamespacesFolder(`${root_folder}/namespaces`) - const validator = new PathRefsValidator(root_file, namespaces_folder) - expect(validator.validate()).toEqual([ - { - file: 'opensearch-openapi.yaml', - location: 'Path: /{index}', - message: 'Unresolved path reference: Path /{index} does not exist in namespace file namespaces/indices.yaml.' - }, - { - file: 'opensearch-openapi.yaml', - location: 'Paths: /_cluster/health , /_cluster/{id}', - message: 'Unresolved path reference: Namespace file namespaces/cluster.yaml does not exist.' - }, - { - file: 'namespaces/indices.yaml', - location: 'Path: /{index}/_aliases', - message: 'Unreferenced path: Path /{index}/_aliases is not referenced in the root file.' - }, - { - file: 'namespaces/missing.yaml', - message: 'Unreferenced paths: No paths are referenced in the root file.' - } - ]) -}) diff --git a/tools/test/linter/RootFile.test.ts b/tools/test/linter/RootFile.test.ts deleted file mode 100644 index fe365fa89..000000000 --- a/tools/test/linter/RootFile.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import RootFile from '../../linter/components/RootFile' - -test('validate()', () => { - const validator = new RootFile('./test/linter/fixtures/root.yaml') - expect(validator.validate()).toEqual([ - { - file: 'root.yaml', - location: 'Path: /', - message: 'Every path must be a reference object to a path in a namespace file.' - }, - { - file: 'root.yaml', - location: 'Path: /{index}', - message: 'Every path must be a reference object to a path in a namespace file.' - }, - { - file: 'root.yaml', - location: '#/components/parameters/_global::query.pretty', - message: "Parameters in root file must be in the format '_global::{in}.{name}'. Expected '_global::query.beautify'." - }, - { - file: 'root.yaml', - location: '#/components/parameters/_global::query.human', - message: "Parameters in root file must have 'x-global' extension set to true." - } - ]) -}) diff --git a/tools/test/linter/SpecValidator.test.ts b/tools/test/linter/SpecValidator.test.ts index 97b40a2dd..181393f02 100644 --- a/tools/test/linter/SpecValidator.test.ts +++ b/tools/test/linter/SpecValidator.test.ts @@ -6,7 +6,6 @@ test('validate()', () => { validator.namespaces_folder.validate = jest.fn().mockReturnValue([{ file: 'namespaces/', message: 'namespace error' }]) validator.schemas_folder.validate = jest.fn().mockReturnValue([{ file: 'schemas/', message: 'schema error' }]) - validator.path_refs_validator.validate = jest.fn().mockReturnValue([{ file: 'path_refs', message: 'path refs error' }]) validator.schema_refs_validator.validate = jest.fn().mockReturnValue([{ file: 'schema_refs', message: 'schema refs error' }]) expect(validator.validate()).toEqual([ @@ -18,7 +17,6 @@ test('validate()', () => { validator.schemas_folder.validate = jest.fn().mockReturnValue([]) expect(validator.validate()).toEqual([ - { file: 'path_refs', message: 'path refs error' }, { file: 'schema_refs', message: 'schema refs error' } ]) }) diff --git a/tools/test/linter/factories/namespace_file.ts b/tools/test/linter/factories/namespace_file.ts index 5e89cba77..7546460a8 100644 --- a/tools/test/linter/factories/namespace_file.ts +++ b/tools/test/linter/factories/namespace_file.ts @@ -8,8 +8,8 @@ export function namespace_file (fixture_file: string): NamespaceFile { interface MockedReturnedValues { validate?: string[] - validate_name?: string | void - validate_schemas?: string | void + validate_name?: string | undefined + validate_schemas?: string | undefined validate_unresolved_refs?: string[] validate_unused_refs?: string[] validate_parameter_refs?: string[] @@ -20,8 +20,10 @@ export function mocked_namespace_file (ops: { returned_values?: MockedReturnedVa ns_file.file = 'namespaces/indices.yaml' ns_file.namespace = 'indices' - if (ops.groups_errors) ns_file._operation_groups = ops.groups_errors.map((errors) => mocked_operation_group({ validate: errors })) - if (ops.spec) ns_file._spec = { paths: {}, components: {}, ...ops.spec } as OpenAPIV3.Document + // eslint-disable-next-line @typescript-eslint/dot-notation + if (ops.groups_errors) ns_file['_operation_groups'] = ops.groups_errors.map((errors) => mocked_operation_group({ validate: errors })) + // eslint-disable-next-line @typescript-eslint/dot-notation,@typescript-eslint/consistent-type-assertions + if (ops.spec) ns_file['_spec'] = { paths: {}, components: {}, ...ops.spec } as OpenAPIV3.Document if (ops.returned_values) { if (ops.returned_values.validate) { @@ -36,11 +38,11 @@ export function mocked_namespace_file (ops: { returned_values?: MockedReturnedVa ns_file.validate_unused_refs = jest.fn() ns_file.validate_parameter_refs = jest.fn() - if (ops.returned_values.validate_name) (ns_file.validate_name as jest.Mock).mockReturnValue(ops.returned_values.validate_name) - if (ops.returned_values.validate_schemas) (ns_file.validate_schemas as jest.Mock).mockReturnValue(ops.returned_values.validate_schemas) - if (ops.returned_values.validate_unresolved_refs) (ns_file.validate_unresolved_refs as jest.Mock).mockReturnValue(ops.returned_values.validate_unresolved_refs) - if (ops.returned_values.validate_unused_refs) (ns_file.validate_unused_refs as jest.Mock).mockReturnValue(ops.returned_values.validate_unused_refs) - if (ops.returned_values.validate_parameter_refs) (ns_file.validate_parameter_refs as jest.Mock).mockReturnValue(ops.returned_values.validate_parameter_refs) + if (ops.returned_values.validate_name != null) (ns_file.validate_name as jest.Mock).mockReturnValue(ops.returned_values.validate_name) + if (ops.returned_values.validate_schemas != null) (ns_file.validate_schemas as jest.Mock).mockReturnValue(ops.returned_values.validate_schemas) + if (ops.returned_values.validate_unresolved_refs != null) (ns_file.validate_unresolved_refs as jest.Mock).mockReturnValue(ops.returned_values.validate_unresolved_refs) + if (ops.returned_values.validate_unused_refs != null) (ns_file.validate_unused_refs as jest.Mock).mockReturnValue(ops.returned_values.validate_unused_refs) + if (ops.returned_values.validate_parameter_refs != null) (ns_file.validate_parameter_refs as jest.Mock).mockReturnValue(ops.returned_values.validate_parameter_refs) } return ns_file diff --git a/tools/test/linter/factories/operation.ts b/tools/test/linter/factories/operation.ts index 42791ce12..f17e092cb 100644 --- a/tools/test/linter/factories/operation.ts +++ b/tools/test/linter/factories/operation.ts @@ -1,24 +1,25 @@ import Operation from '../../../linter/components/Operation' import { type OperationSpec } from '../../../types' -export function operation (spec: Record, file_name = 'indices.yaml') { +export function operation (spec: Record, file_name = 'indices.yaml'): Operation { return new Operation(`namespaces/${file_name}`, '/{index}/something/{abc_xyz}', 'post', spec as OperationSpec) } interface MockedReturnedValues { validate?: string[] - validate_group?: string | void - validate_namespace?: string | void - validate_operationId?: string | void - validate_description?: string | void - validate_requestBody?: string | void + validate_group?: string | undefined + validate_namespace?: string | undefined + validate_operationId?: string | undefined + validate_description?: string | undefined + validate_requestBody?: string | undefined validate_responses?: string[] - validate_parameters?: string | void - validate_path_parameters?: string | void + validate_parameters?: string | undefined + validate_path_parameters?: string | undefined } -export function mocked_operation (returned_values: MockedReturnedValues) { - const op = new Operation('', '', '', {} as OperationSpec) +export function mocked_operation (returned_values: MockedReturnedValues): Operation { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const op: Operation = new Operation('', '', '', {} as OperationSpec) if (returned_values.validate) { op.validate = jest.fn(); @@ -28,21 +29,21 @@ export function mocked_operation (returned_values: MockedReturnedValues) { op.validate_group = jest.fn() op.validate_namespace = jest.fn() - op.validate_operationId = jest.fn() + op.validate_operation_id = jest.fn() op.validate_description = jest.fn() - op.validate_requestBody = jest.fn() + op.validate_request_body = jest.fn() op.validate_responses = jest.fn() op.validate_parameters = jest.fn() op.validate_path_parameters = jest.fn() - if (returned_values.validate_group) (op.validate_group as jest.Mock).mockReturnValue(returned_values.validate_group) - if (returned_values.validate_namespace) (op.validate_namespace as jest.Mock).mockReturnValue(returned_values.validate_namespace) - if (returned_values.validate_operationId) (op.validate_operationId as jest.Mock).mockReturnValue(returned_values.validate_operationId) - if (returned_values.validate_description) (op.validate_description as jest.Mock).mockReturnValue(returned_values.validate_description) - if (returned_values.validate_requestBody) (op.validate_requestBody as jest.Mock).mockReturnValue(returned_values.validate_requestBody) - if (returned_values.validate_responses) (op.validate_responses as jest.Mock).mockReturnValue(returned_values.validate_responses) - if (returned_values.validate_parameters) (op.validate_parameters as jest.Mock).mockReturnValue(returned_values.validate_parameters) - if (returned_values.validate_path_parameters) (op.validate_path_parameters as jest.Mock).mockReturnValue(returned_values.validate_path_parameters) + if (returned_values.validate_group != null) (op.validate_group as jest.Mock).mockReturnValue(returned_values.validate_group) + if (returned_values.validate_namespace != null) (op.validate_namespace as jest.Mock).mockReturnValue(returned_values.validate_namespace) + if (returned_values.validate_operationId != null) (op.validate_operation_id as jest.Mock).mockReturnValue(returned_values.validate_operationId) + if (returned_values.validate_description != null) (op.validate_description as jest.Mock).mockReturnValue(returned_values.validate_description) + if (returned_values.validate_requestBody != null) (op.validate_request_body as jest.Mock).mockReturnValue(returned_values.validate_requestBody) + if (returned_values.validate_responses != null) (op.validate_responses as jest.Mock).mockReturnValue(returned_values.validate_responses) + if (returned_values.validate_parameters != null) (op.validate_parameters as jest.Mock).mockReturnValue(returned_values.validate_parameters) + if (returned_values.validate_path_parameters != null) (op.validate_path_parameters as jest.Mock).mockReturnValue(returned_values.validate_path_parameters) return op } diff --git a/tools/test/linter/factories/operation_group.ts b/tools/test/linter/factories/operation_group.ts index 2ac53b6d1..1439a6834 100644 --- a/tools/test/linter/factories/operation_group.ts +++ b/tools/test/linter/factories/operation_group.ts @@ -10,14 +10,14 @@ export function operation_group (operation_specs: Array>): O interface MockedReturnedValues { validate?: string[] - validate_description?: string | void - validate_externalDocs?: string | void - validate_requestBody?: string | void - validate_responses?: string | void - validate_query_parameters?: string | void + validate_description?: string | undefined + validate_externalDocs?: string | undefined + validate_requestBody?: string | undefined + validate_responses?: string | undefined + validate_query_parameters?: string | undefined } -export function mocked_operation_group (returned_values: MockedReturnedValues, ops_errors: string[][] = []) { +export function mocked_operation_group (returned_values: MockedReturnedValues, ops_errors: string[][] = []): OperationGroup { const ops = ops_errors.map((errors) => mocked_operation({ validate: errors })) const op_group = new OperationGroup('', '', ops) @@ -28,16 +28,16 @@ export function mocked_operation_group (returned_values: MockedReturnedValues, o } op_group.validate_description = jest.fn() - op_group.validate_externalDocs = jest.fn() - op_group.validate_requestBody = jest.fn() + op_group.validate_external_docs = jest.fn() + op_group.validate_request_body = jest.fn() op_group.validate_responses = jest.fn() op_group.validate_query_parameters = jest.fn() - if (returned_values.validate_description) (op_group.validate_description as jest.Mock).mockReturnValue(returned_values.validate_description) - if (returned_values.validate_externalDocs) (op_group.validate_externalDocs as jest.Mock).mockReturnValue(returned_values.validate_externalDocs) - if (returned_values.validate_requestBody) (op_group.validate_requestBody as jest.Mock).mockReturnValue(returned_values.validate_requestBody) - if (returned_values.validate_responses) (op_group.validate_responses as jest.Mock).mockReturnValue(returned_values.validate_responses) - if (returned_values.validate_query_parameters) (op_group.validate_query_parameters as jest.Mock).mockReturnValue(returned_values.validate_query_parameters) + if (returned_values.validate_description != null) (op_group.validate_description as jest.Mock).mockReturnValue(returned_values.validate_description) + if (returned_values.validate_externalDocs != null) (op_group.validate_external_docs as jest.Mock).mockReturnValue(returned_values.validate_externalDocs) + if (returned_values.validate_requestBody != null) (op_group.validate_request_body as jest.Mock).mockReturnValue(returned_values.validate_requestBody) + if (returned_values.validate_responses != null) (op_group.validate_responses as jest.Mock).mockReturnValue(returned_values.validate_responses) + if (returned_values.validate_query_parameters != null) (op_group.validate_query_parameters as jest.Mock).mockReturnValue(returned_values.validate_query_parameters) return op_group } diff --git a/tools/test/linter/factories/schema.ts b/tools/test/linter/factories/schema.ts index 1176ced67..3eb32c943 100644 --- a/tools/test/linter/factories/schema.ts +++ b/tools/test/linter/factories/schema.ts @@ -7,11 +7,11 @@ export function schema (name: string, spec: Record = {}): Schema { interface MockedReturnedValues { validate?: string[] - validate_name?: string | void + validate_name?: string | undefined } -export function mocked_schema (returned_values: MockedReturnedValues) { - const schema = new Schema('_common.yaml', 'Schema', {} as OpenAPIV3.SchemaObject) +export function mocked_schema (returned_values: MockedReturnedValues): Schema { + const schema = new Schema('_common.yaml', 'Schema', {}) if (returned_values.validate) { schema.validate = jest.fn(); @@ -21,7 +21,7 @@ export function mocked_schema (returned_values: MockedReturnedValues) { schema.validate_name = jest.fn() - if (returned_values.validate_name) (schema.validate_name as jest.Mock).mockReturnValue(returned_values.validate_name) + if (returned_values.validate_name != null) (schema.validate_name as jest.Mock).mockReturnValue(returned_values.validate_name) return schema } diff --git a/tools/test/linter/factories/schema_file.ts b/tools/test/linter/factories/schema_file.ts index 3c653c2c5..6d232211a 100644 --- a/tools/test/linter/factories/schema_file.ts +++ b/tools/test/linter/factories/schema_file.ts @@ -7,12 +7,13 @@ export function schema_file (fixture: string): SchemaFile { interface MockedReturnedValues { validate?: string[] - validate_category?: string | void + validate_category?: string | undefined } export function mocked_schema_file (ops: { returned_values?: MockedReturnedValues, schema_errors?: string[][] }): SchemaFile { const validator = schema_file('_common.empty.yaml') - if (ops.schema_errors) validator._schemas = ops.schema_errors.map((errors) => mocked_schema({ validate: errors })) + // eslint-disable-next-line @typescript-eslint/dot-notation + if (ops.schema_errors) validator['_schemas'] = ops.schema_errors.map((errors) => mocked_schema({ validate: errors })) if (ops.returned_values) { if (ops.returned_values.validate) { @@ -23,7 +24,7 @@ export function mocked_schema_file (ops: { returned_values?: MockedReturnedValue validator.validate_category = jest.fn() - if (ops.returned_values.validate_category) (validator.validate_category as jest.Mock).mockReturnValue(ops.returned_values.validate_category) + if (ops.returned_values.validate_category != null) (validator.validate_category as jest.Mock).mockReturnValue(ops.returned_values.validate_category) } return validator diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/cat.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/cat.yaml similarity index 100% rename from tools/test/linter/fixtures/folder_validators/namespaces/cat.yaml rename to tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/cat.yaml diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/dup_path_a.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/dup_path_a.yaml similarity index 100% rename from tools/test/linter/fixtures/folder_validators/namespaces/dup_path_a.yaml rename to tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/dup_path_a.yaml diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/dup_path_b.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/dup_path_b.yaml similarity index 100% rename from tools/test/linter/fixtures/folder_validators/namespaces/dup_path_b.yaml rename to tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/dup_path_b.yaml diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/dup_path_c.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/dup_path_c.yaml similarity index 100% rename from tools/test/linter/fixtures/folder_validators/namespaces/dup_path_c.yaml rename to tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/dup_path_c.yaml diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/indices.txt b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/indices.txt similarity index 100% rename from tools/test/linter/fixtures/folder_validators/namespaces/indices.txt rename to tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/indices.txt diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/invalid_spec.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/invalid_spec.yaml similarity index 100% rename from tools/test/linter/fixtures/folder_validators/namespaces/invalid_spec.yaml rename to tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/invalid_spec.yaml diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/invalid_yaml.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/invalid_yaml.yaml similarity index 100% rename from tools/test/linter/fixtures/folder_validators/namespaces/invalid_yaml.yaml rename to tools/test/linter/fixtures/folder_validators/namespaces/invalid_files/invalid_yaml.yaml diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/cat.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/cat.yaml new file mode 100644 index 000000000..b6c637735 --- /dev/null +++ b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/cat.yaml @@ -0,0 +1,27 @@ +paths: + '/_cat/aliases/{name}': + get: + x-operation-group: cat.aliases + operationId: cat.aliases.0 + description: 'CAT aliases.' + responses: + '200': + $ref: '#/components/responses/cat.aliases@200' + parameters: + - $ref: '#/components/parameters/cat.aliases::path.name' + - $ref: '#/components/parameters/cat.aliases::query.format' + - $ref: '#/components/parameters/cat.aliases::query.local' + +components: + responses: + cat.aliases@200: {} + parameters: + cat.aliases::path.name: + name: name + in: path + cat.aliases::query.format: + name: format + in: query + cat.aliases::query.local: + name: local + in: query \ No newline at end of file diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_a.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_a.yaml new file mode 100644 index 000000000..c0feea716 --- /dev/null +++ b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_a.yaml @@ -0,0 +1,4 @@ +paths: + '/{index}': {} + '/{index}/_clone': {} + '/{index}/_rollover': {} \ No newline at end of file diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_b.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_b.yaml new file mode 100644 index 000000000..6952dddeb --- /dev/null +++ b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_b.yaml @@ -0,0 +1,3 @@ +paths: + '/_rollover': {} + '/{index}/_rollover': {} \ No newline at end of file diff --git a/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_c.yaml b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_c.yaml new file mode 100644 index 000000000..30d77011d --- /dev/null +++ b/tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_c.yaml @@ -0,0 +1,3 @@ +paths: + '/{index}': {} + '/{index}/_rollover': {} \ No newline at end of file diff --git a/tools/test/linter/fixtures/path_refs_validator/namespaces/cat.yaml b/tools/test/linter/fixtures/path_refs_validator/namespaces/cat.yaml deleted file mode 100644 index 983c3de46..000000000 --- a/tools/test/linter/fixtures/path_refs_validator/namespaces/cat.yaml +++ /dev/null @@ -1,3 +0,0 @@ -paths: - '/_cat/aliases': {} - '/_cat/allocation': {} \ No newline at end of file diff --git a/tools/test/linter/fixtures/path_refs_validator/namespaces/indices.yaml b/tools/test/linter/fixtures/path_refs_validator/namespaces/indices.yaml deleted file mode 100644 index e5481a420..000000000 --- a/tools/test/linter/fixtures/path_refs_validator/namespaces/indices.yaml +++ /dev/null @@ -1,3 +0,0 @@ -paths: - '/{index}/_alias/{name}' : {} - '/{index}/_aliases' : {} \ No newline at end of file diff --git a/tools/test/linter/fixtures/path_refs_validator/namespaces/missing.yaml b/tools/test/linter/fixtures/path_refs_validator/namespaces/missing.yaml deleted file mode 100644 index d64fbea98..000000000 --- a/tools/test/linter/fixtures/path_refs_validator/namespaces/missing.yaml +++ /dev/null @@ -1,3 +0,0 @@ -paths: - '/_missing': {} - '/_missing/{id}': {} \ No newline at end of file diff --git a/tools/test/linter/fixtures/path_refs_validator/opensearch-openapi.yaml b/tools/test/linter/fixtures/path_refs_validator/opensearch-openapi.yaml deleted file mode 100644 index 4ea72a813..000000000 --- a/tools/test/linter/fixtures/path_refs_validator/opensearch-openapi.yaml +++ /dev/null @@ -1,13 +0,0 @@ -paths: - '/{index}': - $ref: 'namespaces/indices.yaml#/paths/~1{index}' - '/{index}/_alias/{name}': - $ref: 'namespaces/indices.yaml#/paths/~1{index}~1_alias~1{name}' - '/_cat/aliases': - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1aliases' - '/_cat/allocation': - $ref: 'namespaces/cat.yaml#/paths/~1_cat~1allocation' - '/_cluster/health': - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1health' - '/_cluster/{id}': - $ref: 'namespaces/cluster.yaml#/paths/~1_cluster~1{id}' \ No newline at end of file diff --git a/tools/test/linter/fixtures/root.yaml b/tools/test/linter/fixtures/root.yaml deleted file mode 100644 index 8c4a08750..000000000 --- a/tools/test/linter/fixtures/root.yaml +++ /dev/null @@ -1,32 +0,0 @@ -paths: - '/': {} - '/_cat': - $ref: './namespaces/cat.yaml#/paths/_cat' - '/{index}': - get: - responses: - '200': - $ref: '#/components/responses/indices.create@200' - parameters: - - $ref: '#/components/parameters/indices.create::path.index' - - $ref: '#/components/parameters/indices.create::query.pretty' -components: - parameters: - _global::query.pretty: - x-global: true - name: beautify - in: query - schema: - type: boolean - _global::query.human: - x-global: false - name: human - in: query - schema: - type: boolean - _global::query.error_trace: - x-global: true - name: error_trace - in: query - schema: - type: boolean \ No newline at end of file diff --git a/tools/test/merger/OpenApiMerger.test.ts b/tools/test/merger/OpenApiMerger.test.ts index 0e2513dd3..22d966404 100644 --- a/tools/test/merger/OpenApiMerger.test.ts +++ b/tools/test/merger/OpenApiMerger.test.ts @@ -2,7 +2,7 @@ import OpenApiMerger from '../../merger/OpenApiMerger' import fs from 'fs' test('merge()', async () => { - const merger = new OpenApiMerger('./test/merger/fixtures/spec/opensearch-openapi.yaml') + const merger = new OpenApiMerger('./test/merger/fixtures/spec/') merger.merge('./test/merger/opensearch-openapi.yaml') expect(fs.readFileSync('./test/merger/fixtures/expected.yaml', 'utf8')) .toEqual(fs.readFileSync('./test/merger/opensearch-openapi.yaml', 'utf8')) diff --git a/tools/test/merger/fixtures/expected.yaml b/tools/test/merger/fixtures/expected.yaml index 95772eb3b..3c73f80a1 100644 --- a/tools/test/merger/fixtures/expected.yaml +++ b/tools/test/merger/fixtures/expected.yaml @@ -4,6 +4,17 @@ info: 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 @@ -40,13 +51,13 @@ paths: components: parameters: _global::query.human: - x-global: true 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 diff --git a/tools/test/merger/fixtures/spec/_global_parameters.yaml b/tools/test/merger/fixtures/spec/_global_parameters.yaml new file mode 100644 index 000000000..cf491eee2 --- /dev/null +++ b/tools/test/merger/fixtures/spec/_global_parameters.yaml @@ -0,0 +1,13 @@ +openapi: 3.1.0 +info: + title: '' + version: '' +components: + parameters: + human: + name: human + in: query + description: Whether to return human readable values for statistics. + schema: + type: boolean + default: true \ No newline at end of file diff --git a/tools/test/merger/fixtures/spec/_info.yaml b/tools/test/merger/fixtures/spec/_info.yaml new file mode 100644 index 000000000..9da26bfa0 --- /dev/null +++ b/tools/test/merger/fixtures/spec/_info.yaml @@ -0,0 +1,3 @@ +title: OpenSearch API +description: OpenSearch API +version: 1.0.0 \ No newline at end of file diff --git a/tools/test/merger/fixtures/spec/namespaces/ignored.yaml b/tools/test/merger/fixtures/spec/namespaces/indices.yaml similarity index 100% rename from tools/test/merger/fixtures/spec/namespaces/ignored.yaml rename to tools/test/merger/fixtures/spec/namespaces/indices.yaml diff --git a/tools/test/merger/fixtures/spec/opensearch-openapi.yaml b/tools/test/merger/fixtures/spec/opensearch-openapi.yaml deleted file mode 100644 index b9bdd719d..000000000 --- a/tools/test/merger/fixtures/spec/opensearch-openapi.yaml +++ /dev/null @@ -1,18 +0,0 @@ -openapi: 3.1.0 -info: - title: OpenSearch API - description: OpenSearch API - version: 1.0.0 -paths: - '/adopt/{animal}/dockets/{docket}': - $ref: 'namespaces/shelter.yaml#/paths/~1adopt~1{animal}~1dockets~1{docket}' -components: - parameters: - _global::query.human: - x-global: true - name: human - in: query - description: Whether to return human readable values for statistics. - schema: - type: boolean - default: true \ No newline at end of file diff --git a/tools/types.ts b/tools/types.ts index 8ad814e56..083a05f72 100644 --- a/tools/types.ts +++ b/tools/types.ts @@ -10,7 +10,7 @@ export interface OperationSpec extends OpenAPIV3.OperationObject { parameters?: OpenAPIV3.ReferenceObject[] requestBody?: OpenAPIV3.ReferenceObject - responses: { [code: string]: OpenAPIV3.ReferenceObject } + responses: Record } export interface ParameterSpec extends OpenAPIV3.ParameterObject {