Advanced Conditional Inference Engine in TypeScript
This challenge involves building a flexible and powerful conditional inference engine in TypeScript. Such an engine is crucial for applications requiring dynamic decision-making based on complex, nested conditions, such as business rule engines, configuration systems, or intelligent agents. You will implement a system that can evaluate a set of rules against given data and determine which actions should be taken.
Problem Description
You need to design and implement a ConditionEngine class in TypeScript that can evaluate a list of Rule objects against a given Context object. Each Rule has a condition and an action. The condition is a structured representation of logical expressions (AND, OR, NOT) involving properties within the Context. The action is a function that is executed if the rule's condition evaluates to true.
The engine should support:
- Nested conditions: Conditions can be arbitrarily nested using logical operators.
- Multiple data types: Conditions should be able to compare various data types (numbers, strings, booleans, arrays).
- Logical operators: Support for AND (
&&), OR (||), and NOT (!) operators. - Comparison operators: Support for equality (
===), inequality (!==), greater than (>), less than (<), greater than or equal to (>=), and less than or equal to (<=). - Array operations: Support for checking if an array contains a specific element (
includes) or if all elements satisfy a condition. - Execution of actions: When a rule's condition is met, its associated action should be executed with the
Contextas an argument.
The ConditionEngine should have a method evaluate(context: Context): Promise<void> that iterates through all registered rules, evaluates their conditions against the provided context, and executes the actions of all satisfied rules. The evaluation should be asynchronous to allow for potential future extensions involving I/O or complex computations within conditions.
Key Requirements:
ContextType: Define a flexibleContexttype that can hold any key-value pairs.ConditionStructure: Define a discriminated union type forConditionthat represents different types of logical and comparison operations.LogicalCondition: Contains an operator (&&,||,!) and an array of otherConditionobjects.ComparisonCondition: Contains akey(path to a property inContext), anoperator(e.g.,===,>,includes), and avalueto compare against.
RuleType: Define aRuletype with aconditionand anaction(a function that takesContextand returnsPromise<void>).ConditionEngineClass:- A
constructorthat can optionally take an initial list ofRuleobjects. - An
addRule(rule: Rule): voidmethod. - An
evaluate(context: Context): Promise<void>method.
- A
- Condition Evaluation Logic: Implement a robust recursive function to evaluate the
Conditionstructure against aContext. This function should handle:- Accessing nested properties in
Context(e.g.,user.address.city). - Applying logical operators.
- Applying comparison operators.
- Handling array
includeschecks.
- Accessing nested properties in
Expected Behavior:
- Rules are evaluated sequentially.
- If a rule's condition evaluates to
true, itsactionis invoked. - All rules whose conditions are met should have their actions executed.
- The
evaluatemethod should return aPromisethat resolves when all applicable actions have completed.
Edge Cases:
- Empty
Contextobject. - Conditions referencing non-existent keys in the
Context. - Conditions with unsupported operators.
- Conditions with invalid nesting.
Contextvalues beingnullorundefined.
Examples
Example 1: Simple Comparison and AND Logic
// Input Definitions (for clarity, not actual code for input)
type Context = {
user: {
age: number;
isActive: boolean;
};
order: {
total: number;
items: string[];
};
};
type Rule = {
condition: Condition;
action: (context: Context) => Promise<void>;
};
// Example Condition structure:
// {
// operator: '&&',
// conditions: [
// { key: 'user.age', operator: '>=', value: 18 },
// { key: 'user.isActive', operator: '===', value: true }
// ]
// }
const mockAction1 = jest.fn((context: Context) => console.log("Action 1 executed"));
const mockAction2 = jest.fn((context: Context) => console.log("Action 2 executed"));
const rules: Rule[] = [
{
condition: {
operator: '&&',
conditions: [
{ key: 'user.age', operator: '>=', value: 18 },
{ key: 'user.isActive', operator: '===', value: true }
]
},
action: mockAction1
},
{
condition: {
operator: '||',
conditions: [
{ key: 'order.total', operator: '>', value: 100 },
{ key: 'order.items', operator: 'includes', value: 'premium_service' }
]
},
action: mockAction2
}
];
const context: Context = {
user: { age: 25, isActive: true },
order: { total: 150, items: ['standard_item'] }
};
// Expected Output (after calling engine.evaluate(context)):
// Action 1 executed (Rule 1 condition met: 25 >= 18 AND true === true)
// Action 2 executed (Rule 2 condition met: 150 > 100 OR 'standard_item' includes 'premium_service')
// Explanation:
// Rule 1's condition evaluates to true because the user is 25 (>= 18) and isActive is true.
// Rule 2's condition evaluates to true because the order total is 150 (> 100), even though 'premium_service' is not in items.
// Both mockAction1 and mockAction2 should be called.
Example 2: Nested NOT and Array includes
// Using the same Context and Rule types as Example 1
const mockAction3 = jest.fn((context: Context) => console.log("Action 3 executed"));
const mockAction4 = jest.fn((context: Context) => console.log("Action 4 executed"));
const rules: Rule[] = [
{
condition: {
operator: '!',
conditions: [ // NOT block
{
operator: '&&',
conditions: [
{ key: 'user.age', operator: '<', value: 18 },
{ key: 'user.isActive', operator: '===', value: false }
]
}
]
},
action: mockAction3
},
{
condition: {
key: 'order.items',
operator: 'includes',
value: 'free_trial'
},
action: mockAction4
}
];
const context: Context = {
user: { age: 16, isActive: true },
order: { total: 50, items: ['basic_item', 'free_trial'] }
};
// Expected Output (after calling engine.evaluate(context)):
// Action 3 executed (Rule 1 condition met: NOT (16 < 18 AND true === false) -> NOT (true AND false) -> NOT(false) -> true)
// Action 4 executed (Rule 2 condition met: ['basic_item', 'free_trial'] includes 'free_trial')
// Explanation:
// Rule 1's condition: The inner AND condition (16 < 18 AND true === false) is (true AND false), which is false. The NOT operator inverts this to true. So, Action 3 is executed.
// Rule 2's condition: The 'order.items' array includes 'free_trial'. So, Action 4 is executed.
Example 3: Edge Case - Non-existent Key and undefined Comparison
// Using the same Context and Rule types as Example 1
const mockAction5 = jest.fn((context: Context) => console.log("Action 5 executed"));
const rules: Rule[] = [
{
condition: {
operator: '&&',
conditions: [
{ key: 'user.nonExistentProp', operator: '===', value: undefined },
{ key: 'user.age', operator: '>', value: 0 }
]
},
action: mockAction5
}
];
const context: Context = {
user: { age: 30, isActive: true },
order: { total: 20, items: [] }
};
// Expected Output (after calling engine.evaluate(context)):
// Action 5 executed
// Explanation:
// The `ConditionEngine` should gracefully handle non-existent keys. When accessing `user.nonExistentProp`, it should return `undefined`.
// The condition `undefined === undefined` evaluates to true.
// The condition `user.age > 0` (30 > 0) also evaluates to true.
// Therefore, the AND condition is true, and Action 5 is executed.
Constraints
- The depth of nested conditions is unlimited.
- The
Contextobject can have deeply nested properties, up to 10 levels deep. - The number of rules in the engine can be up to 1000.
- The
evaluatemethod should complete within 500ms for a typical scenario (100 rules, shallow conditions). - Input
Contextvalues will be primitive types (string, number, boolean), arrays of primitives,null, orundefined. actionfunctions are assumed to be relatively quick and do not perform heavy I/O that would significantly exceed the performance constraint.
Notes
- Consider how to efficiently access nested properties in the
Context. A helper function for path traversal might be useful. - Think about the recursive nature of evaluating conditions.
- Ensure your
Conditiontype is expressive enough to cover all specified logical and comparison operators. - For array operations like
includes, you will need to handle cases where theContextvalue at the specifiedkeyis not an array. - The
NOToperator should be applied to a single condition within itsconditionsarray. - You might want to create helper functions for evaluating comparison operators and logical operators to keep the main evaluation logic clean.
- Error handling for invalid condition structures or unsupported operators should be considered, though not explicitly required for the core functionality. You can either throw errors or log warnings.