diff --git a/package.json b/package.json index dd16c07..0f98c12 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "fast-equals": "4.0.3" }, "devDependencies": { - "@netcracker/qubership-apihub-graphapi": "1.0.8", + "@netcracker/qubership-apihub-graphapi": "feature-performance-optimization", "@netcracker/qubership-apihub-npm-gitflow": "3.1.0", "@types/object-hash": "3.0.6", "@types/jest": "29.5.2", diff --git a/src/path-matcher.ts b/src/path-matcher.ts index fc8040d..833efad 100644 --- a/src/path-matcher.ts +++ b/src/path-matcher.ts @@ -1,7 +1,5 @@ import { isObject, JsonPath } from '@netcracker/qubership-apihub-json-crawl' -export const PREDICATE_ANY_VALUE = Symbol('?') -export const PREDICATE_UNCLOSED_END = Symbol('**') export interface GrepValuePredicate { readonly name: string @@ -11,7 +9,19 @@ export const grepValue = (name: string) => { return { name } as GrepValuePredicate } -export type PathPredicate = (PropertyKey | GrepValuePredicate)[] +export interface ValidateValuePredicate { + readonly validate: (value: PropertyKey) => boolean +} + +export const validateValuePredicate = (validate: (value: PropertyKey) => boolean): ValidateValuePredicate => { + return { validate } +} + +export const PREDICATE_ANY_VALUE = Symbol('?') +export const PREDICATE_UNCLOSED_END = Symbol('**') +export const PREDICATE_NOT_OAS_EXTENSION = validateValuePredicate((value) => !value.toString().startsWith('x-')) + +export type PathPredicate = (PropertyKey | GrepValuePredicate | ValidateValuePredicate)[] export type GrepValues = Record @@ -21,6 +31,14 @@ export type MatchResult = { grepValues: GrepValues } +function isGrepValuePredicate(value: unknown): value is GrepValuePredicate { + return isObject(value) && 'name' in (value as object) +} + +function isValidateValuePredicate(value: unknown): value is ValidateValuePredicate { + return isObject(value) && 'validate' in (value as object) +} + function matchPath(path: JsonPath, predicates: PathPredicate[]): MatchResult | undefined { const predicateMap = new Map(predicates.map((value, index) => [index, value])) const state = path.reduce((state, pathItem, currentIndex) => { @@ -30,10 +48,16 @@ function matchPath(path: JsonPath, predicates: PathPredicate[]): MatchResult | u } const predicateCopy = [...predicate] const currentItemPredicate = predicateCopy.shift() - if (isObject(currentItemPredicate)) { - const name = (currentItemPredicate as GrepValuePredicate).name + if (isGrepValuePredicate(currentItemPredicate)) { + const name = currentItemPredicate.name state.result[name] = pathItem map.set(key, predicateCopy) + } else if (isValidateValuePredicate(currentItemPredicate)) { + if (currentItemPredicate.validate(pathItem)) { + map.set(key, predicateCopy) + } else { + map.delete(key) + } } else { switch (currentItemPredicate) { case PREDICATE_ANY_VALUE: { diff --git a/test/oas/declararetion-path-matcher.test.ts b/test/oas/declararetion-path-matcher.test.ts deleted file mode 100644 index ff72412..0000000 --- a/test/oas/declararetion-path-matcher.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - grepValue, - matchPaths, - OPEN_API_PROPERTY_COMPONENTS, - OPEN_API_PROPERTY_CONTENT, - OPEN_API_PROPERTY_DESCRIPTION, - OPEN_API_PROPERTY_EXAMPLE, - OPEN_API_PROPERTY_HEADERS, - OPEN_API_PROPERTY_PARAMETERS, - OPEN_API_PROPERTY_PATHS, - OPEN_API_PROPERTY_RESPONSES, - PREDICATE_ANY_VALUE, - PREDICATE_UNCLOSED_END -} from '../../src' - -describe('Declaration Path Matcher', () => { - it('Not matched', () => { - const matchResult = matchPaths( - [ - [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_PARAMETERS, 'two', OPEN_API_PROPERTY_DESCRIPTION], - [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'one', OPEN_API_PROPERTY_DESCRIPTION], - ], - [ - [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_HEADERS, grepValue('parameterName'), OPEN_API_PROPERTY_DESCRIPTION] - ] - ) - expect(matchResult).toBe(undefined) - }) - - it('Matched', () => { - const matchResult = matchPaths( - [ - [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_PARAMETERS, 'two', OPEN_API_PROPERTY_DESCRIPTION], - [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'one', OPEN_API_PROPERTY_DESCRIPTION], - ], - [ - [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, grepValue('parameterName'), OPEN_API_PROPERTY_DESCRIPTION] - ] - ) - expect(matchResult).toHaveProperty('grepValues.parameterName', 'one') - }) - - it('Any value matched', () => { - const matchResult = matchPaths( - [ - [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_HEADERS, 'two', OPEN_API_PROPERTY_CONTENT], - [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'one', OPEN_API_PROPERTY_DESCRIPTION], - ], - [ - [PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, grepValue('parameterName')] - ] - ) - expect(matchResult).toHaveProperty('grepValues.parameterName', 'description') - }) - - it('Many Property Matching', () => { - const matchResult = matchPaths( - [ - [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_RESPONSES, '404', OPEN_API_PROPERTY_CONTENT, 'jsonType', OPEN_API_PROPERTY_EXAMPLE, 'param1'], - ], - [ - [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_RESPONSES, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE, grepValue('mediaType'), OPEN_API_PROPERTY_EXAMPLE, grepValue('example')], - [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, grepValue('scope')], - ] - ) - expect(matchResult).toHaveProperty('grepValues.scope', 'responses') - expect(matchResult).toHaveProperty('grepValues.mediaType', 'jsonType') - expect(matchResult).toHaveProperty('grepValues.example', 'param1') - }) - - it('True predicate after matching', () => { - const suitablePredicate = [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_UNCLOSED_END] - const matchResult = matchPaths( - [ - [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE], - ], - [ - [OPEN_API_PROPERTY_PATHS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE], - suitablePredicate, - [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE] - ] - ) - expect(matchResult).toHaveProperty('predicate', suitablePredicate) - }) -}) diff --git a/test/oas/declaration-path-matcher.test.ts b/test/oas/declaration-path-matcher.test.ts new file mode 100644 index 0000000..1540580 --- /dev/null +++ b/test/oas/declaration-path-matcher.test.ts @@ -0,0 +1,169 @@ +import { + grepValue, + matchPaths, + OPEN_API_PROPERTY_COMPONENTS, + OPEN_API_PROPERTY_CONTENT, + OPEN_API_PROPERTY_DESCRIPTION, + OPEN_API_PROPERTY_EXAMPLE, + OPEN_API_PROPERTY_HEADERS, + OPEN_API_PROPERTY_PARAMETERS, + OPEN_API_PROPERTY_PATHS, + OPEN_API_PROPERTY_RESPONSES, + PREDICATE_ANY_VALUE, + PREDICATE_UNCLOSED_END, + PREDICATE_NOT_OAS_EXTENSION, + validateValuePredicate +} from '../../src' + +describe('Declaration Path Matcher', () => { + it('Not matched', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_PARAMETERS, 'two', OPEN_API_PROPERTY_DESCRIPTION], + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'one', OPEN_API_PROPERTY_DESCRIPTION], + ], + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_HEADERS, grepValue('parameterName'), OPEN_API_PROPERTY_DESCRIPTION] + ] + ) + expect(matchResult).toBe(undefined) + }) + + it('Matched', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_PARAMETERS, 'two', OPEN_API_PROPERTY_DESCRIPTION], + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'one', OPEN_API_PROPERTY_DESCRIPTION], + ], + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, grepValue('parameterName'), OPEN_API_PROPERTY_DESCRIPTION] + ] + ) + expect(matchResult).toHaveProperty('grepValues.parameterName', 'one') + }) + + it('Any value matched', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_HEADERS, 'two', OPEN_API_PROPERTY_CONTENT], + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'one', OPEN_API_PROPERTY_DESCRIPTION], + ], + [ + [PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, grepValue('parameterName')] + ] + ) + expect(matchResult).toHaveProperty('grepValues.parameterName', 'description') + }) + + it('Many Property Matching', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_RESPONSES, '404', OPEN_API_PROPERTY_CONTENT, 'jsonType', OPEN_API_PROPERTY_EXAMPLE, 'param1'], + ], + [ + [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_RESPONSES, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE, grepValue('mediaType'), OPEN_API_PROPERTY_EXAMPLE, grepValue('example')], + [OPEN_API_PROPERTY_PATHS, PREDICATE_ANY_VALUE, grepValue('scope')], + ] + ) + expect(matchResult).toHaveProperty('grepValues.scope', 'responses') + expect(matchResult).toHaveProperty('grepValues.mediaType', 'jsonType') + expect(matchResult).toHaveProperty('grepValues.example', 'param1') + }) + + it('True predicate after matching', () => { + const suitablePredicate = [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_UNCLOSED_END] + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE], + ], + [ + [OPEN_API_PROPERTY_PATHS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE], + suitablePredicate, + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_EXAMPLE, PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE] + ] + ) + expect(matchResult).toHaveProperty('predicate', suitablePredicate) + }) + + it('ValidateValue predicate matches non-extension properties', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'myParam', OPEN_API_PROPERTY_DESCRIPTION], + ], + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_NOT_OAS_EXTENSION, OPEN_API_PROPERTY_DESCRIPTION] + ] + ) + expect(matchResult).toBeDefined() + expect(matchResult?.path).toEqual([OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'myParam', OPEN_API_PROPERTY_DESCRIPTION]) + }) + + it('ValidateValue predicate filters out OAS extensions', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'x-custom-extension', OPEN_API_PROPERTY_DESCRIPTION], + ], + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_NOT_OAS_EXTENSION, OPEN_API_PROPERTY_DESCRIPTION] + ] + ) + expect(matchResult).toBeUndefined() + }) + + it('ValidateValue predicate combined with grepValue', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'x-extension', OPEN_API_PROPERTY_DESCRIPTION], + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, 'normalParam', OPEN_API_PROPERTY_DESCRIPTION], + ], + [ + [OPEN_API_PROPERTY_COMPONENTS, OPEN_API_PROPERTY_PARAMETERS, PREDICATE_NOT_OAS_EXTENSION, grepValue('property')] + ] + ) + expect(matchResult).toBeDefined() + expect(matchResult).toHaveProperty('grepValues.property', OPEN_API_PROPERTY_DESCRIPTION) + }) + + it('ValidateValue predicate with ANY_VALUE and UNCLOSED_END', () => { + const pathWithoutOASExtension = [OPEN_API_PROPERTY_PATHS, '/api/users', OPEN_API_PROPERTY_RESPONSES, '200', OPEN_API_PROPERTY_CONTENT, 'application/json', 'schema', 'properties', 'id']; + const pathWithOASExtension = [OPEN_API_PROPERTY_PATHS, '/api/users', OPEN_API_PROPERTY_RESPONSES, '200', OPEN_API_PROPERTY_CONTENT, 'x-documentation-extension']; + const matchResult = matchPaths( + [ + pathWithOASExtension, + pathWithoutOASExtension, + ], + [ + [PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_RESPONSES, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_CONTENT, PREDICATE_NOT_OAS_EXTENSION, PREDICATE_UNCLOSED_END] + ] + ) + expect(matchResult).toBeDefined() + expect(matchResult?.path).toEqual(pathWithoutOASExtension) + }) + + it('ValidateValue predicate rejects extension in complex path', () => { + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_PATHS, '/api/users', OPEN_API_PROPERTY_RESPONSES, '200', OPEN_API_PROPERTY_CONTENT, 'x-custom-media', 'schema'], + ], + [ + [PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_RESPONSES, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_CONTENT, PREDICATE_NOT_OAS_EXTENSION, PREDICATE_UNCLOSED_END] + ] + ) + expect(matchResult).toBeUndefined() + }) + + it('Custom ValidateValue predicate with numeric filter', () => { + const numericCodePredicate = validateValuePredicate((value) => /^\d+$/.test(value.toString())) + const matchResult = matchPaths( + [ + [OPEN_API_PROPERTY_PATHS, '/api/users', OPEN_API_PROPERTY_RESPONSES, '200', OPEN_API_PROPERTY_CONTENT], + [OPEN_API_PROPERTY_PATHS, '/api/users', OPEN_API_PROPERTY_RESPONSES, 'default', OPEN_API_PROPERTY_CONTENT], + ], + [ + [PREDICATE_ANY_VALUE, PREDICATE_ANY_VALUE, OPEN_API_PROPERTY_RESPONSES, numericCodePredicate, OPEN_API_PROPERTY_CONTENT] + ] + ) + expect(matchResult).toBeDefined() + expect(matchResult?.path).toEqual([OPEN_API_PROPERTY_PATHS, '/api/users', OPEN_API_PROPERTY_RESPONSES, '200', OPEN_API_PROPERTY_CONTENT]) + }) +})