Skip to content

Commit 0fe3cde

Browse files
committed
fix bugs with subscribers and named fact maps
1 parent e87b6c7 commit 0fe3cde

File tree

8 files changed

+224
-22
lines changed

8 files changed

+224
-22
lines changed

jest.config.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ module.exports = {
77
collectCoverageFrom: ['./src/*.js'],
88
coverageThreshold: {
99
global: {
10-
branches: 75,
11-
functions: 85,
12-
lines: 85,
10+
branches: 80,
11+
functions: 90,
12+
lines: 90,
1313
},
1414
},
1515
};

src/engine.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const createRulesEngine = (
1616

1717
const on = (event, subscriber) => {
1818
const set = eventMap.get(event);
19-
set ? eventMap.set(event, new Set([subscriber])) : set.add(subscriber);
19+
set ? set.add(subscriber) : eventMap.set(event, new Set([subscriber]));
2020
return () => eventMap.get(event).delete(subscriber);
2121
};
2222

src/fact.map.processor.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import { createEvaluator } from './evaluator';
22

33
export const createFactMapProcessor = (validator, opts, emit) => (rule) => {
44
const evaluator = createEvaluator(validator, opts, emit, rule);
5-
return async (factMap, id) => {
6-
emit('debug', { type: 'STARTING_FACT_MAP', rule, mapId: id });
5+
return async (factMap, mapId) => {
6+
emit('debug', { type: 'STARTING_FACT_MAP', rule, mapId, factMap });
77

88
// flags for if there was an error processing the fact map
99
// and if all evaluations in the fact map passed
1010
let error = false;
1111
let passed = true;
1212

1313
const results = (
14-
await Promise.all(Object.entries(factMap).map(evaluator(id)))
14+
await Promise.all(Object.entries(factMap).map(evaluator(mapId)))
1515
).reduce((acc, { factName, ...rest }) => {
1616
if (error) return acc;
1717
error = error || !!rest.error;
@@ -20,11 +20,17 @@ export const createFactMapProcessor = (validator, opts, emit) => (rule) => {
2020
return acc;
2121
}, {});
2222

23-
emit('debug', { type: 'FINISHED_FACT_MAP', rule, mapId: id, results });
23+
emit('debug', {
24+
type: 'FINISHED_FACT_MAP',
25+
rule,
26+
mapId,
27+
results,
28+
passed,
29+
error,
30+
});
2431

25-
// return the results in the same form they were passed in
2632
return {
27-
[id]: {
33+
[mapId]: {
2834
...results,
2935
__passed: passed,
3036
__error: error,

src/index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ type StartingFactMapEvent = {
7777
type: 'STARTING_FACT_MAP';
7878
rule: string;
7979
mapId: string | number;
80+
factMap: FactMap;
81+
};
82+
83+
type FinishedFactMapEvent = {
84+
type: 'FINISHED_FACT_MAP';
85+
rule: string;
86+
mapId: string | number;
87+
results: FactMapResult;
88+
passed: boolean;
89+
error: boolean;
8090
};
8191

8292
type StartingFactEvent = {

src/rule.runner.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,34 +27,36 @@ export const createRuleRunner = (validator, opts, emit) => {
2727
const ruleResults = await Promise.all(
2828
Array.isArray(interpolated)
2929
? interpolated.map(process)
30-
: Object.entries(interpolated).map(([factMap, id]) =>
31-
process(factMap, id),
32-
),
30+
: Object.entries(interpolated).map(async ([k, v]) => process(v, k)),
3331
);
3432

3533
// create the context and evaluate whether the rules have passed or errored in a single loop
36-
const { passed, error, context } = ruleResults.reduce(
37-
({ passed, error, context }, result) => {
34+
const { passed, error, resultsContext } = ruleResults.reduce(
35+
({ passed, error, resultsContext }, result) => {
3836
if (error) return { error };
3937
passed =
4038
passed && Object.values(result).every(({ __passed }) => __passed);
4139
error = Object.values(result).some(({ __error }) => __error);
42-
return { passed, error, context: { ...context, ...result } };
40+
return {
41+
passed,
42+
error,
43+
resultsContext: { ...resultsContext, ...result },
44+
};
4345
},
4446
{
4547
passed: true,
4648
error: false,
47-
context: {},
49+
resultsContext: {},
4850
},
4951
);
5052

51-
const nextContext = { ...opts.context, results: context };
53+
const nextContext = { ...opts.context, results: resultsContext };
5254
const ret = (rest = {}) => ({
5355
[rule]: {
5456
__error: error,
5557
__passed: passed,
5658
...rest,
57-
results: ruleResults,
59+
results: resultsContext,
5860
},
5961
});
6062

@@ -93,7 +95,7 @@ export const createRuleRunner = (validator, opts, emit) => {
9395
rule,
9496
interpolated,
9597
context: opts.context,
96-
result: { actions: actionResults, results: ruleResults },
98+
result: { actions: actionResults, results: resultsContext },
9799
});
98100
return actionResults;
99101
})

test/engine.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,31 @@ describe('rules engine', () => {
4040
expect(call).toHaveBeenCalledWith({ message: 'Who are you?' });
4141
});
4242

43+
it('should execute a rule using a named fact map', async () => {
44+
const rules = {
45+
salutation: {
46+
when: {
47+
myFacts: {
48+
firstName: { is: { type: 'string', pattern: '^J' } },
49+
},
50+
},
51+
then: {
52+
actions: [{ type: 'log', params: { message: 'Hi friend!' } }],
53+
},
54+
otherwise: {
55+
actions: [{ type: 'call', params: { message: 'Who are you?' } }],
56+
},
57+
},
58+
};
59+
engine.setRules(rules);
60+
await engine.run({ firstName: 'John' });
61+
expect(log).toHaveBeenCalledWith({ message: 'Hi friend!' });
62+
log.mockClear();
63+
await engine.run({ firstName: 'Bill' });
64+
expect(log).not.toHaveBeenCalled();
65+
expect(call).toHaveBeenCalledWith({ message: 'Who are you?' });
66+
});
67+
4368
it('should process nested rules', async () => {
4469
const rules = {
4570
salutation: {

test/events.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import createRulesEngine, { RulesEngine } from '../src';
2+
import { createAjvValidator } from './validators';
3+
4+
describe('events', () => {
5+
let engine: RulesEngine;
6+
let log: jest.Mock;
7+
let call: jest.Mock;
8+
9+
beforeEach(() => {
10+
log = jest.fn();
11+
call = jest.fn();
12+
engine = createRulesEngine(createAjvValidator(), {
13+
actions: { log, call },
14+
});
15+
});
16+
17+
it('should unsubscribe', async () => {
18+
const rules = {
19+
salutation: {
20+
when: {
21+
myFacts: {
22+
firstName: { is: { type: 'string', pattern: '^J' } },
23+
},
24+
},
25+
then: {
26+
actions: [{ type: 'log', params: { message: 'Hi friend!' } }],
27+
},
28+
otherwise: {
29+
actions: [{ type: 'call', params: { message: 'Who are you?' } }],
30+
},
31+
},
32+
};
33+
engine.setRules(rules);
34+
const subscriber = jest.fn();
35+
const unsub = engine.on('debug', subscriber);
36+
await engine.run({ firstName: 'John' });
37+
expect(subscriber).toHaveBeenCalled();
38+
unsub();
39+
subscriber.mockClear();
40+
await engine.run({ firstName: 'John' });
41+
expect(subscriber).not.toHaveBeenCalled();
42+
});
43+
44+
it('should subscribe to debug events', async () => {
45+
const rules = {
46+
salutation: {
47+
when: {
48+
myFacts: {
49+
firstName: { is: { type: 'string', pattern: '^J' } },
50+
},
51+
},
52+
then: {
53+
actions: [{ type: 'log', params: { message: 'Hi friend!' } }],
54+
},
55+
otherwise: {
56+
actions: [{ type: 'call', params: { message: 'Who are you?' } }],
57+
},
58+
},
59+
};
60+
engine.setRules(rules);
61+
const subscriber = jest.fn();
62+
const context = { firstName: 'John' };
63+
engine.on('debug', subscriber);
64+
await engine.run(context);
65+
66+
expect(subscriber).toHaveBeenNthCalledWith(
67+
1,
68+
expect.objectContaining({
69+
type: 'STARTING_RULE',
70+
rule: 'salutation',
71+
interpolated: rules.salutation.when,
72+
context,
73+
}),
74+
);
75+
76+
expect(subscriber).toHaveBeenNthCalledWith(
77+
2,
78+
expect.objectContaining({
79+
type: 'STARTING_FACT_MAP',
80+
rule: 'salutation',
81+
mapId: 'myFacts',
82+
factMap: rules.salutation.when.myFacts,
83+
}),
84+
);
85+
86+
expect(subscriber).toHaveBeenNthCalledWith(
87+
3,
88+
expect.objectContaining({
89+
type: 'STARTING_FACT',
90+
rule: 'salutation',
91+
mapId: 'myFacts',
92+
factName: 'firstName',
93+
}),
94+
);
95+
96+
expect(subscriber).toHaveBeenNthCalledWith(
97+
4,
98+
expect.objectContaining({
99+
type: 'EXECUTED_FACT',
100+
rule: 'salutation',
101+
mapId: 'myFacts',
102+
path: undefined,
103+
factName: 'firstName',
104+
value: context.firstName,
105+
resolved: context.firstName,
106+
}),
107+
);
108+
109+
expect(subscriber).toHaveBeenNthCalledWith(
110+
5,
111+
expect.objectContaining({
112+
type: 'EVALUATED_FACT',
113+
rule: 'salutation',
114+
mapId: 'myFacts',
115+
path: undefined,
116+
factName: 'firstName',
117+
value: 'John',
118+
resolved: 'John',
119+
is: { type: 'string', pattern: '^J' },
120+
result: { result: true },
121+
}),
122+
);
123+
124+
expect(subscriber).toHaveBeenNthCalledWith(
125+
6,
126+
expect.objectContaining({
127+
type: 'FINISHED_FACT_MAP',
128+
rule: 'salutation',
129+
mapId: 'myFacts',
130+
results: {
131+
firstName: { result: true, value: 'John', resolved: 'John' },
132+
},
133+
passed: true,
134+
error: false,
135+
}),
136+
);
137+
138+
expect(subscriber).toHaveBeenNthCalledWith(
139+
7,
140+
expect.objectContaining({
141+
type: 'FINISHED_RULE',
142+
rule: 'salutation',
143+
interpolated: rules.salutation.when,
144+
context,
145+
result: {
146+
actions: [{ type: 'log', params: { message: 'Hi friend!' } }],
147+
results: {
148+
myFacts: expect.objectContaining({
149+
firstName: {
150+
result: true,
151+
value: 'John',
152+
resolved: 'John',
153+
},
154+
}),
155+
},
156+
},
157+
}),
158+
);
159+
});
160+
});

test/validators.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ export const createAjvValidator = () => {
44
const ajv = new Ajv2019();
55
return async (object: any, schema: any) => {
66
const validate = ajv.compile(schema);
7-
const result = validate(object);
8-
return { result, errors: validate.errors };
7+
return { result: validate(object) };
98
};
109
};

0 commit comments

Comments
 (0)