feat(complex-matcher): -addPropertyClause, +getPropertyClausesStrings, +setPropertyClause

This commit is contained in:
Julien Fontanet
2016-06-14 16:36:24 +02:00
parent 3b7bdee814
commit f27de8015b
4 changed files with 210 additions and 72 deletions

View File

@@ -1,11 +1,13 @@
import every from 'lodash/every'
import findIndex from 'lodash/findIndex'
import filter from 'lodash/filter'
import forEach from 'lodash/forEach'
import isArray from 'lodash/isArray'
import isPlainObject from 'lodash/isPlainObject'
import isString from 'lodash/isString'
import map from 'lodash/map'
import some from 'lodash/some'
import filterReduce from '../filter-reduce'
import invoke from '../invoke'
// ===================================================================
@@ -221,58 +223,107 @@ export const parse = invoke(() => {
// -------------------------------------------------------------------
export const addPropertyClause = invoke(() => {
const addAndClause = (node, child) => node.type === 'and'
? createAnd(node.children.concat(child))
: createAnd([ node, child ])
const addOrClause = (node, child) => node.type === 'or'
? createOr(node.children.concat(child))
: createOr([ node, child ])
const _getPropertyClauseStrings = ({ child }) => {
const { type } = child
const addOrClauseToProperty = (node, child) => createProperty(
node.name,
addOrClause(node.child, child)
if (type === 'or') {
const strings = []
forEach(child.children, child => {
if (child.type === 'string') {
strings.push(child.value)
}
})
return strings
}
if (type === 'string') {
return [ child.value ]
}
return []
}
// Find possible values for property clauses in a and clause.
export const getPropertyClausesStrings = function () {
if (!this) {
return {}
}
const { type } = this
if (type === 'property') {
return {
[this.name]: _getPropertyClauseStrings(this)
}
}
if (type === 'and') {
const strings = {}
forEach(this.children, node => {
if (node.type === 'property') {
const { name } = node
const values = strings[name]
if (values) {
values.push.apply(values, _getPropertyClauseStrings(node))
} else {
strings[name] = _getPropertyClauseStrings(node)
}
}
})
return strings
}
return {}
}
// -------------------------------------------------------------------
export const removePropertyClause = function (name) {
let type
if (
!this ||
(type = this.type) === 'property' && this.name === name
) {
return
}
if (type === 'and') {
return createAnd(filter(this.children, node =>
node.type !== 'property' || node.name !== name
))
}
return this
}
// -------------------------------------------------------------------
const _addAndClause = (node, child, predicate, reducer) =>
createAnd(filterReduce(
node.type === 'and'
? node.children
: [ node ],
predicate,
reducer,
child
))
export const setPropertyClause = function (name, child) {
const property = createProperty(
name,
isString(child) ? createString(child) : child
)
const addPropertyClause_ = (node, name, child) => {
if (!node) {
return createProperty(name, child)
}
const { type } = node
if (type === 'and') {
const { children } = node
const i = findIndex(children, node =>
node.type === 'property' && node.name === name
)
return i === -1
? createAnd([
...children,
createProperty(name, child)
])
: createAnd([
...children.slice(0, i),
addOrClauseToProperty(children[i], child),
...children.slice(i + 1)
])
}
if (type === 'property' && node.name === name) {
return addOrClauseToProperty(node, child)
}
return addAndClause(node, createProperty(name, child))
if (!this) {
return property
}
return function addPropertyClause (name, child) {
if (isString(child)) {
child = createString(child)
}
return addPropertyClause_(this, name, child)
}
})
return _addAndClause(
this,
property,
node => node.type === 'property' && node.name === name,
)
}
// -------------------------------------------------------------------
@@ -324,7 +375,7 @@ export const toString = invoke(() => {
and: ({ children }) => toStringGroup(children),
not: ({ child }) => `!${toString(child)}`,
or: ({ children }) => `|${toStringGroup(children)}`,
property: ({ name, child }) => `${name}:${toString(child)}`,
property: ({ name, child }) => `${toString(createString(name))}:${toString(child)}`,
string: ({ value }) => isRawString(value)
? value
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`
@@ -334,9 +385,11 @@ export const toString = invoke(() => {
// Special case for a root “and”: do not add braces.
return function () {
return this.type === 'and'
? toStringTerms(this.children)
: toString(this)
return !this
? ''
: this.type === 'and'
? toStringTerms(this.children)
: toString(this)
}
})

View File

@@ -1,8 +1,9 @@
import test from 'ava'
import {
addPropertyClause,
getPropertyClausesStrings,
parse,
setPropertyClause,
toString
} from './'
import {
@@ -10,25 +11,14 @@ import {
pattern
} from './index.fixtures'
test('addPropertyClause', t => {
t.is(
null::addPropertyClause('foo', 'bar')::toString(),
'foo:bar'
)
t.is(
parse('baz')::addPropertyClause('foo', 'bar')::toString(),
'baz foo:bar'
)
t.is(
parse('plip foo:baz plop')::addPropertyClause('foo', 'bar')::toString(),
'plip foo:|(baz bar) plop'
)
t.is(
parse('foo:|(baz plop)')::addPropertyClause('foo', 'bar')::toString(),
'foo:|(baz plop bar)'
test('getPropertyClausesStrings', t => {
let tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings()
t.deepEqual(
tmp,
{
bar: [ 'baz' ],
baz: [ 'foo', 'bar' ]
}
)
})
@@ -36,6 +26,28 @@ test('parse', t => {
t.deepEqual(parse(pattern), ast)
})
test('setPropertyClause', t => {
t.is(
null::setPropertyClause('foo', 'bar')::toString(),
'foo:bar'
)
t.is(
parse('baz')::setPropertyClause('foo', 'bar')::toString(),
'baz foo:bar'
)
t.is(
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString(),
'plip plop foo:bar'
)
t.is(
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString(),
'foo:bar'
)
})
test('toString', t => {
t.is(pattern, ast::toString())
})

View File

@@ -0,0 +1,45 @@
import findIndex from 'lodash/findIndex'
import identity from 'lodash/identity'
// Returns a copy of the array containing:
// - the elements which did not matches the predicate
// - the result of the reduction of the elements matching the
// predicates
//
// As a special case, if the predicate is not provided, it is
// considered to have not matched.
const filterReduce = (array, predicate, reducer, initial) => {
const { length } = array
let i
if (
!length ||
!predicate ||
(i = findIndex(array, predicate)) === -1
) {
return initial == null
? array.slice(0)
: array.concat(initial)
}
if (reducer == null) {
reducer = identity
}
const result = array.slice(0, i)
let value = initial == null
? array[i]
: reducer(initial, array[i], i, array)
for (i = i + 1; i < length; ++i) {
const current = array[i]
if (predicate(current, i, array)) {
value = reducer(value, current, i, array)
} else {
result.push(current)
}
}
result.push(value)
return result
}
export { filterReduce as default }

View File

@@ -0,0 +1,28 @@
import test from 'ava'
import filterReduce from './filter-reduce'
const add = (a, b) => a + b
const data = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
const isEven = x => !(x & 1)
test('filterReduce', t => {
// Returns all elements not matching the predicate and the result of
// a reduction over those who do.
t.deepEqual(
filterReduce(data, isEven, add),
[ 1, 3, 5, 7, 9, 20 ]
)
// The default reducer is the identity.
t.deepEqual(
filterReduce(data, isEven),
[ 1, 3, 5, 7, 9, 0 ]
)
// If an initial value is passed it is used.
t.deepEqual(
filterReduce(data, isEven, add, 22),
[ 1, 3, 5, 7, 9, 42 ]
)
})