|
| 1 | +import lodash from 'lodash' |
| 2 | +import { |
| 3 | + Condition, |
| 4 | + JsonRule, |
| 5 | + Operator, |
| 6 | + ResourceData, |
| 7 | + RuleResult, |
| 8 | + _ResourceData, |
| 9 | +} from '../types' |
| 10 | +import { RuleEvaluator } from './rule-evaluator' |
| 11 | + |
| 12 | +export default class JsonEvaluator implements RuleEvaluator<JsonRule> { |
| 13 | + canEvaluate(rule: JsonRule): boolean { |
| 14 | + return 'conditions' in rule |
| 15 | + } |
| 16 | + |
| 17 | + async evaluateSingleResource( |
| 18 | + rule: JsonRule, |
| 19 | + data: ResourceData |
| 20 | + ): Promise<RuleResult> { |
| 21 | + return this.evaluateCondition(rule.conditions, data) |
| 22 | + ? RuleResult.MATCHES |
| 23 | + : RuleResult.DOESNT_MATCH |
| 24 | + } |
| 25 | + |
| 26 | + calculatePath = (data: _ResourceData, _path: string) => { |
| 27 | + let path = _path |
| 28 | + if (path.indexOf('@') === 0) { |
| 29 | + // @ means the curr resource, we replace by base path |
| 30 | + path = path.replace('@', data.resourcePath).substr(2) // remove `$.` |
| 31 | + } |
| 32 | + if (path.indexOf('[*]') === 0 && data.elementPath) { |
| 33 | + // @ means the curr resource, we replace by base path |
| 34 | + path = path.replace('[*]', data.elementPath) |
| 35 | + } |
| 36 | + return path |
| 37 | + } |
| 38 | + |
| 39 | + resolvePath = (data: _ResourceData, path: string): any => { |
| 40 | + return lodash.get(data.data, path) |
| 41 | + } |
| 42 | + |
| 43 | + operators: { [key: string]: Operator } = { |
| 44 | + // eslint-disable-next-line eqeqeq |
| 45 | + equal: (a, b) => a == b, // == is fine |
| 46 | + notEqual: (a, b) => a !== b, |
| 47 | + in: (a, b) => (b as any[]).indexOf(a) > -1, |
| 48 | + notIn: (a, b) => (b as any[]).indexOf(a) === -1, |
| 49 | + contains: (a, b) => a.indexOf(b) > -1, |
| 50 | + doesNotContain: (a, b) => a.indexOf(b) === -1, |
| 51 | + lessThan: (a, b) => a < b, |
| 52 | + lessThanInclusive: (a, b) => a <= b, |
| 53 | + greaterThan: (a, b) => a > b, |
| 54 | + greaterThanInclusive: (a, b) => a >= b, |
| 55 | + |
| 56 | + daysAgo: a => |
| 57 | + Math.trunc((Date.now() - new Date(a).getTime()) / (60 * 60 * 1000 * 24)), // @TODO use library |
| 58 | + |
| 59 | + or: (_, conditions: Condition[], data) => { |
| 60 | + for (let i = 0; i < conditions.length; i++) { |
| 61 | + // if 1 is true, it's true |
| 62 | + if (this.evaluateCondition(conditions[i], data)) return true |
| 63 | + } |
| 64 | + return false |
| 65 | + }, |
| 66 | + and: (_, conditions: Condition[], data) => { |
| 67 | + for (let i = 0; i < conditions.length; i++) { |
| 68 | + // if 1 is false, it's false |
| 69 | + if (!this.evaluateCondition(conditions[i], data)) return false |
| 70 | + } |
| 71 | + return true |
| 72 | + }, |
| 73 | + array_all: (array, conditions: Condition, data) => { |
| 74 | + // an AND, but with every resource item |
| 75 | + const baseElementPath = data.elementPath |
| 76 | + |
| 77 | + for (let i = 0; i < array.length; i++) { |
| 78 | + if ( |
| 79 | + !this.evaluateCondition(conditions, { |
| 80 | + ...data, |
| 81 | + elementPath: `${baseElementPath}[${i}]`, |
| 82 | + }) |
| 83 | + ) |
| 84 | + return false |
| 85 | + } |
| 86 | + return true |
| 87 | + }, |
| 88 | + array_any: (array, conditions, data) => { |
| 89 | + // an OR, but with every resource item |
| 90 | + |
| 91 | + const baseElementPath = data.elementPath |
| 92 | + for (let i = 0; i < array.length; i++) { |
| 93 | + if ( |
| 94 | + this.evaluateCondition(conditions as Condition, { |
| 95 | + ...data, |
| 96 | + elementPath: `${baseElementPath}[${i}]`, |
| 97 | + }) |
| 98 | + ) |
| 99 | + return true |
| 100 | + } |
| 101 | + return false |
| 102 | + }, |
| 103 | + } |
| 104 | + |
| 105 | + isCondition = (a: unknown): boolean => |
| 106 | + !!a && (a as any).constructor === Object |
| 107 | + |
| 108 | + evaluateCondition( |
| 109 | + _condition: Condition, |
| 110 | + _data: _ResourceData |
| 111 | + ): boolean | number { |
| 112 | + const condition = { ..._condition } |
| 113 | + const { path, value } = condition |
| 114 | + delete condition.path |
| 115 | + delete condition.value |
| 116 | + // remaining field should be the op name |
| 117 | + const op = Object.keys(condition)[0] // |
| 118 | + const operator = this.operators[op] |
| 119 | + const otherArgs = condition[op] // {[and]: xxx } |
| 120 | + if (!op || !operator) { |
| 121 | + throw new Error(`unrecognized operation${JSON.stringify(condition)}`) |
| 122 | + } |
| 123 | + |
| 124 | + const data = { ..._data } |
| 125 | + let firstArg |
| 126 | + |
| 127 | + if (path) { |
| 128 | + const elementPath = this.calculatePath(data, path) |
| 129 | + data.elementPath = elementPath |
| 130 | + firstArg = this.resolvePath(data, elementPath) |
| 131 | + } else if (this.isCondition(value)) { |
| 132 | + firstArg = this.evaluateCondition(value as any, data) |
| 133 | + } else { |
| 134 | + firstArg = value |
| 135 | + } |
| 136 | + // console.log(operator, firstArg, otherArgs, data) |
| 137 | + return operator(firstArg, otherArgs, data) |
| 138 | + } |
| 139 | +} |
0 commit comments