From fede939a2cafe8a9a210617440ec1a88c4d5c37c Mon Sep 17 00:00:00 2001 From: janrywang Date: Sun, 13 Feb 2022 00:21:23 +0800 Subject: [PATCH 1/5] fix(path): fix nested group match is not work --- packages/path/src/__tests__/match.spec.ts | 23 ++- packages/path/src/matcher.ts | 229 ++++++++++++++++++++-- 2 files changed, 236 insertions(+), 16 deletions(-) diff --git a/packages/path/src/__tests__/match.spec.ts b/packages/path/src/__tests__/match.spec.ts index e936f7acf8f..72fd6122777 100644 --- a/packages/path/src/__tests__/match.spec.ts +++ b/packages/path/src/__tests__/match.spec.ts @@ -81,9 +81,9 @@ test('exclude match', () => { expect( Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name') ).toBeFalsy() - expect( - Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name.kkk') - ).toBeFalsy() + // expect( + // Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name.kkk') + // ).toBeFalsy() expect(Path.parse('aa.*(!bb)').match('kk.mm.aa.bb.cc')).toBeFalsy() expect(Path.parse('aa.*(!bb)').match('aa')).toBeFalsy() expect(Path.parse('aa.*(!bb.*)').match('aa')).toBeFalsy() @@ -115,8 +115,8 @@ test('test optional wild match', () => { expect(Path.parse('*(aa.**,bb.**)').match(['bb'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**)').match(['bb', 'cc', 'dd'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**)').match(['cc'])).toEqual(false) - expect(Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo'])).toEqual(false) - expect(Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo'])).toEqual(false) + expect(Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo'])).toEqual(true) + expect(Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo'])).toEqual(true) expect(Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo', 'bb'])).toEqual( true ) @@ -125,9 +125,15 @@ test('test optional wild match', () => { ) expect( Path.parse('*(aa.**,bb.**).bb').match(['aa', 'oo', 'kk', 'dd', 'bb']) + ).toEqual(true) + expect( + Path.parse('*(aa.**,bb.**).bb').match(['cc', 'oo', 'kk', 'dd', 'bb']) ).toEqual(false) expect( Path.parse('*(aa.**,bb.**).bb').match(['bb', 'oo', 'kk', 'dd', 'bb']) + ).toEqual(true) + expect( + Path.parse('*(aa.**,bb.**).bb').match(['kk', 'oo', 'kk', 'dd', 'bb']) ).toEqual(false) }) @@ -151,6 +157,12 @@ test('test segments', () => { expect(node.match(['a', 0, 'b'])).toEqual(true) }) +test('nested group match', () => { + expect( + Path.parse('aa.*.*(bb,cc).dd.*(kk,oo).ee').match('aa.0.cc.dd.kk.ee') + ).toEqual(true) +}) + test('group match with destructor', () => { expect(Path.parse('*([startDate,endDate],date,weak)').match('date')).toEqual( true @@ -247,6 +259,7 @@ match({ ['a', 'b'], ['a', 'b', 'c'], ], + 'aa.*.*(bb,cc).dd': [['aa', '0', 'cc', 'dd']], 'aaa.products.0.*': [['aaa', 'products', '0', 'aaa']], 'aa~.ccc': [ ['aa', 'ccc'], diff --git a/packages/path/src/matcher.ts b/packages/path/src/matcher.ts index 03051beba1c..51662e358e3 100644 --- a/packages/path/src/matcher.ts +++ b/packages/path/src/matcher.ts @@ -22,7 +22,7 @@ import { isEqual, toArr, isSegmentEqual } from './shared' const isValid = (val) => val !== undefined && val !== null && val !== '' -export class Matcher { +export class MatcherOld { private tree: Node private pos: number @@ -229,20 +229,227 @@ export class Matcher { } static matchSegments(source: Segments, target: Segments, record?: any) { - const pos = 0 if (source.length !== target.length) return false - const match = (pos: number) => { - const current = () => { - const res = isSegmentEqual(source[pos], target[pos]) - if (record && record.score !== undefined) { - record.score++ + const match = (pos = 0) => { + const current = isSegmentEqual(source[pos], target[pos]) + if (record?.score >= 0) { + record.score++ + } + return current && pos < source.length - 1 ? match(pos + 1) : true + } + + return { matched: match(), record } + } +} + +export interface IRecord { + score: number +} + +export class Matcher { + private tree: Node + + private stack: Node[] + + private record: IRecord + + private excluding: boolean + + private wildcards: WildcardOperatorNode[] + + private path: Segments + + constructor(tree: Node, record?: any) { + this.tree = tree + this.stack = [] + this.excluding = false + this.wildcards = [] + this.record = record + } + + next(node: Node, pos: number) { + const isLastToken = pos === this.path.length - 1 + const isOverToken = pos > this.path.length + if (node.after) { + if (isOverToken) { + return false + } + return this.matchNode(node.after, pos) + } + + if (isWildcardOperator(node) && !node.filter) { + if (this.excluding) { + return false + } else { + if (pos === 0 || node.optional) return true + return !!this.take(pos) + } + } + if (this.excluding) { + this.excluding = false + } + if (isLastToken) { + return !!this.take(pos) + } else { + const wildcard = this.wildcards.pop() + if (wildcard && wildcard.after) { + return this.next(wildcard, pos) + } + } + + return false + } + + shot() { + if (this.record?.score >= 0) { + this.record.score++ + } + } + + take(pos: number) { + return String(this.path[pos] ?? '') + } + + matchIdentifier(node: IdentifierNode, pos: number) { + const current = this.take(pos) + const isLastToken = pos === this.path.length - 1 + const isContainToken = pos < this.path.length + const isOverToken = !isContainToken + let matched = false + if (isExpandOperator(node.after)) { + if (current.indexOf(node.value) === 0) { + this.shot() + matched = true + } + if (this.excluding) { + if (!matched && isContainToken) { + return true } - return res + if (node.after.after) { + return this.next(node.after, pos) + } else { + return !isOverToken + } + } else { + return matched && this.next(node.after, pos) } - const next = () => (pos < source.length - 1 ? match(pos + 1) : true) - return current() && next() + } else if (current === node.value) { + this.shot() + matched = true } + if (this.excluding) { + if (matched) { + if (node.after) { + if (isContainToken) { + return this.next(node, pos) + } else { + return true + } + } + if (isLastToken) { + return false + } + } + if (isLastToken) { + return true + } + return isContainToken + } else { + return matched && this.next(node, pos) + } + } + + matchIgnoreExpression(node: IgnoreExpressionNode, pos: number) { + return isEqual(node.value, this.take(pos)) && this.next(node, pos) + } + + matchDestructorExpression(node: DestructorExpressionNode, pos: number) { + return isEqual(node.source, this.take(pos)) && this.next(node, pos) + } + + matchExpandOperator(node: ExpandOperatorNode, pos: number) { + return this.next(node, pos) + } + + matchWildcardOperator(node: WildcardOperatorNode, pos: number) { + let matched = false + if (node.filter) { + this.stack.push(node) + matched = this.matchNode(node.filter, pos) + this.stack.pop() + } else { + matched = this.next(node, pos) + } + return matched + } + + matchGroupExpression(node: GroupExpressionNode, pos: number) { + let excluding = false + if (node.isExclude) { + excluding = !this.excluding + } + return toArr(node.value)[excluding ? 'every' : 'some']((item) => { + this.wildcards = this.stack.slice() as any + this.excluding = excluding + return this.matchNode(item, pos) + }) + } + + matchRangeExpression(node: RangeExpressionNode, pos: number) { + const current = Number(this.take(pos)) + if (node.start) { + if (node.end) { + return ( + current >= Number(node.start.value) && + current <= Number(node.end.value) + ) + } else { + return current >= Number(node.start.value) + } + } else { + if (node.end) { + return current <= Number(node.end.value) + } else { + return true + } + } + } + + matchNode(node: Node, pos = 0) { + if (isDotOperator(node)) { + return this.next(node, pos + 1) + } else if (isIdentifier(node)) { + return this.matchIdentifier(node, pos) + } else if (isIgnoreExpression(node)) { + return this.matchIgnoreExpression(node, pos) + } else if (isDestructorExpression(node)) { + return this.matchDestructorExpression(node, pos) + } else if (isExpandOperator(node)) { + return this.matchExpandOperator(node, pos) + } else if (isWildcardOperator(node)) { + return this.matchWildcardOperator(node, pos) + } else if (isGroupExpression(node)) { + return this.matchGroupExpression(node, pos) + } else if (isRangeExpression(node)) { + return this.matchRangeExpression(node, pos) + } + return true + } + + match(path: Segments) { + this.path = path + return { matched: this.matchNode(this.tree), record: this.record } + } - return { matched: match(pos), record } + static matchSegments(source: Segments, target: Segments, record?: any) { + if (source.length !== target.length) return { matched: false, record } + const match = (pos = 0) => { + const current = isSegmentEqual(source[pos], target[pos]) + if (record?.score >= 0) { + record.score++ + } + return current && pos < source.length - 1 ? match(pos + 1) : true + } + return { matched: match(), record } } } From 8a7a490720fd67e64ef7247a2f3a9f1a7ef13d16 Mon Sep 17 00:00:00 2001 From: janrywang Date: Sun, 13 Feb 2022 00:24:38 +0800 Subject: [PATCH 2/5] chore: improve code --- packages/path/src/__tests__/match.spec.ts | 6 +- packages/path/src/matcher.ts | 224 ---------------------- 2 files changed, 3 insertions(+), 227 deletions(-) diff --git a/packages/path/src/__tests__/match.spec.ts b/packages/path/src/__tests__/match.spec.ts index 72fd6122777..6773675b28c 100644 --- a/packages/path/src/__tests__/match.spec.ts +++ b/packages/path/src/__tests__/match.spec.ts @@ -81,9 +81,9 @@ test('exclude match', () => { expect( Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name') ).toBeFalsy() - // expect( - // Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name.kkk') - // ).toBeFalsy() + expect( + Path.parse('*(!basic.name,basic.name.*,versionTag)').match('basic.name.kkk') + ).toBeFalsy() expect(Path.parse('aa.*(!bb)').match('kk.mm.aa.bb.cc')).toBeFalsy() expect(Path.parse('aa.*(!bb)').match('aa')).toBeFalsy() expect(Path.parse('aa.*(!bb.*)').match('aa')).toBeFalsy() diff --git a/packages/path/src/matcher.ts b/packages/path/src/matcher.ts index 51662e358e3..b88c45a34c6 100644 --- a/packages/path/src/matcher.ts +++ b/packages/path/src/matcher.ts @@ -16,232 +16,8 @@ import { WildcardOperatorNode, GroupExpressionNode, RangeExpressionNode, - DotOperatorNode, } from './types' import { isEqual, toArr, isSegmentEqual } from './shared' - -const isValid = (val) => val !== undefined && val !== null && val !== '' - -export class MatcherOld { - private tree: Node - - private pos: number - - private tail: Node - - private stack: any[] - - private excluding: boolean - - private record: any - - constructor(tree: Node, record?: any) { - this.tree = tree - this.pos = 0 - this.excluding = false - this.record = record - this.stack = [] - } - - currentElement(path: Segments) { - return String(path[this.pos] || '').replace(/\s*/g, '') - } - - matchNext = (node: any, path: any) => { - return node.after - ? this.matchAtom(path, node.after) - : isValid(path[this.pos]) - } - - recordMatch(match: () => boolean) { - return () => { - const result = match() - if (result) { - if (this.record && this.record.score !== undefined) { - this.record.score++ - } - } - return result - } - } - - matchIdentifier(path: Segments, node: IdentifierNode) { - this.tail = node - if (isValid(path[this.pos + 1]) && !node.after) { - if (this.stack.length) { - for (let i = this.stack.length - 1; i >= 0; i--) { - if (!this.stack[i].after || !this.stack[i].filter) { - return false - } - } - } else { - return false - } - } - let current: any - const next = () => { - return this.matchNext(node, path) - } - - if (isExpandOperator(node.after)) { - current = this.recordMatch( - () => - node.value === String(path[this.pos]).substring(0, node.value.length) - ) - } else { - current = this.recordMatch(() => - isEqual(String(node.value), String(path[this.pos])) - ) - } - - if (this.excluding) { - if (node.after) { - if (this.pos < path.length) { - return current() && next() - } else { - if (node.after && isWildcardOperator(node.after.after)) { - return true - } - return false - } - } else { - if (this.pos >= path.length) { - return true - } - return current() - } - } - - return current() && next() - } - - matchIgnoreExpression(path: Segments, node: IgnoreExpressionNode) { - return ( - isEqual(node.value, this.currentElement(path)) && - this.matchNext(node, path) - ) - } - - matchDestructorExpression(path: Segments, node: DestructorExpressionNode) { - return ( - isEqual(node.source, this.currentElement(path)) && - this.matchNext(node, path) - ) - } - - matchExpandOperator(path: Segments, node: ExpandOperatorNode) { - return this.matchAtom(path, node.after) - } - - matchWildcardOperator(path: Segments, node: WildcardOperatorNode) { - this.tail = node - this.stack.push(node) - let matched = false - if (node.filter) { - if (node.after) { - matched = - this.matchAtom(path, node.filter) && this.matchAtom(path, node.after) - } else { - matched = this.matchAtom(path, node.filter) - } - } else if (node.optional) { - matched = true - } else { - matched = this.matchNext(node, path) - } - this.stack.pop() - return matched - } - - matchGroupExpression(path: Segments, node: GroupExpressionNode) { - const current = this.pos - this.excluding = !!node.isExclude - const method = this.excluding ? 'every' : 'some' - const result = toArr(node.value)[method]((_node) => { - this.pos = current - return this.excluding - ? !this.matchAtom(path, _node) - : this.matchAtom(path, _node) - }) - this.excluding = false - return result - } - - matchRangeExpression(path: Segments, node: RangeExpressionNode) { - if (node.start) { - if (node.end) { - return ( - path[this.pos] >= parseInt(node.start.value) && - path[this.pos] <= parseInt(node.end.value) - ) - } else { - return path[this.pos] >= parseInt(node.start.value) - } - } else { - if (node.end) { - return path[this.pos] <= parseInt(node.end.value) - } else { - return true - } - } - } - - matchDotOperator(path: Segments, node: DotOperatorNode) { - this.pos++ - return this.matchNext(node, path) - } - - matchAtom(path: Segments, node: Node) { - if (!node) { - if (this.stack.length > 0) return true - if (isValid(path[this.pos + 1])) return false - if (this.pos == path.length - 1) return true - } - if (isIdentifier(node)) { - return this.matchIdentifier(path, node) - } else if (isIgnoreExpression(node)) { - return this.matchIgnoreExpression(path, node) - } else if (isDestructorExpression(node)) { - return this.matchDestructorExpression(path, node) - } else if (isExpandOperator(node)) { - return this.matchExpandOperator(path, node) - } else if (isWildcardOperator(node)) { - return this.matchWildcardOperator(path, node) - } else if (isGroupExpression(node)) { - return this.matchGroupExpression(path, node) - } else if (isRangeExpression(node)) { - return this.matchRangeExpression(path, node) - } else if (isDotOperator(node)) { - return this.matchDotOperator(path, node) - } - - return true - } - - match(path: Segments) { - const matched = this.matchAtom(path, this.tree) - if (!this.tail) return { matched: false } - if (this.tail == this.tree && isWildcardOperator(this.tail)) { - return { matched: true } - } - - return { matched, record: this.record } - } - - static matchSegments(source: Segments, target: Segments, record?: any) { - if (source.length !== target.length) return false - const match = (pos = 0) => { - const current = isSegmentEqual(source[pos], target[pos]) - if (record?.score >= 0) { - record.score++ - } - return current && pos < source.length - 1 ? match(pos + 1) : true - } - - return { matched: match(), record } - } -} - export interface IRecord { score: number } From e2c8a67671dfc8fa01dd1c20adbd3c136b6337ac Mon Sep 17 00:00:00 2001 From: janrywang Date: Sun, 13 Feb 2022 00:29:40 +0800 Subject: [PATCH 3/5] chore: improve code --- packages/path/src/matcher.ts | 50 +++++++++++++++++------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/path/src/matcher.ts b/packages/path/src/matcher.ts index b88c45a34c6..abec982bf1d 100644 --- a/packages/path/src/matcher.ts +++ b/packages/path/src/matcher.ts @@ -86,11 +86,29 @@ export class Matcher { return String(this.path[pos] ?? '') } - matchIdentifier(node: IdentifierNode, pos: number) { - const current = this.take(pos) + matchExcludeIdentifier(matched: boolean, node: Node, pos: number) { const isLastToken = pos === this.path.length - 1 const isContainToken = pos < this.path.length - const isOverToken = !isContainToken + if (matched) { + if (node.after) { + if (isContainToken) { + return this.next(node, pos) + } else { + return true + } + } + if (isLastToken) { + return false + } + } + if (isLastToken) { + return true + } + return isContainToken + } + + matchIdentifier(node: IdentifierNode, pos: number) { + const current = this.take(pos) let matched = false if (isExpandOperator(node.after)) { if (current.indexOf(node.value) === 0) { @@ -98,14 +116,7 @@ export class Matcher { matched = true } if (this.excluding) { - if (!matched && isContainToken) { - return true - } - if (node.after.after) { - return this.next(node.after, pos) - } else { - return !isOverToken - } + return this.matchExcludeIdentifier(matched, node.after, pos) } else { return matched && this.next(node.after, pos) } @@ -114,22 +125,7 @@ export class Matcher { matched = true } if (this.excluding) { - if (matched) { - if (node.after) { - if (isContainToken) { - return this.next(node, pos) - } else { - return true - } - } - if (isLastToken) { - return false - } - } - if (isLastToken) { - return true - } - return isContainToken + return this.matchExcludeIdentifier(matched, node, pos) } else { return matched && this.next(node, pos) } From 86f613c6ccc807127bcf1511a8c0863af3d15adc Mon Sep 17 00:00:00 2001 From: janrywang Date: Sun, 13 Feb 2022 01:00:13 +0800 Subject: [PATCH 4/5] chore: improve code --- packages/path/src/__tests__/match.spec.ts | 5 +++++ packages/path/src/matcher.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/path/src/__tests__/match.spec.ts b/packages/path/src/__tests__/match.spec.ts index 6773675b28c..e1dc02bc5f9 100644 --- a/packages/path/src/__tests__/match.spec.ts +++ b/packages/path/src/__tests__/match.spec.ts @@ -31,6 +31,11 @@ const unmatch = (obj) => { } } +test('basic match', () => { + expect(Path.parse('xxx').match('')).toBeFalsy() + expect(Path.parse('xxx').match('aaa')).toBeFalsy() +}) + test('test matchGroup', () => { const pattern = new Path('*(aa,bb,cc)') expect(pattern.matchAliasGroup('aa', 'bb')).toEqual(true) diff --git a/packages/path/src/matcher.ts b/packages/path/src/matcher.ts index abec982bf1d..6b376884718 100644 --- a/packages/path/src/matcher.ts +++ b/packages/path/src/matcher.ts @@ -220,7 +220,7 @@ export class Matcher { if (record?.score >= 0) { record.score++ } - return current && pos < source.length - 1 ? match(pos + 1) : true + return current && (pos < source.length - 1 ? match(pos + 1) : true) } return { matched: match(), record } } From 723c6146d44059a2c347f55f0a2bb4a739a2aaa8 Mon Sep 17 00:00:00 2001 From: janrywang Date: Sun, 13 Feb 2022 21:18:15 +0800 Subject: [PATCH 5/5] chore: improve code --- packages/path/src/__tests__/match.spec.ts | 23 ++++++++++++++++++++++ packages/path/src/matcher.ts | 24 ++++++++++------------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/path/src/__tests__/match.spec.ts b/packages/path/src/__tests__/match.spec.ts index e1dc02bc5f9..41ce2f70c65 100644 --- a/packages/path/src/__tests__/match.spec.ts +++ b/packages/path/src/__tests__/match.spec.ts @@ -1,5 +1,6 @@ import expect from 'expect' import { Path } from '../' +import { Matcher } from '../matcher' const match = (obj) => { for (let name in obj) { @@ -34,6 +35,18 @@ const unmatch = (obj) => { test('basic match', () => { expect(Path.parse('xxx').match('')).toBeFalsy() expect(Path.parse('xxx').match('aaa')).toBeFalsy() + expect(Path.parse('xxx.eee').match('xxx')).toBeFalsy() + expect(Path.parse('*(xxx.eee~)').match('xxx')).toBeFalsy() + expect(Path.parse('xxx.eee~').match('xxx.eee')).toBeTruthy() + expect(Path.parse('*(!xxx.eee,yyy)').match('xxx')).toBeFalsy() + expect(Path.parse('*(!xxx.eee,yyy)').match('xxx.ooo.ppp')).toBeTruthy() + expect(Path.parse('*(!xxx.eee,yyy)').match('xxx.eee')).toBeFalsy() + expect(Path.parse('*(!xxx.eee~,yyy)').match('xxx.eee')).toBeFalsy() + expect(Path.parse('~.aa').match('xxx.aa')).toBeTruthy() +}) + +test('not expect match not', () => { + expect(new Matcher({}).match(['']).matched).toBeFalsy() }) test('test matchGroup', () => { @@ -241,6 +254,16 @@ match({ ['a', 10, 's'], ['a', 50, 's'], ], + 'a.*[10:].*(!a,b)': [ + ['a', 49, 's'], + ['a', 10, 's'], + ['a', 50, 's'], + ], + 'a.*[].*(!a,b)': [ + ['a', 49, 's'], + ['a', 10, 's'], + ['a', 50, 's'], + ], 'a.*[:50].*(!a,b)': [ ['a', 49, 's'], ['a', 10, 's'], diff --git a/packages/path/src/matcher.ts b/packages/path/src/matcher.ts index 6b376884718..e12bc0c240d 100644 --- a/packages/path/src/matcher.ts +++ b/packages/path/src/matcher.ts @@ -45,11 +45,11 @@ export class Matcher { next(node: Node, pos: number) { const isLastToken = pos === this.path.length - 1 - const isOverToken = pos > this.path.length + // const isOverToken = pos > this.path.length if (node.after) { - if (isOverToken) { - return false - } + // if (isOverToken) { + // return false + // } return this.matchNode(node.after, pos) } @@ -61,9 +61,6 @@ export class Matcher { return !!this.take(pos) } } - if (this.excluding) { - this.excluding = false - } if (isLastToken) { return !!this.take(pos) } else { @@ -89,13 +86,12 @@ export class Matcher { matchExcludeIdentifier(matched: boolean, node: Node, pos: number) { const isLastToken = pos === this.path.length - 1 const isContainToken = pos < this.path.length + if (!node.after) { + this.excluding = false + } if (matched) { if (node.after) { - if (isContainToken) { - return this.next(node, pos) - } else { - return true - } + return this.next(node, pos) } if (isLastToken) { return false @@ -161,7 +157,7 @@ export class Matcher { excluding = !this.excluding } return toArr(node.value)[excluding ? 'every' : 'some']((item) => { - this.wildcards = this.stack.slice() as any + this.wildcards = this.stack.slice() as WildcardOperatorNode[] this.excluding = excluding return this.matchNode(item, pos) }) @@ -205,7 +201,7 @@ export class Matcher { } else if (isRangeExpression(node)) { return this.matchRangeExpression(node, pos) } - return true + return false } match(path: Segments) {