ergogen/src/filter.js

126 lines
3.8 KiB
JavaScript
Raw Normal View History

2021-12-26 14:06:30 +01:00
const u = require('./utils')
const a = require('./assert')
2022-01-10 13:44:57 +01:00
const anchor_lib = require('./anchor')
const anchor = anchor_lib.parse
2021-12-26 14:06:30 +01:00
const _true = () => true
const _and = arr => p => arr.map(e => e(p)).reduce((a, b) => a && b)
const _or = arr => p => arr.map(e => e(p)).reduce((a, b) => a || b)
2022-01-03 14:43:45 +01:00
const similar = (key, reference, name, units) => {
2021-12-26 14:06:30 +01:00
let neg = false
2022-01-03 14:43:45 +01:00
if (reference.startsWith('-')) {
2021-12-26 14:06:30 +01:00
neg = true
2022-01-03 14:43:45 +01:00
reference = reference.slice(1)
2021-12-26 14:06:30 +01:00
}
2022-01-03 14:43:45 +01:00
// support both string or regex as reference
let internal_tester = val => (''+val) == reference
if (reference.startsWith('/')) {
const regex_parts = reference.split('/')
regex_parts.shift() // remove starting slash
const flags = regex_parts.pop()
const regex = new RegExp(regex_parts.join('/'), flags)
internal_tester = val => regex.test(''+val)
2021-12-26 14:06:30 +01:00
}
2022-01-03 14:43:45 +01:00
// support strings, arrays, or objects as key
const external_tester = point => {
const value = u.deep(point, key)
if (a.type(value)() == 'array') {
return value.some(subkey => internal_tester(subkey))
} else if (a.type(value)() == 'object') {
return Object.keys(value).some(subkey => internal_tester(subkey))
} else {
return internal_tester(value)
}
}
// negation happens at the end
if (neg) {
return point => !external_tester(point)
}
return external_tester
2021-12-26 14:06:30 +01:00
}
const comparators = {
'~': similar
// TODO: extension point for other operators...
}
const symbols = Object.keys(comparators)
const simple = (exp, name, units) => {
2022-01-03 14:43:45 +01:00
let keys = ['meta.name', 'meta.tags']
2021-12-26 14:06:30 +01:00
let op = '~'
2022-01-03 14:43:45 +01:00
let value
2021-12-26 14:06:30 +01:00
const parts = exp.split(/\s+/g)
// full case
if (symbols.includes(parts[1])) {
2022-01-03 14:43:45 +01:00
keys = parts[0].split(',')
2021-12-26 14:06:30 +01:00
op = parts[1]
2022-01-03 14:43:45 +01:00
value = parts.slice(2).join(' ')
2021-12-26 14:06:30 +01:00
2022-01-03 14:43:45 +01:00
// middle case, just an operator spec, default "keys"
2021-12-26 14:06:30 +01:00
} else if (symbols.includes(parts[0])) {
op = parts[0]
2022-01-03 14:43:45 +01:00
value = parts.slice(1).join(' ')
2021-12-26 14:06:30 +01:00
2022-01-03 14:43:45 +01:00
// basic case, only "value"
2021-12-26 14:06:30 +01:00
} else {
2022-01-03 14:43:45 +01:00
value = exp
2021-12-26 14:06:30 +01:00
}
2022-01-08 23:51:03 +01:00
return point => keys.some(key => comparators[op](key, value, name, units)(point))
2021-12-26 14:06:30 +01:00
}
2022-01-08 23:51:03 +01:00
const complex = (config, name, units, aggregator=_or) => {
2021-12-26 14:06:30 +01:00
// default is all points
if (config === undefined) {
return _true
}
// otherwise we branch by type
const type = a.type(config)()
switch(type) {
// base case is a string, meaning a simple/single filter
case 'string':
return simple(config, name, units)
// arrays are aggregated with alternating and/or conditions
case 'array':
const alternate = aggregator == _and ? _or : _and
return aggregator(config.map(elem => complex(elem, name, units, alternate)))
default:
throw new Error(`Unexpected type "${type}" found at filter "${name}"!`)
}
}
2022-01-10 13:44:57 +01:00
exports.parse = (config, name, points={}, units={}, include_mirrors=false) => {
2021-12-26 14:06:30 +01:00
2022-01-10 13:44:57 +01:00
let result = []
2022-01-03 14:43:45 +01:00
// if a filter decl is an object, it is an anchor
2021-12-26 14:06:30 +01:00
if (a.type(config)() == 'object') {
2022-01-10 13:44:57 +01:00
result.push(anchor(config, name, points)(units))
if (include_mirrors) {
// this is strict: if the ref of the anchor doesn't have a mirror pair, it will error out
result.push(anchor(config, name, points, true, undefined, true)(units))
}
// otherwise, it is treated as a condition to filter all available points
} else {
result = Object.values(points).filter(complex(config, name, units))
if (include_mirrors) {
// this is permissive: we only include mirrored versions if they exist, and don't fuss if they don't
result = result.concat(result.map(p => points[anchor_lib.mirror(p.meta.name)]).filter(p => !!p))
}
2021-12-26 14:06:30 +01:00
}
2022-01-10 13:44:57 +01:00
return result
2021-12-26 14:06:30 +01:00
}