Skip to content

Commit 6042cad

Browse files
author
Christopher Pardy
committed
Split conditions into classes
Split conditions into classes that can be independently evaluated. This will allow new condition classes to be added by changing the implementation of the condition constructor.
1 parent 50920ab commit 6042cad

18 files changed

+823
-581
lines changed

src/condition.js

Lines changed: 0 additions & 156 deletions
This file was deleted.

src/condition/all-condition.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict'
2+
3+
import Condition from './condition'
4+
import prioritizeAndRun from './prioritize-and-run'
5+
6+
export default class AllCondition extends Condition {
7+
constructor (properties, conditionConstructor) {
8+
super(properties)
9+
if (!Object.prototype.hasOwnProperty.call(properties, 'all')) {
10+
throw new Error('AllCondition: constructor "all" property required')
11+
}
12+
if (!Array.isArray(properties.all)) {
13+
throw new Error('AllCondition: constructor "all" must be an array')
14+
}
15+
this.all = properties.all.map((c) => conditionConstructor.construct(c))
16+
this.operator = 'all'
17+
// boolean conditions always have a priority; default 1
18+
this.priority = this.priority || 1
19+
}
20+
21+
/**
22+
* Converts the condition into a json-friendly structure
23+
* @param {Boolean} stringify - whether to return as a json string
24+
* @returns {string,object} json string or json-friendly object
25+
*/
26+
toJSON (stringify = true) {
27+
const props = super.toJSON(false)
28+
props.all = this.all.map((c) => c.toJSON(false))
29+
if (stringify) {
30+
return JSON.stringify(props)
31+
}
32+
return props
33+
}
34+
35+
/**
36+
* Takes the fact result and compares it to the condition 'value', using the operator
37+
* LHS OPER RHS
38+
* <fact + params + path> <operator> <value>
39+
*
40+
* @param {Almanac} almanac
41+
* @param {Map} operatorMap - map of available operators, keyed by operator name
42+
* @returns {Boolean} - evaluation result
43+
*/
44+
evaluate (almanac, operatorMap, conditionMap) {
45+
return Promise.all([
46+
prioritizeAndRun(this.all, 'all', almanac, operatorMap, conditionMap),
47+
super.evaluate(almanac, operatorMap, conditionMap)
48+
]).then(([result, evaluateResult]) => {
49+
evaluateResult.result = result.result
50+
evaluateResult.all = result.conditions
51+
evaluateResult.operator = 'all'
52+
return evaluateResult
53+
})
54+
}
55+
56+
skip () {
57+
const skipResult = super.skip()
58+
skipResult.all = this.all.map((c) => c.skip())
59+
skipResult.operator = 'all'
60+
return skipResult
61+
}
62+
}

src/condition/any-condition.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict'
2+
3+
import Condition from './condition'
4+
import prioritizeAndRun from './prioritize-and-run'
5+
6+
export default class AnyCondition extends Condition {
7+
constructor (properties, conditionConstructor) {
8+
super(properties)
9+
if (!Object.prototype.hasOwnProperty.call(properties, 'any')) {
10+
throw new Error('AnyCondition: constructor "any" property required')
11+
}
12+
if (!Array.isArray(properties.any)) {
13+
throw new Error('AnyCondition: constructor "any" must be an array')
14+
}
15+
this.any = properties.any.map((c) => conditionConstructor.construct(c))
16+
this.operator = 'any'
17+
// boolean conditions always have a priority; default 1
18+
this.priority = this.priority || 1
19+
}
20+
21+
/**
22+
* Converts the condition into a json-friendly structure
23+
* @param {Boolean} stringify - whether to return as a json string
24+
* @returns {string,object} json string or json-friendly object
25+
*/
26+
toJSON (stringify = true) {
27+
const props = super.toJSON(false)
28+
props.any = this.any.map((c) => c.toJSON(false))
29+
if (stringify) {
30+
return JSON.stringify(props)
31+
}
32+
return props
33+
}
34+
35+
/**
36+
* Takes the fact result and compares it to the condition 'value', using the operator
37+
* LHS OPER RHS
38+
* <fact + params + path> <operator> <value>
39+
*
40+
* @param {Almanac} almanac
41+
* @param {Map} operatorMap - map of available operators, keyed by operator name
42+
* @returns {Boolean} - evaluation result
43+
*/
44+
evaluate (almanac, operatorMap, conditionMap) {
45+
return Promise.all([
46+
prioritizeAndRun(this.any, 'any', almanac, operatorMap, conditionMap),
47+
super.evaluate(almanac, operatorMap, conditionMap)
48+
]).then(([result, evaluateResult]) => {
49+
evaluateResult.any = result.conditions
50+
evaluateResult.operator = 'any'
51+
evaluateResult.result = result.result
52+
return evaluateResult
53+
})
54+
}
55+
56+
skip () {
57+
const skipResult = super.skip()
58+
skipResult.any = this.any.map((c) => c.skip())
59+
skipResult.operator = 'any'
60+
return skipResult
61+
}
62+
}

