diff --git a/README.md b/README.md index 55eaf7c..52c139e 100644 --- a/README.md +++ b/README.md @@ -586,7 +586,7 @@ process.env.DEBUG = "true"; Rule introspection is built into `RulePilot` and can be used to inspect a rule and get information about it. When `RulePilot` introspects a rule it attempts to determine, for each distinct result in the rule, the -distribution of inputs which will satisfy the rule resolving to said result provided an input constraint. +distribution of inputs which will satisfy the rule resolving to said result, provided an input criteria. For example, using introspection we can ask Rule Pilot the following question: @@ -656,6 +656,9 @@ The following will be returned by the `introspection`: }] ``` +We can also ask `RulePilot` to introspect the rule to determine what countries would receive aa discount and what that +discount would be if the totalCheckoutPrice was 100. + ```typescript const subjects = ["country"]; const constraint = { field: "totalCheckoutPrice", value: 100 }; @@ -676,10 +679,12 @@ The following will be returned by the `introspection`: }] ``` -Each object in the `response` criteria which are possible inputs for the rule to evaluate to the result provided. +Each object in the `response` criteria which are possible inputs for the rule to evaluate to the result with the criteria +provided. -Although calculating such results might seem trivial, it can be in fact be quite a complex thing to do especially when -dealing with complex rules with multiple nested conditions comprised of many different operators. +The results from the introspection are sanitized before being returned. This essentially means that if the introspection +results in these possibilities `price == 10` || `price > 10` || `price == 20`, the results will be sanitized to +simply `price > 10`. Similarly to mutations, it is possible to enable debug mode on the introspection feature by setting the environment variable `DEBUG="true"`. @@ -691,7 +696,7 @@ process.env.DEBUG = "true"; **Note:** Introspection requires a [Granular](#granular-example) rule to be passed to it, otherwise `RulePilot` will throw an `RuleTypeError`. -**Note** Introspection does not yet work with these constraint operators: +**Note** Introspection does not yet work with these operators: - `contains` - `not contains` - `contains any` @@ -758,7 +763,10 @@ const rule: Rule = builder builder.constraint("weight", ">=", 2), builder.condition( "all", - [builder.constraint("color", "==", "green"), builder.constraint("discount", ">", 20)], + [ + builder.constraint("color", "==", "green"), + builder.constraint("discount", ">", 20) + ], { price: 10 } ) ]), diff --git a/src/services/introspector.ts b/src/services/introspector.ts index 0a50523..76ba6ad 100644 --- a/src/services/introspector.ts +++ b/src/services/introspector.ts @@ -23,7 +23,7 @@ export class Introspector { introspect( rule: Rule, - constraint: Omit, + criteria: Omit, subjects: string[] ): IntrospectionResult[] { // We care about all the possible values for the subjects which will satisfy @@ -41,7 +41,7 @@ export class Introspector { rule.conditions[i] = this.#reverseNoneToAll(rule.conditions[i]); rule.conditions[i] = this.#removeIrrelevantConstraints( rule.conditions[i], - [...subjects, constraint.field] + [...subjects, criteria.field] ); } @@ -63,7 +63,7 @@ export class Introspector { return; } - const result = rule.subRule.result; + const result = rule.subRule?.result ?? "default"; delete rule.parent.result; delete rule.subRule.result; @@ -80,7 +80,7 @@ export class Introspector { // We introspect the conditions to determine the possible values for the subjects for (const condition of conditions) { - const { values } = this.#introspectConditions(condition, constraint); + const { values } = this.#introspectConditions(condition, criteria); if (!values) continue; const key = condition.result ?? "default"; @@ -365,14 +365,14 @@ export class Introspector { * Extracts all the possible combinations of criteria from the condition which are * self-consistent to the condition passing. * @param condition The condition to introspect. - * @param input The constraint passed as an input to the introspection. + * @param criteria The constraint passed as an input to the introspection. * @param parentType The type of the parent condition. * @param parentResults The parent condition results. * @param depth The current recursion depth. */ #introspectConditions( condition: Condition, - input: Omit, + criteria: Omit, parentType: keyof Condition = null, parentResults: Map = new Map(), depth: number = 0 @@ -427,7 +427,7 @@ export class Introspector { } if ("all" === type) { - if (!this.test(candidates, input, c, depth)) { + if (!this.test(candidates, criteria, c, depth)) { candidates = []; clearLocal = true; Logger.debug(` ${gap}- Clearing local candidates...`); @@ -484,7 +484,7 @@ export class Introspector { for (const [field, candidates] of results.entries()) { for (const c of candidates) { const parentRes = parentResults.get(field) ?? []; - if (this.test(parentRes, input, c, depth)) valid.push(c); + if (this.test(parentRes, criteria, c, depth)) valid.push(c); } } @@ -506,7 +506,7 @@ export class Introspector { for (const [field, candidates] of results.entries()) { for (const c of candidates) { const parentRes = parentResults.get(field) ?? []; - if (!this.test(parentRes, input, c, depth)) allPass = false; + if (!this.test(parentRes, criteria, c, depth)) allPass = false; } } @@ -551,7 +551,13 @@ export class Introspector { for (const c of conditions) { // Introspect the condition and append the results to the parent results const d = depth + 1; - const res = this.#introspectConditions(c, input, type, parentResults, d); + const res = this.#introspectConditions( + c, + criteria, + type, + parentResults, + d + ); if (res.void) parentResults = new Map(); else parentResults = res.values; @@ -587,13 +593,13 @@ export class Introspector { * * Testing happens by considering each item in the list as linked by an AND * @param candidates The result candidates to test against. - * @param input The constraint which was input to the introspection. + * @param criteria The constraint which was input to the introspection. * @param item The constraint item to test against the candidates. * @param depth The current recursion depth. */ protected test( candidates: Constraint[], - input: Omit, + criteria: Omit, item: Constraint, depth: number ): boolean { @@ -601,8 +607,8 @@ export class Introspector { candidates = candidates.filter((r) => r.field === item.field); // Add the input constraint to the results (if it also matches the field) - if (input.field === item.field) { - candidates.push({ ...input, operator: "==" }); + if (criteria.field === item.field) { + candidates.push({ ...criteria, operator: "==" }); } // Test that the constraint does not breach the results @@ -1063,37 +1069,6 @@ export class Introspector { return results; } - /** - * Remove the provided sub-rule needle from the haystack condition - * @param node The node to remove. - * @param haystack The condition to search in and remove the sub-rule from. - */ - #removeNode(node: Record, haystack: Condition): Condition { - // Clone the condition so that we can modify it - const clone = JSON.parse(JSON.stringify(haystack)); - - // Iterate over each node in the condition - const type = this.#objectDiscovery.conditionType(clone); - for (let i = 0; i < clone[type].length; i++) { - // Check if the current node is the node we are looking for - if (JSON.stringify(clone[type][i]) == JSON.stringify(node)) { - // Remove the node from the cloned object - clone[type].splice(i, 1); - - // If the node is now empty, we can prune it - if (Array.isArray(clone[type]) && !clone[type].length) return null; - continue; - } - - // If the node is a condition, recurse - if (this.#objectDiscovery.isCondition(clone[type][i])) { - clone[type][i] = this.#removeNode(node, clone[type][i]); - } - } - - return this.#stripNullProps(clone); - } - /** * Remove the provided item needle from the haystack list * @param needle The item to find and remove. @@ -1105,31 +1080,6 @@ export class Introspector { ); } - /** - * Checks if a condition has a constraint with the provided field. - * @param field The field to check for. - * @param haystack The condition to search in. - */ - #hasConstraintField(field: string, haystack: Condition): boolean { - // Iterate over each node in the condition - const type = this.#objectDiscovery.conditionType(haystack); - for (let i = 0; i < haystack[type].length; i++) { - const node = haystack[type][i]; - - // If the node is a constraint, check if it has the field we are looking for - if (this.#objectDiscovery.isConstraint(node) && node.field === field) { - return true; - } - - // If the node is a condition, recurse - if (this.#objectDiscovery.isCondition(node)) { - return this.#hasConstraintField(field, node); - } - } - - return false; - } - /** * Test Routine: * We check each test item against the entire list of results.