From 09fbfd92c9de2fabd88c5d93a1945398b2c2fe14 Mon Sep 17 00:00:00 2001 From: Theo Truong Date: Wed, 1 May 2024 10:20:13 -0600 Subject: [PATCH] Fixed Linting for Tools Signed-off-by: Theo Truong --- tools/eslint.config.mjs | 57 +++++++++++------ tools/helpers.ts | 27 ++++---- tools/linter/PathRefsValidator.ts | 17 ++--- tools/linter/SchemaRefsValidator.ts | 42 ++++++------ tools/linter/components/NamespaceFile.ts | 28 ++++---- tools/linter/components/NamespacesFolder.ts | 8 +-- tools/linter/components/Operation.ts | 47 +++++++------- tools/linter/components/OperationGroup.ts | 22 +++---- tools/linter/components/RootFile.ts | 9 +-- tools/linter/components/SchemaFile.ts | 11 ++-- .../components/SupersededOperationsFile.ts | 2 +- tools/linter/components/base/FileValidator.ts | 9 +-- .../linter/components/base/FolderValidator.ts | 11 ++-- tools/linter/lint.ts | 4 +- tools/merger/OpenApiMerger.ts | 64 ++++++++++--------- tools/merger/OpenDistro.ts | 6 +- tools/merger/SupersededOpsGenerator.ts | 8 +-- tools/package.json | 4 +- tools/test/linter/NamespacesFolder.test.ts | 28 ++++---- tools/test/linter/Operation.test.ts | 12 ++-- tools/test/linter/OperationGroup.test.ts | 20 +++--- tools/test/linter/factories/namespace_file.ts | 20 +++--- tools/test/linter/factories/operation.ts | 41 ++++++------ .../test/linter/factories/operation_group.ts | 26 ++++---- tools/test/linter/factories/schema.ts | 8 +-- tools/test/linter/factories/schema_file.ts | 7 +- .../namespaces/{ => invalid_files}/cat.yaml | 0 .../{ => invalid_files}/dup_path_a.yaml | 0 .../{ => invalid_files}/dup_path_b.yaml | 0 .../{ => invalid_files}/dup_path_c.yaml | 0 .../{ => invalid_files}/indices.txt | 0 .../{ => invalid_files}/invalid_spec.yaml | 0 .../{ => invalid_files}/invalid_yaml.yaml | 0 .../namespaces/invalid_folder/cat.yaml | 27 ++++++++ .../namespaces/invalid_folder/dup_path_a.yaml | 4 ++ .../namespaces/invalid_folder/dup_path_b.yaml | 3 + .../namespaces/invalid_folder/dup_path_c.yaml | 3 + tools/types.ts | 2 +- 38 files changed, 327 insertions(+), 250 deletions(-) rename tools/test/linter/fixtures/folder_validators/namespaces/{ => invalid_files}/cat.yaml (100%) rename tools/test/linter/fixtures/folder_validators/namespaces/{ => invalid_files}/dup_path_a.yaml (100%) rename tools/test/linter/fixtures/folder_validators/namespaces/{ => invalid_files}/dup_path_b.yaml (100%) rename tools/test/linter/fixtures/folder_validators/namespaces/{ => invalid_files}/dup_path_c.yaml (100%) rename tools/test/linter/fixtures/folder_validators/namespaces/{ => invalid_files}/indices.txt (100%) rename tools/test/linter/fixtures/folder_validators/namespaces/{ => invalid_files}/invalid_spec.yaml (100%) rename tools/test/linter/fixtures/folder_validators/namespaces/{ => invalid_files}/invalid_yaml.yaml (100%) create mode 100644 tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/cat.yaml create mode 100644 tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_a.yaml create mode 100644 tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_b.yaml create mode 100644 tools/test/linter/fixtures/folder_validators/namespaces/invalid_folder/dup_path_c.yaml 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..4f3301930 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,43 @@ 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 write_to_yaml (file_path: string, content: Record): void { + fs.writeFileSync(file_path, quote_refs(YAML.stringify(remove_anchors(content), { lineWidth: 0, singleQuote: true }))) } -function quoteRefs (str: string): string { +function quote_refs (str: string): string { return str.split('\n').map((line) => { if (line.includes('$ref')) { const [key, value] = line.split(': ') @@ -56,7 +57,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 index e8d464b40..2ebb53a68 100644 --- a/tools/linter/PathRefsValidator.ts +++ b/tools/linter/PathRefsValidator.ts @@ -16,18 +16,19 @@ export default class PathRefsValidator { this.#build_available_paths() } - #build_referenced_paths () { + #build_referenced_paths (): void { for (const [path, spec] of Object.entries(this.root_file.spec().paths)) { - const ref = spec!.$ref! + const ref = spec?.$ref ?? '' const file = ref.split('#')[0] - if (!this.referenced_paths[file]) this.referenced_paths[file] = new Set() + const ref_path = this.referenced_paths[file] as Set | undefined + if (!ref_path) this.referenced_paths[file] = new Set() this.referenced_paths[file].add(path) } } - #build_available_paths () { + #build_available_paths (): void { for (const file of this.namespaces_folder.files) { - this.available_paths[file.file] = new Set(Object.keys(file.spec().paths || {})) + this.available_paths[file.file] = new Set(Object.keys(file.spec().paths ?? {})) } } @@ -40,7 +41,7 @@ export default class PathRefsValidator { validate_unresolved_refs (): ValidationError[] { return Object.entries(this.referenced_paths).flatMap(([ref_file, ref_paths]) => { - const available = this.available_paths[ref_file] + const available = this.available_paths[ref_file] as Set | undefined if (!available) { return { file: this.root_file.file, @@ -63,7 +64,7 @@ export default class PathRefsValidator { validate_unreferenced_paths (): ValidationError[] { return Object.entries(this.available_paths).flatMap(([ns_file, ns_paths]) => { - const referenced = this.referenced_paths[ns_file] + const referenced = this.referenced_paths[ns_file] as Set | undefined if (!referenced) { return { file: ns_file, @@ -71,7 +72,7 @@ export default class PathRefsValidator { } } return Array.from(ns_paths).map((path) => { - if (!referenced || !referenced.has(path)) { + if (!referenced?.has(path)) { return { file: ns_file, location: `Path: ${path}`, 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/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 index 3cfd9b7ae..4ee9858d9 100644 --- a/tools/linter/components/RootFile.ts +++ b/tools/linter/components/RootFile.ts @@ -4,7 +4,7 @@ import FileValidator from './base/FileValidator' export default class RootFile extends FileValidator { constructor (file_path: string) { super(file_path) - this.file = file_path.split('/').pop()! + this.file = file_path.split('/').pop() ?? '' } validate_file (): ValidationError[] { @@ -16,16 +16,17 @@ export default class RootFile extends FileValidator { 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}`) } + if (spec?.$ref != null) return undefined + 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 + 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}`) } + if (param['x-global'] !== true) { 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..9f9252aba 100644 --- a/tools/linter/components/SupersededOperationsFile.ts +++ b/tools/linter/components/SupersededOperationsFile.ts @@ -5,7 +5,7 @@ import YAML from 'yaml' import { type ValidationError } from '../../types' 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 [ diff --git a/tools/linter/components/base/FileValidator.ts b/tools/linter/components/base/FileValidator.ts index 6f5d234aa..b01d59d17 100644 --- a/tools/linter/components/base/FileValidator.ts +++ b/tools/linter/components/base/FileValidator.ts @@ -6,7 +6,7 @@ import { type OpenAPIV3 } from 'openapi-types' 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 +15,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 = YAML.parse(fs.readFileSync(this.file_path, 'utf8')) 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 +27,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/OpenApiMerger.ts b/tools/merger/OpenApiMerger.ts index f76731aae..a35f065ea 100644 --- a/tools/merger/OpenApiMerger.ts +++ b/tools/merger/OpenApiMerger.ts @@ -2,7 +2,7 @@ import { type OpenAPIV3 } from 'openapi-types' import fs from 'fs' import _ from 'lodash' import yaml from 'yaml' -import { write2file } from '../helpers' +import { write_to_yaml } from '../helpers' import SupersededOpsGenerator from './SupersededOpsGenerator' // Create a single-file OpenAPI spec from multiple files for OpenAPI validation and programmatic consumption @@ -19,7 +19,7 @@ export default class OpenApiMerger { 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 || {} + 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, @@ -29,22 +29,22 @@ export default class OpenApiMerger { } } - 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() - if (output_path) write2file(output_path, this.spec) + if (output_path !== '') write_to_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 || [] + Object.entries(this.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(this.global_param_refs)] }) }) @@ -57,22 +57,23 @@ export default class OpenApiMerger { const spec = yaml.parse(fs.readFileSync(`${folder}/${file}`, 'utf8')) const namespace = file.split('.yaml')[0] 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'] } + 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] + Object.entries(this.spec.paths as Document).forEach(([path, ref_obj]) => { + const ref: string = (ref_obj as Record).$ref ?? '' + const namespace = ref.match(/namespaces\/(.*)\.yaml/)?.[1] ?? '' + if (namespace === '') throw new Error(`Invalid path reference: ${ref}`) this.spec.paths[path] = this.paths[namespace][path] }) } // 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) { @@ -87,22 +88,23 @@ export default class OpenApiMerger { const spec = yaml.parse(fs.readFileSync(`${folder}/${file}`, 'utf8')) 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,14 +116,14 @@ 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]))) }) } diff --git a/tools/merger/OpenDistro.ts b/tools/merger/OpenDistro.ts index 4014c3ccc..126e3b5eb 100644 --- a/tools/merger/OpenDistro.ts +++ b/tools/merger/OpenDistro.ts @@ -1,7 +1,7 @@ import fs from 'fs' import YAML from 'yaml' import { type HttpVerb, type OperationPath, type SupersededOperationMap } from '../types' -import { write2file } from '../helpers' +import { write_to_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. @@ -13,10 +13,10 @@ export default class OpenDistro { constructor (file_path: string) { this.input = YAML.parse(fs.readFileSync(file_path, 'utf8')) this.build_output() - write2file(file_path, this.output) + write_to_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..826a7eab0 100644 --- a/tools/merger/SupersededOpsGenerator.ts +++ b/tools/merger/SupersededOpsGenerator.ts @@ -17,13 +17,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 +44,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/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/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/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 {