|
| 1 | +import { interpolateDeep } from './interpolate'; |
| 2 | +import { defaults } from './options'; |
| 3 | +import { patch, memoRecord } from './utils'; |
| 4 | + |
| 5 | +const createFactEvaluator = |
| 6 | + (context, facts, { validator, resolver }, emit) => |
| 7 | + (rule, index) => |
| 8 | + async ([factName, { name, params, path, is }]) => { |
| 9 | + emit('debug', { |
| 10 | + type: 'STARTING_FACT', |
| 11 | + fact: factName, |
| 12 | + rule, |
| 13 | + index, |
| 14 | + params, |
| 15 | + }); |
| 16 | + const fact = facts[factName] || context[factName]; |
| 17 | + try { |
| 18 | + const value = await (typeof fact === 'function' ? fact(params) : fact); |
| 19 | + const resolved = path ? resolver(value, path) : value; |
| 20 | + emit('debug', { |
| 21 | + type: 'EXECUTED_FACT', |
| 22 | + fact: factName, |
| 23 | + rule, |
| 24 | + index, |
| 25 | + params, |
| 26 | + value, |
| 27 | + resolved, |
| 28 | + }); |
| 29 | + try { |
| 30 | + const result = await validator(resolved, is); |
| 31 | + emit('debug', { |
| 32 | + type: 'EVALUATED_FACT', |
| 33 | + fact: factName, |
| 34 | + rule, |
| 35 | + index, |
| 36 | + value, |
| 37 | + resolved, |
| 38 | + is, |
| 39 | + result, |
| 40 | + }); |
| 41 | + return { |
| 42 | + factName, |
| 43 | + name, |
| 44 | + value, |
| 45 | + resolved, |
| 46 | + ...result, |
| 47 | + }; |
| 48 | + } catch (error) { |
| 49 | + emit('error', { |
| 50 | + type: 'FactEvaluationError', |
| 51 | + rule, |
| 52 | + error, |
| 53 | + context, |
| 54 | + factName, |
| 55 | + value, |
| 56 | + resolved, |
| 57 | + path, |
| 58 | + is, |
| 59 | + }); |
| 60 | + return { error: true }; |
| 61 | + } |
| 62 | + } catch (error) { |
| 63 | + emit('error', { |
| 64 | + type: 'FactExecutionError', |
| 65 | + rule, |
| 66 | + error, |
| 67 | + context, |
| 68 | + factName, |
| 69 | + params, |
| 70 | + }); |
| 71 | + return { error: true }; |
| 72 | + } |
| 73 | + }; |
| 74 | + |
| 75 | +const createFactmapChecker = (context, facts, options, emit) => { |
| 76 | + const evaluator = createFactEvaluator(context, facts, options, emit); |
| 77 | + return (rule) => { |
| 78 | + return async (factMap, index) => { |
| 79 | + emit('debug', { |
| 80 | + type: 'STARTING_FACT_MAP', |
| 81 | + rule, |
| 82 | + index, |
| 83 | + }); |
| 84 | + const results = await Promise.all( |
| 85 | + Object.entries(factMap).map(evaluator(rule, index)), |
| 86 | + ); |
| 87 | + return results.reduce( |
| 88 | + (acc, { factName, ...rest }) => ({ ...acc, [factName]: rest }), |
| 89 | + {}, |
| 90 | + ); |
| 91 | + }; |
| 92 | + }; |
| 93 | +}; |
| 94 | + |
| 95 | +const getRuleResults = (results) => { |
| 96 | + return Object.values(results).reduce( |
| 97 | + (acc, resultMap) => { |
| 98 | + if (acc.error) return acc; |
| 99 | + const error = Object.values(resultMap).some(({ error }) => error); |
| 100 | + return { |
| 101 | + error, |
| 102 | + passed: |
| 103 | + !error && |
| 104 | + acc.passed && |
| 105 | + Object.values(resultMap).every(({ result }) => result), |
| 106 | + }; |
| 107 | + }, |
| 108 | + { |
| 109 | + passed: true, |
| 110 | + error: false, |
| 111 | + }, |
| 112 | + ); |
| 113 | +}; |
| 114 | + |
| 115 | +const getResultsContext = (results) => |
| 116 | + results.reduce( |
| 117 | + (acc, r) => |
| 118 | + Object.values(r).reduce( |
| 119 | + (a, { name, ...rest }) => (name ? { ...a, [name]: rest } : a), |
| 120 | + acc, |
| 121 | + ), |
| 122 | + { ...results }, |
| 123 | + ); |
| 124 | + |
| 125 | +const createActionExecutor = (actions, opts, emit) => (what, context) => |
| 126 | + Promise.all( |
| 127 | + interpolateDeep(what, context, opts.pattern, opts.resolver).map( |
| 128 | + async ({ type, params }) => { |
| 129 | + try { |
| 130 | + if (!actions[type]) throw new Error(`No action found for ${type}`); |
| 131 | + await actions[type](params); |
| 132 | + } catch (error) { |
| 133 | + emit('error', { |
| 134 | + type: 'ActionExecutionError', |
| 135 | + action: type, |
| 136 | + params, |
| 137 | + error, |
| 138 | + }); |
| 139 | + } |
| 140 | + }, |
| 141 | + ), |
| 142 | + ); |
| 143 | + |
| 144 | +const createRuleRunner = (context, facts, actions, opts, emit) => { |
| 145 | + const checker = createFactmapChecker(context, facts, opts, emit); |
| 146 | + const executor = createActionExecutor(actions, opts, emit); |
| 147 | + return async ([rule, { when, ...rest }]) => { |
| 148 | + const interpolatedRule = interpolateDeep( |
| 149 | + when, |
| 150 | + context, |
| 151 | + opts.pattern, |
| 152 | + opts.resolver, |
| 153 | + ); |
| 154 | + emit('debug', { |
| 155 | + type: 'STARTING_RULE', |
| 156 | + rule, |
| 157 | + interpolated: interpolatedRule, |
| 158 | + }); |
| 159 | + const results = await Promise.all(interpolatedRule.map(checker(rule))); |
| 160 | + const { passed, error } = getRuleResults(results); |
| 161 | + const resultsContext = getResultsContext(results); |
| 162 | + emit('debug', { |
| 163 | + type: 'FINISHED_RULE', |
| 164 | + rule, |
| 165 | + results, |
| 166 | + passed, |
| 167 | + error, |
| 168 | + context: resultsContext, |
| 169 | + }); |
| 170 | + if (error) return; |
| 171 | + const key = passed ? 'then' : 'otherwise'; |
| 172 | + const which = rest[key]; |
| 173 | + if (!which) return; |
| 174 | + const nextContext = { |
| 175 | + ...context, |
| 176 | + results: { ...(context.results || {}), ...resultsContext }, |
| 177 | + }; |
| 178 | + |
| 179 | + return Promise.all([ |
| 180 | + which.when |
| 181 | + ? createRuleRunner( |
| 182 | + nextContext, |
| 183 | + facts, |
| 184 | + actions, |
| 185 | + opts, |
| 186 | + emit, |
| 187 | + )([`${rule}.${key}`, which]) |
| 188 | + : null, |
| 189 | + which.actions ? executor(which.actions, nextContext) : null, |
| 190 | + ]); |
| 191 | + }; |
| 192 | +}; |
| 193 | + |
| 194 | +export const createRulesEngine = ({ |
| 195 | + facts = {}, |
| 196 | + actions = {}, |
| 197 | + rules = {}, |
| 198 | + ...options |
| 199 | +} = {}) => { |
| 200 | + options = { ...defaults, ...options }; |
| 201 | + |
| 202 | + if (!options.validator) throw new Error('A validator is required'); |
| 203 | + |
| 204 | + const eventMap = new Map(); |
| 205 | + |
| 206 | + const emit = (event, a) => (eventMap.get(event) || []).forEach((s) => s(a)); |
| 207 | + |
| 208 | + const on = (event, subscriber) => { |
| 209 | + if (!eventMap.get(event)) eventMap.set(event, new Set()); |
| 210 | + eventMap.get(event).add(subscriber); |
| 211 | + return () => eventMap.get(event).delete(subscriber); |
| 212 | + }; |
| 213 | + |
| 214 | + return { |
| 215 | + setFacts: (next) => (facts = patch(next, facts)), |
| 216 | + setActions: (next) => (actions = patch(next, actions)), |
| 217 | + setRules: (next) => (rules = patch(next, rules)), |
| 218 | + run: async (context = {}) => { |
| 219 | + emit('start', { context, facts, rules, actions }); |
| 220 | + const runner = createRuleRunner( |
| 221 | + context, |
| 222 | + memoRecord(facts), |
| 223 | + actions, |
| 224 | + options, |
| 225 | + emit, |
| 226 | + ); |
| 227 | + await Promise.all(Object.entries(rules).map(runner)); |
| 228 | + emit('complete', { context }); |
| 229 | + }, |
| 230 | + on, |
| 231 | + }; |
| 232 | +}; |
0 commit comments