Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate order of superseded operations. #326

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Added `npm run test:unit` and `test:integ` ([#320](https://github.com/opensearch-project/opensearch-api-specification/pull/320))
- Added code coverage to tools' tests ([#323](https://github.com/opensearch-project/opensearch-api-specification/pull/323))
- Added a YAML linter ([#312](https://github.com/opensearch-project/opensearch-api-specification/pull/312))

- Added linter to validate order of spec operations ([#325](https://github.com/opensearch-project/opensearch-api-specification/pull/326)) ([#326](https://github.com/opensearch-project/opensearch-api-specification/pull/326))

### Changed

- Replaced Smithy with a native OpenAPI spec ([#189](https://github.com/opensearch-project/opensearch-api-specification/issues/189))
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Code Covergage](https://codecov.io/github/opensearch-project/opensearch-api-specification/graph/badge.svg?token=TO9YMAKSHH)](https://codecov.io/github/opensearch-project/opensearch-api-specification)
[![Test Tools (Unit)](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test-tools-unit.yml/badge.svg)](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test-tools-unit.yml)
[![Test Tools (Integration)](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test-tools-integ.yml/badge.svg)](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test-tools-integ.yml)
[![Test Spec](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test.yml/badge.svg)](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test.yml)
[![Test Spec](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test-spec.yml/badge.svg)](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/test-spec.yml)
[![Validate Spec](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/validate-spec.yml/badge.svg)](https://github.com/opensearch-project/opensearch-api-specification/actions/workflows/validate-spec.yml)

- [OpenSearch API Specification](#opensearch-api-specification)
Expand Down
18 changes: 9 additions & 9 deletions spec/_superseded_operations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
superseded_by: /_plugins/_alerting/monitors/{monitorID}
operations:
- GET
- HEAD
- PUT
- DELETE
- HEAD
/_opendistro/_alerting/monitors/{monitorID}/_acknowledge/alerts:
superseded_by: /_plugins/_alerting/monitors/{monitorID}/_acknowledge/alerts
operations:
Expand Down Expand Up @@ -107,9 +107,9 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
superseded_by: /_plugins/_anomaly_detection/detectors/{detectorID}
operations:
- GET
- HEAD
- PUT
- DELETE
- HEAD
/_opendistro/_anomaly_detection/detectors/{detectorID}/_preview:
superseded_by: /_plugins/_anomaly_detection/detectors/{detectorID}/_preview
operations:
Expand Down Expand Up @@ -200,9 +200,9 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
superseded_by: /_plugins/_ism/policies/{policyID}
operations:
- GET
- HEAD
- PUT
- DELETE
- HEAD
/_opendistro/_ism/remove:
superseded_by: /_plugins/_ism/remove
operations:
Expand Down Expand Up @@ -353,9 +353,9 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
superseded_by: /_plugins/_rollup/jobs/{rollupID}
operations:
- GET
- HEAD
- PUT
- DELETE
- HEAD
/_opendistro/_rollup/jobs/{rollupID}/_explain:
superseded_by: /_plugins/_rollup/jobs/{rollupID}/_explain
operations:
Expand Down Expand Up @@ -420,8 +420,8 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
superseded_by: /_plugins/_security/api/cache
operations:
- GET
- PUT
- POST
- PUT
- DELETE
/_opendistro/_security/api/internalusers/:
superseded_by: /_plugins/_security/api/internalusers/
Expand All @@ -433,8 +433,8 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
operations:
- GET
- PUT
- DELETE
- PATCH
- DELETE
/_opendistro/_security/api/internalusers/{name}/authtoken:
superseded_by: /_plugins/_security/api/internalusers/{name}/authtoken
operations:
Expand All @@ -457,8 +457,8 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
operations:
- GET
- PUT
- DELETE
- PATCH
- DELETE
/_opendistro/_security/api/rolesmapping/:
superseded_by: /_plugins/_security/api/rolesmapping/
operations:
Expand All @@ -469,8 +469,8 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
operations:
- GET
- PUT
- DELETE
- PATCH
- DELETE
/_opendistro/_security/api/securityconfig:
superseded_by: /_plugins/_security/api/securityconfig
operations:
Expand Down Expand Up @@ -519,8 +519,8 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
operations:
- GET
- PUT
- DELETE
- PATCH
- DELETE
/_opendistro/_security/api/user:
superseded_by: /_plugins/_security/api/user
operations:
Expand Down
2 changes: 1 addition & 1 deletion tools/src/OpenSearchHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { Option } from '@commander-js/extra-typings'
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
import * as https from 'node:https'
import { sleep } from '../helpers'
import { sleep } from './helpers'

const DEFAULT_URL = 'https://localhost:9200'
const DEFAULT_USER = 'admin'
Expand Down
2 changes: 1 addition & 1 deletion tools/src/coverage/CoverageCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { type OpenAPIV3 } from 'openapi-types'
import { HTTP_METHODS, read_yaml, write_json } from '../../helpers'
import { HTTP_METHODS, read_yaml, write_json } from '../helpers'

export default class CoverageCalculator {
private readonly _cluster_spec: OpenAPIV3.Document
Expand Down
2 changes: 1 addition & 1 deletion tools/src/dump-cluster-spec/dump-cluster-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { Command, Option } from '@commander-js/extra-typings'
import { resolve } from 'path'
import * as process from 'node:process'
import { write_yaml } from '../../helpers'
import { write_yaml } from '../helpers'
import {
get_opensearch_opts_from_cli,
OPENSEARCH_INSECURE_OPTION,
Expand Down
12 changes: 12 additions & 0 deletions tools/helpers.ts → tools/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ export function sort_by_keys (obj: Record<string, any>, priorities: string[] = [
})
}

export function sort_array_by_keys (values: any[], priorities: string[] = []): string[] {
const orders = _.fromPairs(priorities.map((k, i) => [k, i + 1]))
return _.clone(values).sort((a, b) => {
const order_a = orders[a]
const order_b = orders[b]
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.localeCompare(b)
})
}

export function ensure_parent_dir (file_path: string): void {
fs.mkdirSync(path.dirname(file_path), { recursive: true })
}
Expand Down
2 changes: 1 addition & 1 deletion tools/src/linter/components/NamespaceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { type OperationSpec, type ValidationError } from 'types'
import OperationGroup from './OperationGroup'
import _ from 'lodash'
import Operation from './Operation'
import { resolve_ref, sort_by_keys } from '../../../helpers'
import { resolve_ref, sort_by_keys } from '../../helpers'
import FileValidator from './base/FileValidator'

const HTTP_METHODS = ['get', 'head', 'post', 'put', 'patch', 'delete', 'options', 'trace']
Expand Down
32 changes: 32 additions & 0 deletions tools/src/linter/components/SupersededOperationsFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,40 @@
* compatible open source license.
*/

import _ from 'lodash'
import { sort_array_by_keys } from '../../helpers'
import FileValidator from './base/FileValidator'
import { type ValidationError } from 'types'
import { type OpenAPIV3 } from 'openapi-types'

const HTTP_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE']

export default class SupersededOperationsFile extends FileValidator {
has_json_schema = true
protected _superseded_ops: OpenAPIV3.Document | undefined

validate (): ValidationError[] {
const schema_validations = super.validate()
if (schema_validations.length > 0) return schema_validations
return this.validate_order_of_operations()
}

superseded_ops (): OpenAPIV3.Document {
if (this._superseded_ops) return this._superseded_ops
this._superseded_ops = this.spec()
delete (this._superseded_ops as any).$schema
return this._superseded_ops
}

validate_order_of_operations (): ValidationError[] {
return _.entries(this.superseded_ops()).map(([path, p]) => {
const current_keys = p.operations
const sorted_keys = sort_array_by_keys(p.operations as string[], HTTP_METHODS)
if(!_.isEqual(current_keys, sorted_keys)) {
return this.error(
`Operations must be sorted. Expected ${_.join(sorted_keys, ', ')}.`,
path)
}
}).filter((e) => e) as ValidationError[]
}
}
2 changes: 1 addition & 1 deletion tools/src/linter/components/base/FileValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import ValidatorBase from './ValidatorBase'
import { type ValidationError } from 'types'
import { type OpenAPIV3 } from 'openapi-types'
import { read_yaml } from '../../../../helpers'
import { read_yaml } from '../../../helpers'
import AJV from 'ajv'
import addFormats from 'ajv-formats'

Expand Down
2 changes: 1 addition & 1 deletion tools/src/merger/GlobalParamsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { type OpenAPIV3 } from 'openapi-types'
import _ from 'lodash'
import { read_yaml } from '../../helpers'
import { read_yaml } from '../helpers'

export default class GlobalParamsGenerator {
global_params: Record<string, OpenAPIV3.ParameterObject>
Expand Down
2 changes: 1 addition & 1 deletion tools/src/merger/OpenApiMerger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { type OpenAPIV3 } from 'openapi-types'
import fs from 'fs'
import _ from 'lodash'
import { read_yaml, write_yaml } from '../../helpers'
import { read_yaml, write_yaml } from '../helpers'
import SupersededOpsGenerator from './SupersededOpsGenerator'
import GlobalParamsGenerator from './GlobalParamsGenerator'
import { Logger } from '../Logger'
Expand Down
2 changes: 1 addition & 1 deletion tools/src/merger/OpenDistro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { type HttpVerb, type OperationPath, type SupersededOperationMap } from 'types'
import { read_yaml, write_yaml } 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.
Expand Down
2 changes: 1 addition & 1 deletion tools/src/merger/SupersededOpsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { type OperationSpec, type SupersededOperationMap } from 'types'
import _ from 'lodash'
import { read_yaml } from '../../helpers'
import { read_yaml } from '../helpers'
import { type Logger } from '../Logger'

export default class SupersededOpsGenerator {
Expand Down
2 changes: 1 addition & 1 deletion tools/src/tester/OperationLocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { type OpenAPIV3 } from 'openapi-types'
import { resolve_ref } from '../../helpers'
import { resolve_ref } from '../helpers'
import { type Chapter } from './types/story.types'
import { type ParsedOperation } from './types/spec.types'
import _ from 'lodash'
Expand Down
2 changes: 1 addition & 1 deletion tools/src/tester/TestRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type StoryEvaluator from './StoryEvaluator'
import { type StoryFile } from './StoryEvaluator'
import fs from 'fs'
import { type Story } from './types/story.types'
import { read_yaml } from '../../helpers'
import { read_yaml } from '../helpers'
import { Result, type StoryEvaluation } from './types/eval.types'
import { type ResultLogger } from './ResultLogger'
import { basename, resolve } from 'path'
Expand Down
2 changes: 1 addition & 1 deletion tools/src/tester/_generate_story_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import * as js2ts from 'json-schema-to-typescript'
import fs from 'fs'
import { read_yaml } from '../../helpers'
import { read_yaml } from '../helpers'

const schema = read_yaml('json_schemas/test_story.schema.yaml')
void js2ts.compile(schema, 'Story',
Expand Down
28 changes: 28 additions & 0 deletions tools/tests/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

import { sort_array_by_keys } from '../src/helpers'

describe('helpers', () => {
describe('sort_array_by_keys', () => {
test('sorts arrays of string', () => {
expect(sort_array_by_keys([])).toEqual([])
expect(sort_array_by_keys(['GET', 'POST'], ['GET', 'POST'])).toEqual(['GET', 'POST'])
expect(sort_array_by_keys(['GET', 'POST'], ['POST', 'GET'])).toEqual(['POST', 'GET'])
expect(sort_array_by_keys(['GET', 'POST'], ['POST', 'GET', 'DELETE'])).toEqual(['POST', 'GET'])
expect(sort_array_by_keys(['DELETE', 'POST', 'GET'], ['POST', 'GET', 'DELETE'])).toEqual(['POST', 'GET', 'DELETE'])
})

test('does not modify the original object', () => {
const arr = ['GET', 'POST']
expect(sort_array_by_keys(arr, ['POST', 'GET'])).toEqual(['POST', 'GET'])
expect(arr).toEqual(['GET', 'POST'] )
})
})
})
2 changes: 1 addition & 1 deletion tools/tests/linter/SchemasValidator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { Logger } from 'Logger'
import SchemasValidator from '../../src/linter/SchemasValidator'
import SchemasValidator from 'linter/SchemasValidator'

test('validate() - named_schemas', () => {
const validator = new SchemasValidator('./tools/tests/linter/fixtures/schemas_validator/named_schemas', new Logger())
Expand Down
49 changes: 41 additions & 8 deletions tools/tests/linter/SupersededOperationsFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,45 @@

import SupersededOperationsFile from 'linter/components/SupersededOperationsFile'

test('validate()', () => {
const validator = new SupersededOperationsFile('./tools/tests/linter/fixtures/_superseded_operations.yaml')
expect(validator.validate()).toEqual([
{
file: 'fixtures/_superseded_operations.yaml',
message: "File content does not match JSON schema found in './json_schemas/_superseded_operations.schema.yaml':\n [\n {\n \"instancePath\": \"/~1hello~1world/operations/1\",\n \"schemaPath\": \"#/patternProperties/%5E~1/properties/operations/items/enum\",\n \"keyword\": \"enum\",\n \"params\": {\n \"allowedValues\": [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"DELETE\",\n \"HEAD\",\n \"OPTIONS\",\n \"PATCH\"\n ]\n },\n \"message\": \"must be equal to one of the allowed values\"\n }\n]"
}
])
describe('validate()', () => {
test('invalid schema', () => {
const validator = new SupersededOperationsFile('./tools/tests/linter/fixtures/superseded_operations/invalid_schema.yaml')
expect(validator.validate()).toEqual([
{
file: 'superseded_operations/invalid_schema.yaml',
message: "File content does not match JSON schema found in './json_schemas/_superseded_operations.schema.yaml':\n " +
JSON.stringify([
{
"instancePath": "/~1hello~1world/operations/1",
"schemaPath": "#/patternProperties/%5E~1/properties/operations/items/enum",
"keyword": "enum",
"params": {
"allowedValues": [
"GET",
"POST",
"PUT",
"DELETE",
"HEAD",
"OPTIONS",
"PATCH"
]
},
"message": "must be equal to one of the allowed values"
}
], null, 2),
},
])
})

test('incorrect order of operations', () => {
const validator = new SupersededOperationsFile('./tools/tests/linter/fixtures/superseded_operations/incorrect_order_of_operations.yaml')
expect(validator.validate()).toEqual([
{
file: 'superseded_operations/incorrect_order_of_operations.yaml',
location: '/world/hello',
message: "Operations must be sorted. Expected GET, HEAD, POST, PUT, PATCH, DELETE."
},
])
})
})

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
$schema: ./json_schemas/_superseded_operations.schema.yaml

/world/hello:
superseded_by: /world/goodbye
operations:
- PATCH
- GET
- DELETE
- HEAD
- POST
- PUT
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ $schema: ./json_schemas/_superseded_operations.schema.yaml
superseded_by: /goodbye/world
operations:
- GET
- CLEAN
- CLEAN
Loading
Loading