src/condition/comparison-condition.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use strict'
2+
3+
import deepClone from 'clone'
4+
import debug from '../debug'
5+
import Condition from './condition'
6+
7+
export default class ComparisonCondition extends Condition {
8+
constructor (properties) {
9+
super(properties)
10+
if (!Object.prototype.hasOwnProperty.call(properties, 'fact')) { throw new Error('Condition: constructor "fact" property required') }
11+
if (!Object.prototype.hasOwnProperty.call(properties, 'operator')) { throw new Error('Condition: constructor "operator" property required') }
12+
if (!Object.prototype.hasOwnProperty.call(properties, 'value')) { throw new Error('Condition: constructor "value" property required') }
13+
this.fact = properties.fact
14+
if (Object.prototype.hasOwnProperty.call(properties, 'params')) {
15+
this.params = properties.params
16+
}
17+
if (Object.prototype.hasOwnProperty.call(properties, 'path')) {
18+
this.path = properties.path
19+
}
20+
this.operator = properties.operator
21+
this.value = properties.value
22+
}
23+
24+
/**
25+
* Converts the condition into a json-friendly structure
26+
* @param {Boolean} stringify - whether to return as a json string
27+
* @returns {string,object} json string or json-friendly object
28+
*/
29+
toJSON (stringify = true) {
30+
const props = super.toJSON(false)
31+
props.operator = this.operator
32+
props.value = this.value
33+
props.fact = this.fact
34+
if (this.params) {
35+
props.params = this.params
36+
}
37+
if (this.path) {
38+
props.path = this.path
39+
}
40+
if (stringify) {
41+
return JSON.stringify(props)
42+
}
43+
return props
44+
}
45+
46+
getPriority (almanac) {
47+
const priority = super.getPriority(almanac)
48+
return priority !== undefined ? priority : almanac.factPriority(this.fact)
49+
}
50+
51+
/**
52+
* Takes the fact result and compares it to the condition 'value', using the operator
53+
* LHS OPER RHS
54+
* <fact + params + path> <operator> <value>
55+
*
56+
* @param {Almanac} almanac
57+
* @param {Map} operatorMap - map of available operators, keyed by operator name
58+
* @returns {Boolean} - evaluation result
59+
*/
60+
evaluate (almanac, operatorMap, conditionMap) {
61+
if (!almanac) return Promise.reject(new Error('almanac required'))
62+
if (!operatorMap) return Promise.reject(new Error('operatorMap required'))
63+
64+
const op = operatorMap.get(this.operator)
65+
if (!op) { return Promise.reject(new Error(`Unknown operator: ${this.operator}`)) }
66+
67+
return Promise.all([
68+
almanac.getValue(this.value),
69+
almanac.factValue(this.fact, this.params, this.path),
70+
super.evaluate(almanac, operatorMap, conditionMap)
71+
]).then(([rightHandSideValue, leftHandSideValue, evaluateResult]) => {
72+
const result = op.evaluate(leftHandSideValue, rightHandSideValue)
73+
debug(
74+
`condition::evaluate <${JSON.stringify(leftHandSideValue)} ${
75+
this.operator
76+
} ${JSON.stringify(rightHandSideValue)}?> (${result})`
77+
)
78+
evaluateResult.result = result
79+
evaluateResult.fact = this.fact
80+
if (this.params) {
81+
evaluateResult.params = deepClone(this.params)
82+
}
83+
if (this.path) {
84+
evaluateResult.path = this.path
85+
}
86+
evaluateResult.operator = this.operator
87+
evaluateResult.value = deepClone(this.value)
88+
evaluateResult.factResult = leftHandSideValue
89+
evaluateResult.valueResult = rightHandSideValue
90+
return evaluateResult
91+
})
92+
}
93+
94+
skip () {
95+
const skipResult = super.skip()
96+
skipResult.fact = this.fact
97+
if (this.params) {
98+
skipResult.params = deepClone(this.params)
99+
}
100+
if (this.path) {
101+
skipResult.path = this.path
102+
}
103+
skipResult.operator = this.operator
104+
skipResult.value = deepClone(this.value)
105+
return skipResult
106+
}
107+
}

0 commit comments

Comments
 (0)