-
Notifications
You must be signed in to change notification settings - Fork 469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
version 7 - Design #372
Comments
A nice feature would be a synchronous API. To be honest, it seems unusual to provide only an async API when there's no I/O. If there was a synchronous API and the consumer REALLY wanted to use it in an async way, they could wrap it in a promise. |
So the asynchronous nature of the engine comes from the fact that Currently I've been working under the assumption this this is the interface for a interface Condition {
priority: number;
evaluate(almanac: Alamanc): Promise<ConditionResult>;
} Moving to have such a minimal interface for conditions it allows the introduction of new condition types which means that the engine is easier to extend, but the downside(?) is that there's very little for the An alternative approach makes the conditions and other data structures purely just data and moves the execution / evaluation of the rules entirely into the engine. This would allow for us to take the same rules and run them in an sync or async engine. For something like that you'd usually use a visitor pattern: interface Condition {
visit(visitor: Visitor): void
}
class ComparisonConditions {
visit(visitor: Visitor): void {
visitor.visitComparison(this.fact, this.operator, this.value);
}
} The tradeoff between these two patterns is really about if we want to lock down the features that can be described by conditions or if we want to lock down the more general behavior of the engine. |
ParametersThe rules engine current supports what we've called condition references, which are useful constructs for doing rule inheritance. What would immensely up the power of these would be to add support for parameters that could be passed to the reference so in your rule you'd have something like: {
"condition": "sharedCondition",
"parameter": { "minAge": 21 }
} Then in your shared condition you could do something like: {
"fact": "age",
"operator": "greaterThanInclusive",
"value": { "parameter": "minAge" }
} This seems great but it opens up a few rabbit holes worth going down Passing Facts to ParametersThis is one is no brainer but you should be able to do this: {
"condition": "sharedCondition",
"paramters": {
"minAge": {
"fact": "legalDrinkingAge",
"parameters": { "country": "US" }
}
}
} But if you can pass facts to the parameters of a condition reference then why can't you pass them to the parameters of a fact, and then while we're at it why no allow passing a parameter to the parameters of a fact, so something like: {
"fact": "age",
"operator": "greaterThanInclusive",
"value": {
"fact": "legalDrinkingAge",
"parameters": {
"country": { "parameter": "country" }
}
}
} This is great and we should support it but limit it to one level deep, effectively this is already done in events. Using Parameters on the Left hand side.Let's assume we want to re-use something like this but check different facts: {
"fact": "word",
"operator": "endsWith",
"value": "y"
} In the current state of the engine we have a strict rule which is only facts on the left hand side, not a big deal since we mostly have symmetric operators, still if we wanted to change what facts we were checking for we'd have to do something like: {
"fact": "letterY",
"operator": "endOf",
"value": { "parameter": "text" }
} This is annoying but it also means that we can't write this rule without having this highly specific fact in place, wouldn't it be better if we could do this?: {
"parameter" : "text",
"operator": "endsWith",
"value": "y"
} Ok, makes sense but if we've done that why bother limiting what can be on the left hand side of the operator at-all? {
"value": "y",
"operator": "endOf",
"value": { "parameter": "text" }
} Admittedly we'll need a better way to specify the fact that the left-hand-side will be a value but that's solvable. What else can we do with parameters?Once we have parameters the question becomes, are they useful outside of condition references and the answer is without a doubt YES and they specifically enable us to cross another functional barrier which is dealing with iteration. Currently if you have a fact with the value
What I'd propose is to add the {
"for": ["game", "trying", "salad"],
"as": "word",
"every": {
"parameter": "word",
"operator": "endsWith",
"value": "ing"
}
} we could now put that list of words behind a RecapSo what have we added?
|
FunctionsWith parameters and iterations there's really only one last bridge to cross and that's functions. In short a function call should let you modify values before they are passed to an operator. Something like {
"fact": "age",
"operator:" "greaterThenInclusve",
"value": {
"fn:coallesce": [
{ "parameter": "minAge" },
21
]
}
} |
JSON StructureThe current JSON structure is ok at being something that can be statically checked but not the best. For instance this object: {
"fact": "test",
"operator": "equal",
"value": 2,
"all": []
} This will be treated like an empty {
"type": "comparison",
"fact": "test",
"operator": "equal",
"value": 2,
"all": []
} This type is unambiguous because of the type field. However it signals a move away from a format that tries to be more human readable and towards a format that is more machine readable Assume we were adding support for functions you could have something like: { "fn:max": [10, { "fact": "count" }] } or you could have something like {
"type": "function",
"function": "max",
"args": [10, { "fact": "count" }]
} This also informs when we need to know about things like operators, in the current version we know about operators at execution when we resolve the string name to an actual Operator instance. However if we wanted to add an {
"type": "aggregate",
"operator": "all",
"conditions": []
} If we're comfortable knowing about operators a head of time then we could represent comparisons like: { "equal": [{ "fact": "test" }, 2] } |
IteratorsOne thing you can't do in version 6 of the JSON rules engine is run the same set of conditions across multiple items in a list. For our example let's assume you have a list of items and you want to know if every item in the list is greater than 2. Option 1 - Dedicated Iterator, pluggable aggregate opreations{
"type": "foreach",
"foreach": { "fact": "items" },
"as": "item",
"operation": "all",
"condition": {
"type": "comparsion",
"operator": "greaterThan",
"operands": [{ "parameter": "item"}, 2]
}
} In this case the Option 2 - Dedicated Aggregator, pluggable lists{
"type": "aggregate",
"operator": "all",
"conditions": {
"type": "foreach",
"foreach": { "fact": "items" },
"as": "item",
"condition": {
"type": "comparison",
"operator": "greaterThan",
"operands": [{ "parameter": "item" }, 2]
}
}
} In this case the foreach becomes a special iterator that is fed into the same |
I cannot stress enough how powerful this would be. However, I would say there is no need for "parameters" per se, just pass in params that match the params of the predefined conditionals. |
Starting a thread here to put notes on a version 7 design.
Version 7 is an opportunity to make a number of changes that have been suggested in issues filed here that would make the JSON rules engine more extensible at the cost of breaking backwards compatibility with the current 6.x versions.
Extensibility vs. Structure
With version 7 we want to consider the tradeoff between extensibility and having a known structure. For instance introducing the ability to add custom
Condition
classes or customRule
subclasses allows for the system to be highly extensible but makes it much harder to reason about the input and output of the rules engine without knowing about all these extensions.Ultimately we need to draw this line somewhere and my initial inclination is to draw it in favor of more extensibility in order to allow the broadest possible use of the rules engine.
Concepts
Starting from a clean stale the following basic concepts will be part of the rules engine
Facts
Facts are things that are known by the rules engine during execution.
Almanac
The Almanac is the system for storing and looking up
Facts
.Conditions
Conditions are executable, contain a priority, and return a result containing a boolean value. Generally there are 2 types of conditions, comparison conditions which check the value of a fact against another value, and composite conditions like
all
,any
, andnot
which use 1 or more nested conditions to produce a result. By being fully extensible it's possible to introduce other types of conditions.Rules
Rules are also executable and contain a priority. They generally are comprised of a set of conditions which are executed. Rules produce events as a result of execution.
Engine
The engine provides the mechanism to evaluate rules, and conditions. It includes mechanisms to resolve the values of facts or other special objects. Changing the engine can significantly change the behavior of the system.
The text was updated successfully, but these errors were encountered: