feat(complex-matcher): add glob pattern support (#3198)

Fixes #3190
This commit is contained in:
Julien Fontanet 2018-07-18 15:06:33 +02:00 committed by GitHub
parent e58d56a656
commit 7dfb1635c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 78 additions and 15 deletions

View File

@ -1,7 +1,7 @@
import * as CM from './' import * as CM from './'
export const pattern = export const pattern =
'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape? age:32' 'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape? age:32 chi*go'
export const ast = new CM.And([ export const ast = new CM.And([
new CM.String('foo'), new CM.String('foo'),
@ -12,4 +12,5 @@ export const ast = new CM.And([
), ),
new CM.TruthyProperty('hasCape'), new CM.TruthyProperty('hasCape'),
new CM.Property('age', new CM.Number(32)), new CM.Property('age', new CM.Number(32)),
new CM.GlobPattern('chi*go'),
]) ])

View File

@ -1,4 +1,4 @@
import { isPlainObject, some } from 'lodash' import { escapeRegExp, isPlainObject, some } from 'lodash'
// =================================================================== // ===================================================================
@ -178,6 +178,48 @@ const formatString = value =>
: `"${value.replace(/\\|"/g, escapeChar)}"` : `"${value.replace(/\\|"/g, escapeChar)}"`
: `"${value}"` : `"${value}"`
export class GlobPattern extends Node {
constructor (value) {
// fallback to string node if no wildcard
if (value.indexOf('*') === -1) {
return new StringNode(value)
}
super()
this.value = value
// should not be enumerable for the tests
Object.defineProperty(this, 'match', {
value: this.match.bind(
this,
new RegExp(
value
.split('*')
.map(escapeRegExp)
.join('.*'),
'i'
)
),
})
}
match (re, value) {
if (typeof value === 'string') {
return re.test(value)
}
if (Array.isArray(value) || isPlainObject(value)) {
return some(value, this.match)
}
return false
}
toString () {
return this.value
}
}
export class StringNode extends Node { export class StringNode extends Node {
constructor (value) { constructor (value) {
super() super()
@ -389,6 +431,17 @@ const parser = P.grammar({
P.seq(r.ws, r.term.repeat(), P.eof).map( P.seq(r.ws, r.term.repeat(), P.eof).map(
([, terms]) => (terms.length === 0 ? new Null() : new And(terms)) ([, terms]) => (terms.length === 0 ? new Null() : new And(terms))
), ),
globPattern: new P((input, pos, end) => {
let value = ''
let c
while (pos < end && ((c = input[pos]) === '*' || c in RAW_STRING_CHARS)) {
++pos
value += c
}
return value.length === 0
? new Failure(pos, 'a raw string')
: new Success(pos, value)
}),
quotedString: new P((input, pos, end) => { quotedString: new P((input, pos, end) => {
if (input[pos] !== '"') { if (input[pos] !== '"') {
return new Failure(pos, '"') return new Failure(pos, '"')
@ -406,6 +459,7 @@ const parser = P.grammar({
return new Success(pos, value.join('')) return new Success(pos, value.join(''))
}), }),
property: r => P.alt(r.quotedString, r.rawString),
rawString: new P((input, pos, end) => { rawString: new P((input, pos, end) => {
let value = '' let value = ''
let c let c
@ -417,7 +471,6 @@ const parser = P.grammar({
? new Failure(pos, 'a raw string') ? new Failure(pos, 'a raw string')
: new Success(pos, value) : new Success(pos, value)
}), }),
string: r => P.alt(r.quotedString, r.rawString),
term: r => term: r =>
P.alt( P.alt(
P.seq(P.text('('), r.ws, r.term.repeat(1), P.text(')')).map( P.seq(P.text('('), r.ws, r.term.repeat(1), P.text(')')).map(
@ -439,20 +492,22 @@ const parser = P.grammar({
} }
return new Comparison(op, val) return new Comparison(op, val)
}), }),
P.seq(r.string, r.ws, P.text(':'), r.ws, r.term).map( P.seq(r.property, r.ws, P.text(':'), r.ws, r.term).map(
_ => new Property(_[0], _[4]) _ => new Property(_[0], _[4])
), ),
P.seq(r.string, P.text('?')).map(_ => new TruthyProperty(_[0])), P.seq(r.property, P.text('?')).map(_ => new TruthyProperty(_[0])),
P.alt( r.value
r.quotedString.map(_ => new StringNode(_)),
r.rawString.map(str => {
const asNum = +str
return Number.isNaN(asNum)
? new StringNode(str)
: new NumberNode(asNum)
})
)
).skip(r.ws), ).skip(r.ws),
value: r =>
P.alt(
r.quotedString.map(_ => new StringNode(_)),
r.globPattern.map(str => {
const asNum = +str
return Number.isNaN(asNum)
? new GlobPattern(str)
: new NumberNode(asNum)
})
),
ws: P.regex(/\s*/), ws: P.regex(/\s*/),
}).default }).default
export const parse = parser.parse.bind(parser) export const parse = parser.parse.bind(parser)

View File

@ -3,6 +3,7 @@
import { ast, pattern } from './index.fixtures' import { ast, pattern } from './index.fixtures'
import { import {
getPropertyClausesStrings, getPropertyClausesStrings,
GlobPattern,
Null, Null,
NumberNode, NumberNode,
parse, parse,
@ -41,6 +42,12 @@ describe('parse', () => {
}) })
}) })
describe('GlobPattern', () => {
it('matches a glob pattern recursively', () => {
expect(new GlobPattern('b*r').match({ foo: 'bar' })).toBe(true)
})
})
describe('Number', () => { describe('Number', () => {
it('match a number recursively', () => { it('match a number recursively', () => {
expect(new NumberNode(3).match([{ foo: 3 }])).toBe(true) expect(new NumberNode(3).match([{ foo: 3 }])).toBe(true)
@ -82,5 +89,5 @@ describe('setPropertyClause', () => {
}) })
it('toString', () => { it('toString', () => {
expect(pattern).toBe(ast.toString()) expect(ast.toString()).toBe(pattern)
}) })