From efcbe36c1fda9c10c39d4f7e05ecc10f7337f206 Mon Sep 17 00:00:00 2001 From: stefan-gorules <127550877+stefan-gorules@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:00:34 +0100 Subject: [PATCH] feat: switch node (#103) * feat: switch node * refactors * add tests * fix: change digraph to stable * fix: test models * fix: readme * add aml test, disable miri for graph test due to v8 * Update README.md * Update README.md --------- Co-authored-by: Ivan Miletic Co-authored-by: Ivan Miletic <60513195+ivanmiletic@users.noreply.github.com> --- README.md | 187 ++++-- core/engine/Cargo.toml | 1 + core/engine/src/decision.rs | 2 +- core/engine/src/handler/decision.rs | 2 +- core/engine/src/handler/graph.rs | 96 ++-- core/engine/src/handler/mod.rs | 1 + core/engine/src/handler/traversal.rs | 240 ++++++++ core/engine/src/model/mod.rs | 37 +- core/engine/tests/engine.rs | 62 +- test-data/graphs/aml.json | 537 ++++++++++++++++++ test-data/graphs/company-analysis.json | 390 +++++++++++++ .../graphs/decision-table-discounts.json | 168 ++++++ test-data/graphs/decision-table-shipping.json | 398 +++++++++++++ test-data/graphs/insurance-breakdown.json | 421 ++++++++++++++ test-data/graphs/merch-bags.json | 171 ++++++ test-data/graphs/multi-switch.json | 498 ++++++++++++++++ test-data/switch-node.json | 167 ++++++ 17 files changed, 3274 insertions(+), 104 deletions(-) create mode 100644 core/engine/src/handler/traversal.rs create mode 100644 test-data/graphs/aml.json create mode 100644 test-data/graphs/company-analysis.json create mode 100644 test-data/graphs/decision-table-discounts.json create mode 100644 test-data/graphs/decision-table-shipping.json create mode 100644 test-data/graphs/insurance-breakdown.json create mode 100644 test-data/graphs/merch-bags.json create mode 100644 test-data/graphs/multi-switch.json create mode 100644 test-data/switch-node.json diff --git a/README.md b/README.md index d654d841..4cf25d01 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ZEN Engine is a cross-platform, Open-Source Business Rules Engine (BRE). It is written in **Rust** and provides native bindings for **NodeJS** and **Python**. ZEN Engine allows to load and execute [JSON Decision Model (JDM)](https://gorules.io/docs/rules-engine/json-decision-model) from JSON files. -JSON Decision Model +GoRules JSON Decision Model An open-source React editor is available on our [JDM Editor](https://github.com/gorules/jdm-editor) repo. @@ -13,7 +13,7 @@ An open-source React editor is available on our [JDM Editor](https://github.com/ ZEN Engine is built as embeddable BRE for your **Rust**, **NodeJS** or **Python** applications. It parses JDM from JSON content. It is up to you to obtain the JSON content, e.g. from file system, database or service call. -If you are looking for a **Decision-as-a-Service** (DaaS) over REST, take a look at [GoRules Cloud](https://gorules.io). +If you are looking for a complete **BRMS**, take a look at self-hosted [GoRules BRMS](https://gorules.io) or [GoRules Cloud](https://gorules.io). ### Rust @@ -35,6 +35,7 @@ async fn evaluate() { let result = decision.evaluate(&json!({ "input": 12 })).await; } ``` +For more advanced use cases where you want to load multiple decisions you can use loaders. To learn more please visit [Rust Docs](https://gorules.io/docs/rules-engine/engines/rust). ### NodeJS @@ -56,7 +57,7 @@ import fs from 'fs/promises'; })(); ``` -For more advanced use cases where you want to load multiple decisions you can use loaders. To learn more please visit [NodeJS Docs](bindings/nodejs) +For more advanced use cases where you want to load multiple decisions you can use loaders. To learn more please visit [NodeJS Docs](https://gorules.io/docs/rules-engine/engines/nodejs). ### Python @@ -77,46 +78,98 @@ decision = engine.create_decision(content) result = decision.evaluate({"input": 15}) ``` -For more advanced use cases where you want to load multiple decisions you can use loaders. To learn more please visit [Python Docs](bindings/python) +For more advanced use cases where you want to load multiple decisions you can use loaders. To learn more please visit [Python Docs](https://gorules.io/docs/rules-engine/engines/python). ## JSON Decision Model (JDM) -JDM is a modeling standard for business decisions and business rules and is stored in a JSON format. Decision models are represented by graphs. Graphs are built using nodes and edges. Edges are used to pass the data from one node to another (left-side to right-side). +GoRules JDM (JSON Decision Model) is a modeling framework designed to streamline the representation and implementation of decision models. -An open-source version of the React Component is available on our [JDM Editor](https://github.com/gorules/jdm-editor) repo. +#### Understanding GoRules JDM +At its core, GoRules JDM revolves around the concept of decision models as interconnected graphs stored in JSON format. +These graphs capture the intricate relationships between various decision points, conditions, and outcomes in a GoRules Zen-Engine. -You can try [Free Online Editor](https://editor.gorules.io) with built in Simulator. +Graphs are made by linking nodes with edges, which act like pathways for moving information from one node to another, usually from the left to the right. -[JSON Example](test-data/credit-analysis.json) +The Input node serves as an entry for all data relevant to the context, while the Output nodes produce the result of decision-making process. The progression of data follows a path from the Input Node to the Output Node, traversing all interconnected nodes in between. As the data flows through this network, it undergoes evaluation at each node, and connections determine where the data is passed along the graph. -Input node contains all data sent in the context, and Output node returns the data from the decision. Data flows from the Input Node towards Output Node evaluating all the Nodes in between and passing the data where nodes are connected. +To see JDM Graph in action you can use [Free Online Editor](https://editor.gorules.io) with built in Simulator. -### Decision Tables +There are 5 main node types in addition to a graph Input Node (Request) and Output Node (Response): +* Decision Table Node +* Switch Node +* Function Node +* Expression Node +* Decision Node -Decision table is a node which allows business users to easily modify and add new rules in an intuitive way using spreadsheet like interface. The structure of decision table is defined by its schema. Schema consists of inputs and outputs. +### Decision Table Node -Decision tables are evaluated row by row from top to bottom, and depending on the hit policy a result is calculated. +#### Overview -**Inputs** +Tables provide a structured representation of decision-making processes, allowing developers and business users to express complex rules in a clear and concise manner. -Input fields are commonly (qualified) names for example `cart.total` or `customer.country`. +Decision Table + +#### Structure + +At the core of the Decision Table is its schema, defining the structure with inputs and outputs. Inputs encompass business-friendly expressions using the ZEN Expression Language, accommodating a range of conditions such as equality, numeric comparisons, boolean values, date time functions, array functions and more. The schema's outputs dictate the form of results generated by the Decision Table. +Inputs and outputs are expressed through a user-friendly interface, often resembling a spreadsheet. This facilitates easy modification and addition of rules, enabling business users to contribute to decision logic without delving into intricate code. + +#### Evaluation Process + +Decision Tables are evaluated row by row, from top to bottom, adhering to a specified hit policy. +Single row is evaluated via Inputs columns, from left to right. Each input column represents `AND` operator. If cell is empty that column is evaluated **truthfully**, independently of the value. + +If a single cell within a row fails (due to error, or otherwise), the row is skipped. + +**HitPolicy** + +The hit policy determines the outcome calculation based on matching rules. + +The result of the evaluation is: + +* **an object** if the hit policy of the decision table is `first` and a rule matched. The structure is defined by the output fields. Qualified field names with a dot (.) inside lead to nested objects. +* **`null`/`undefined`** if no rule matched in `first` hit policy +* **an array of objects** if the hit policy of the decision table is `collect` (one array item for each matching rule) or empty array if no rules match + + +#### Inputs + +In the assessment of rules or rows, input columns embody the `AND` operator. The values typically consist of (qualified) names, such as `customer.country` or `customer.age`. + +There are two types of evaluation of inputs, `Unary` and `Expression`. + + +**Unary Evaluation** + +Unary evaluation is usually used when we would like to compare single fields from incoming context separately, for example `customer.country` and `cart.total` . It is activated when a column has `field` defined in its schema. + +***Example*** + +For the input: ```json { - "cart": { - "total": 1000 - }, "customer": { "country": "US" + }, + "cart": { + "total": 1500 } } ``` -Inputs are using business-friendly ZEN Expression Language. The language is designed to follow these principles: +Unary test + + +This evaluation translates to + +``` +IF customer.country == 'US' AND cart.total > 1000 THEN {"fees": {"percent": 2}} +ELSE IF customer.country == 'US' THEN {"fees": {"flat": 30}} +ELSE IF customer.country == 'CA' OR customer.country == 'MX' THEN {"fees": {"flat": 50}} +ELSE {"fees": {"flat": 150}} +``` -* Side-effect free -* Dynamic types -* Simple syntax for broad audiences List shows basic example of the unary tests in the Input Fields: @@ -137,15 +190,43 @@ List shows basic example of the unary tests in the Input Fields: Note: For the full list please visit [ZEN Expression Language](https://gorules.io/docs/rules-engine/expression-language/). +**Expression Evaluation** + +Expression evaluation is used when we would like to create more complex evaluation logic inside single cell. It allows us to compare multiple fields from the incoming context inside same cell. + +It can be used by providing an empty `Selector (field)` inside column configuration. + +***Example*** + +For the input: + +```json +{ + "transaction": { + "country": "US", + "createdAt": "2023-11-20T19:00:25Z", + "amount": 10000 + } +} +``` + +Expression + +``` +IF time(transaction.createdAt) > time("17:00:00") AND transaction.amount > 1000 THEN {"status": "reject"} +ELSE {"status": "approve"} +``` + +Note: For the full list please visit [ZEN Expression Language](https://gorules.io/docs/rules-engine/expression-language/). + + **Outputs** -The result of the decisionTableNode evaluation is: +Output columns serve as the blueprint for the data that the decision table will generate when the conditions are met during evaluation. -* an object if the hit policy of the decision table is FIRST and a rule matched. The structure is defined by the output fields. Qualified field names with a dot (.) inside lead to nested objects. -* `null`/`undefined` if no rule matched in FIRST hit policy -* an array of objects if the hit policy of the decision table is COLLECT (one array item for each matching rule) or empty array if no rules match +When a row in the decision table satisfies its specified conditions, the output columns determine the nature and structure of the information that will be returned. Each output column represents a distinct field, and the collective set of these fields forms the output or result associated with the validated row. This mechanism allows decision tables to precisely define and control the data output. -Example: +***Example*** Screenshot 2023-03-10 at 22 57 04 @@ -162,20 +243,58 @@ And the result would be: } } ``` +### Switch Node -### Functions +The Switch node in GoRules JDM introduces a dynamic branching mechanism to decision models, enabling the graph to diverge based on conditions. -Function nodes are JavaScript lambdas that allow for quick and easy parsing, re-mapping or otherwise modifying the data. Inputs of the node are provided as function's arguments. Functions are executed on top of Google's V8 Engine that is built in into the ZEN Engine. +Conditions are written in a Zen Expression Language. + +By incorporating the Switch node, decision models become more flexible and context-aware. This capability is particularly valuable in scenarios where diverse decision logic is required based on varying inputs. The Switch node efficiently manages branching within the graph, enhancing the overall complexity and realism of decision models in GoRules JDM, making it a pivotal component for crafting intelligent and adaptive systems. + +The Switch node preserves the incoming data without modification; it forwards the entire context to the output branch(es). + +Switch / Branching node + +#### HitPolicy +There are two HitPolicy options for the switch node, `first` and `collect`. + +In the context of a first hit policy, the graph branches to the initial matching condition, analogous to the behavior observed in a table. Conversely, under a collect hit policy, the graph extends to all branches where conditions hold true, allowing branching to multiple paths. + +Note: If there are multiple edges from the same condition, there is no guaranteed order of execution. + +### Functions Node + +Function nodes are JavaScript snippets that allow for quick and easy parsing, re-mapping or otherwise modifying the data using JavaScript. Inputs of the node are provided as function's arguments. Functions are executed on top of Google's V8 Engine Isolates that are built into the ZEN Engine. + +Function timeout is set to a 50ms. ```js -const handler = (input) => { - return input; +const handler = (input, {dayjs, Big}) => { + return { + ...input, + someField: 'hello' + }; }; ``` -### Decision +There are two built in libraries: +* [dayjs](https://www.npmjs.com/package/dayjs) - for Date Manipulation +* [big.js](https://www.npmjs.com/package/big.js) - for arbitrary-precision decimal arithmetic. -Decision is a special node whose purpose is for decision model to have an ability to call other/re-usable decision models during an execution. +### Expression Node +The Expression node serves as a tool for transforming input objects into alternative objects using the Zen Expression Language. When specifying the output properties, each property requires a separate row. These rows are defined by two fields: +- Key - qualified name of the output property +- Value - value expressed through the Zen Expression Language + +Note: Any errors within the Expression node will bring the graph to a halt. + +Expression node + +### Decision Node + +The "Decision" node is designed to extend the capabilities of decision models. Its function is to invoke and reuse other decision models during execution. + +By incorporating the "Decision" node, developers can modularize decision logic, promoting reusability and maintainability in complex systems. ## Support matrix @@ -191,8 +310,8 @@ We do not support linux-musl for now as we are relying on the GoogleV8 engine to ## Contribution -Contributions are welcome 🎉. +JDM standard is growing and we need to keep tight control over its development and roadmap as there are number of companies that are using GoRules Zen-Engine and GoRules BRMS. +For this reason we can't accept any code contributions at this moment, apart from help with documentation and additional tests. ## License - [MIT License]() diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index 4c10a74b..9f072a15 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -21,6 +21,7 @@ serde_json = { workspace = true, features = ["arbitrary_precision"] } serde = { workspace = true, features = ["derive"] } serde_v8 = { version = "0.141.0" } once_cell = { version = "1.18.0" } +fixedbitset = "0.4.2" futures = "0.3.29" v8 = { version = "0.81.0" } zen-expression = { path = "../expression", version = "0.15.1" } diff --git a/core/engine/src/decision.rs b/core/engine/src/decision.rs index 5bad1355..58899456 100644 --- a/core/engine/src/decision.rs +++ b/core/engine/src/decision.rs @@ -62,7 +62,7 @@ where context: &Value, options: EvaluationOptions, ) -> Result> { - let decision_graph = DecisionGraph::try_new(DecisionGraphConfig { + let mut decision_graph = DecisionGraph::try_new(DecisionGraphConfig { max_depth: options.max_depth.unwrap_or(5), trace: options.trace.unwrap_or_default(), loader: self.loader.clone(), diff --git a/core/engine/src/handler/decision.rs b/core/engine/src/handler/decision.rs index f0b7fd04..cc945d74 100644 --- a/core/engine/src/handler/decision.rs +++ b/core/engine/src/handler/decision.rs @@ -30,7 +30,7 @@ impl DecisionHandler { }?; let sub_decision = self.loader.load(&content.key).await?; - let sub_tree = DecisionGraph::try_new(DecisionGraphConfig { + let mut sub_tree = DecisionGraph::try_new(DecisionGraphConfig { content: sub_decision.deref(), max_depth: self.max_depth, loader: self.loader.clone(), diff --git a/core/engine/src/handler/graph.rs b/core/engine/src/handler/graph.rs index 08bcafcf..c1afec0a 100644 --- a/core/engine/src/handler/graph.rs +++ b/core/engine/src/handler/graph.rs @@ -4,13 +4,9 @@ use std::time::Instant; use anyhow::anyhow; use petgraph::algo::is_cyclic_directed; -use petgraph::graph::NodeIndex; -use petgraph::prelude::DiGraph; -use petgraph::visit::Topo; -use petgraph::Direction; use serde::ser::SerializeMap; use serde::{Deserialize, Serialize, Serializer}; -use serde_json::{Map, Value}; +use serde_json::Value; use thiserror::Error; use crate::handler::decision::DecisionHandler; @@ -18,12 +14,13 @@ use crate::handler::expression::ExpressionHandler; use crate::handler::function::FunctionHandler; use crate::handler::node::NodeRequest; use crate::handler::table::zen::DecisionTableHandler; +use crate::handler::traversal::{GraphWalker, StableDiDecisionGraph}; use crate::loader::DecisionLoader; -use crate::model::{DecisionContent, DecisionNode, DecisionNodeKind}; +use crate::model::{DecisionContent, DecisionNodeKind}; use crate::{EvaluationError, NodeError}; pub struct DecisionGraph<'a, L: DecisionLoader> { - graph: DiGraph<&'a DecisionNode, usize>, + graph: StableDiDecisionGraph<'a>, loader: Arc, trace: bool, max_depth: u8, @@ -43,7 +40,7 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { config: DecisionGraphConfig<'a, L>, ) -> Result { let content = config.content; - let mut graph = DiGraph::new(); + let mut graph = StableDiDecisionGraph::new(); let mut index_map = HashMap::new(); for node in &content.nodes { @@ -53,7 +50,7 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { index_map.insert(node_id, node_index); } - for (weight, edge) in content.edges.iter().enumerate() { + for (_, edge) in content.edges.iter().enumerate() { let source_index = index_map.get(&edge.source_id).ok_or_else(|| { DecisionGraphValidationError::MissingNode(edge.source_id.to_string()) })?; @@ -62,7 +59,7 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { DecisionGraphValidationError::MissingNode(edge.target_id.to_string()) })?; - graph.add_edge(source_index.clone(), target_index.clone(), weight); + graph.add_edge(source_index.clone(), target_index.clone(), edge); } Ok(Self { @@ -98,18 +95,12 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { fn node_kind_count(&self, kind: DecisionNodeKind) -> usize { self.graph - .raw_nodes() - .iter() - .filter(|node| node.weight.kind == kind) + .node_weights() + .filter(|weight| weight.kind == kind) .count() } - fn incoming_nodes(&self, node_id: NodeIndex) -> Vec<&DecisionNode> { - let neighbors = self.graph.neighbors_directed(node_id, Direction::Incoming); - neighbors.map(|neighbor| self.graph[neighbor]).collect() - } - - pub async fn evaluate(&self, context: &Value) -> Result { + pub async fn evaluate(&mut self, context: &Value) -> Result { let root_start = Instant::now(); self.validate().map_err(|e| NodeError { @@ -124,13 +115,14 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { }); } - let mut dfs = Topo::new(&self.graph); - let mut node_data = HashMap::<&str, Value>::default(); + let mut walker = GraphWalker::new(&self.graph); let mut node_traces = self.trace.then(|| HashMap::default()); - let default_patch = Value::Object(Map::new()); + while let Some((nid, walker_metadata)) = walker.next(&mut self.graph) { + if let Some(_) = walker.get_node_data(nid) { + continue; + } - while let Some(nid) = dfs.next(&self.graph) { let node = self.graph[nid]; let start = Instant::now(); @@ -142,28 +134,15 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { }; } - let incoming_nodes = self.incoming_nodes(nid); - let incoming_data = - incoming_nodes - .iter() - .fold(Value::Object(Map::new()), |mut prev, &curr| { - let data = node_data - .get(curr.id.as_str()) - .unwrap_or_else(|| &default_patch); - - merge_json(&mut prev, data, true); - prev - }); - let node_request = NodeRequest { node, iteration: self.iteration, - input: incoming_data, + input: walker.incoming_node_data(&self.graph, nid), }; match node.kind { DecisionNodeKind::InputNode => { - node_data.insert(&node.id, context.clone()); + walker.set_node_data(nid, context.clone()); trace!({ input: Value::Null, output: Value::Null, @@ -189,6 +168,17 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { trace: node_traces, }); } + DecisionNodeKind::SwitchNode { .. } => { + walker.set_node_data(nid, node_request.input.clone()); + trace!({ + input: node_request.input.clone(), + output: node_request.input.clone(), + name: node.name.clone(), + id: node.id.clone(), + performance: Some(format!("{:?}", start.elapsed())), + trace_data: Some(walker_metadata), + }); + } DecisionNodeKind::FunctionNode { .. } => { let res = FunctionHandler::new(self.trace) .handle(&node_request) @@ -198,7 +188,7 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { node_id: node.id.clone(), })?; - node_data.insert(&node.id, res.output.clone()); + walker.set_node_data(nid, res.output.clone()); trace!({ input: node_request.input, output: res.output, @@ -217,7 +207,7 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { node_id: node.id.to_string(), })?; - node_data.insert(&node.id, res.output.clone()); + walker.set_node_data(nid, res.output.clone()); trace!({ input: node_request.input, output: res.output, @@ -236,7 +226,7 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { source: e.into(), })?; - node_data.insert(&node.id, res.output.clone()); + walker.set_node_data(nid, res.output.clone()); trace!({ input: node_request.input, output: res.output, @@ -255,7 +245,7 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { source: e.into(), })?; - node_data.insert(&node.id, res.output.clone()); + walker.set_node_data(nid, res.output.clone()); trace!({ input: node_request.input, output: res.output, @@ -275,28 +265,6 @@ impl<'a, L: DecisionLoader> DecisionGraph<'a, L> { } } -fn merge_json(doc: &mut Value, patch: &Value, top_level: bool) { - if !patch.is_object() && !patch.is_array() && top_level { - return; - } - - if doc.is_object() && patch.is_object() { - let map = doc.as_object_mut().unwrap(); - for (key, value) in patch.as_object().unwrap() { - if value.is_null() { - map.remove(key.as_str()); - } else { - merge_json(map.entry(key.as_str()).or_insert(Value::Null), value, false); - } - } - } else if doc.is_array() && patch.is_array() { - let arr = doc.as_array_mut().unwrap(); - arr.extend(patch.as_array().unwrap().clone()); - } else { - *doc = patch.clone(); - } -} - #[derive(Debug, Error)] pub enum DecisionGraphValidationError { #[error("Invalid input node count: {0}")] diff --git a/core/engine/src/handler/mod.rs b/core/engine/src/handler/mod.rs index cadb02f9..c41450c5 100644 --- a/core/engine/src/handler/mod.rs +++ b/core/engine/src/handler/mod.rs @@ -5,3 +5,4 @@ pub mod table; pub(crate) mod graph; pub(crate) mod node; +pub(crate) mod traversal; diff --git a/core/engine/src/handler/traversal.rs b/core/engine/src/handler/traversal.rs new file mode 100644 index 00000000..8db83925 --- /dev/null +++ b/core/engine/src/handler/traversal.rs @@ -0,0 +1,240 @@ +use std::collections::HashMap; + +use fixedbitset::FixedBitSet; +use petgraph::data::DataMap; +use petgraph::matrix_graph::Zero; +use petgraph::prelude::{EdgeIndex, NodeIndex, StableDiGraph}; +use petgraph::visit::{EdgeRef, IntoNeighbors, IntoNodeIdentifiers, Reversed, VisitMap, Visitable}; +use petgraph::{Incoming, Outgoing}; +use serde_json::{json, Map, Value}; + +use zen_expression::isolate::Isolate; + +use crate::model::{ + DecisionEdge, DecisionNode, DecisionNodeKind, SwitchStatement, SwitchStatementHitPolicy, +}; + +pub(crate) type StableDiDecisionGraph<'a> = StableDiGraph<&'a DecisionNode, &'a DecisionEdge>; + +pub(crate) struct GraphWalker { + ordered: FixedBitSet, + to_visit: Vec, + node_data: HashMap, + iter: usize, +} + +const ITER_MAX: usize = 1_000; + +impl GraphWalker { + pub fn new(graph: &StableDiDecisionGraph) -> Self { + let mut topo = Self::empty(graph); + topo.extend_with_initials(graph); + topo + } + + fn extend_with_initials(&mut self, g: &StableDiDecisionGraph) { + // find all initial nodes (nodes without incoming edges) + self.to_visit + .extend(g.node_identifiers().filter(move |&nid| { + g.neighbors_directed(nid, Incoming).count().is_zero() + && !g.neighbors_directed(nid, Outgoing).count().is_zero() + })); + } + + fn empty(graph: &StableDiDecisionGraph) -> Self { + Self { + ordered: graph.visit_map(), + to_visit: Vec::new(), + node_data: Default::default(), + iter: 0, + } + } + + pub fn reset(&mut self, g: &StableDiDecisionGraph) { + self.ordered.clear(); + self.to_visit.clear(); + self.extend_with_initials(g); + + self.iter += 1; + } + + pub fn get_node_data(&self, node_id: NodeIndex) -> Option<&Value> { + self.node_data.get(&node_id) + } + + pub fn set_node_data(&mut self, node_id: NodeIndex, value: Value) { + self.node_data.insert(node_id, value); + } + + pub fn incoming_node_data(&self, g: &StableDiDecisionGraph, node_id: NodeIndex) -> Value { + self.merge_node_data(g.neighbors_directed(node_id, Incoming)) + } + + pub fn merge_node_data(&self, iter: I) -> Value + where + I: Iterator, + { + let default_map = Value::Object(Map::new()); + iter.fold(Value::Object(Map::new()), |mut prev, curr| { + let data = self.node_data.get(&curr).unwrap_or(&default_map); + + merge_json(&mut prev, data, true); + prev + }) + } + + pub fn next(&mut self, g: &mut StableDiDecisionGraph) -> Option<(NodeIndex, Value)> { + if self.iter >= ITER_MAX { + return None; + } + // Take an unvisited element and find which of its neighbors are next + let mut value = Value::Null; + while let Some(nid) = self.to_visit.pop() { + let decision_node = *g.node_weight(nid)?; + if self.ordered.is_visited(&nid) { + continue; + } + + self.ordered.visit(nid); + + if let DecisionNodeKind::SwitchNode { content } = &decision_node.kind { + let mut input_data = self.incoming_node_data(g, nid); + let input_context = json!({ "$": &input_data }); + merge_json(&mut input_data, &input_context, true); + + let isolate = Isolate::default(); + isolate.inject_env(&input_data); + + let mut statement_iter = content.statements.iter(); + let valid_statements: Vec<&SwitchStatement> = match content.hit_policy { + SwitchStatementHitPolicy::First => statement_iter + .find(|&s| switch_statement_evaluate(&isolate, &s)) + .into_iter() + .collect(), + SwitchStatementHitPolicy::Collect => statement_iter + .filter(|&s| switch_statement_evaluate(&isolate, &s)) + .collect(), + }; + + let valid_statements_trace: Value = valid_statements + .iter() + .map(|&statement| json!({ "id": &statement.id })) + .collect(); + value = json!({ "statements": valid_statements_trace }); + + // Remove all non-valid edges + let edges_to_remove: Vec = g + .edges_directed(nid, Outgoing) + .filter(|edge| { + edge.weight().source_handle.as_ref().map_or(true, |handle| { + !valid_statements.iter().any(|s| s.id == *handle) + }) + }) + .map(|edge| edge.id()) + .collect(); + let edges_remove_count = edges_to_remove.len(); + for edge in edges_to_remove { + remove_edge_recursive(g, edge); + } + + // Reset the graph if whole branch has been removed + if edges_remove_count > 0 { + self.reset(g); + continue; + } + } + + for neigh in g.neighbors(nid) { + // Look at each neighbor, and those that only have incoming edges + // from the already ordered list, they are the next to visit. + if Reversed(&*g) + .neighbors(neigh) + .all(|b| self.ordered.is_visited(&b)) + { + self.to_visit.push(neigh); + } + } + + return Some((nid, value)); + } + + None + } +} + +fn switch_statement_evaluate<'a>( + isolate: &Isolate<'a>, + switch_statement: &'a SwitchStatement, +) -> bool { + if switch_statement.condition.is_empty() { + return true; + } + + isolate + .run_standard(switch_statement.condition.as_str()) + .map_or(false, |v| v.as_bool().unwrap_or(false)) +} + +fn remove_edge_recursive(g: &mut StableDiDecisionGraph, edge_id: EdgeIndex) { + let Some((source_nid, target_nid)) = g.edge_endpoints(edge_id) else { + return; + }; + + g.remove_edge(edge_id); + + // Remove dead branches from target + let target_incoming_count = g.edges_directed(target_nid, Incoming).count(); + if target_incoming_count.is_zero() { + let edge_ids: Vec = g + .edges_directed(target_nid, Outgoing) + .map(|edge| edge.id()) + .collect(); + + edge_ids.iter().for_each(|edge_id| { + remove_edge_recursive(g, edge_id.clone()); + }); + + if g.edges(target_nid).count().is_zero() { + g.remove_node(target_nid); + } + } + + // Remove dead branches from source + let source_outgoing_count = g.edges_directed(source_nid, Outgoing).count(); + if source_outgoing_count.is_zero() { + let edge_ids: Vec = g + .edges_directed(source_nid, Incoming) + .map(|edge| edge.id()) + .collect(); + + edge_ids.iter().for_each(|edge_id| { + remove_edge_recursive(g, edge_id.clone()); + }); + + if g.edges(source_nid).count().is_zero() { + g.remove_node(source_nid); + } + } +} + +fn merge_json(doc: &mut Value, patch: &Value, top_level: bool) { + if !patch.is_object() && !patch.is_array() && top_level { + return; + } + + if doc.is_object() && patch.is_object() { + let map = doc.as_object_mut().unwrap(); + for (key, value) in patch.as_object().unwrap() { + if value.is_null() { + map.remove(key.as_str()); + } else { + merge_json(map.entry(key.as_str()).or_insert(Value::Null), value, false); + } + } + } else if doc.is_array() && patch.is_array() { + let arr = doc.as_array_mut().unwrap(); + arr.extend(patch.as_array().unwrap().clone()); + } else { + *doc = patch.clone(); + } +} diff --git a/core/engine/src/model/mod.rs b/core/engine/src/model/mod.rs index cf78f8d0..a610f48d 100644 --- a/core/engine/src/model/mod.rs +++ b/core/engine/src/model/mod.rs @@ -17,11 +17,13 @@ pub struct DecisionContent { #[cfg_attr(feature = "bincode", derive(Encode, Decode))] #[serde(rename_all = "camelCase")] pub struct DecisionEdge { + pub id: String, pub source_id: String, pub target_id: String, + pub source_handle: Option, } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] #[serde(rename_all = "camelCase")] pub struct DecisionNode { @@ -32,6 +34,12 @@ pub struct DecisionNode { pub kind: DecisionNodeKind, } +impl PartialEq for DecisionNode { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] #[serde(tag = "type")] @@ -43,6 +51,7 @@ pub enum DecisionNodeKind { DecisionNode { content: DecisionNodeContent }, DecisionTableNode { content: DecisionTableContent }, ExpressionNode { content: ExpressionNodeContent }, + SwitchNode { content: SwitchNodeContent }, } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -103,3 +112,29 @@ pub struct Expression { pub key: String, pub value: String, } + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct SwitchNodeContent { + #[serde(default)] + pub hit_policy: SwitchStatementHitPolicy, + pub statements: Vec, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub struct SwitchStatement { + pub id: String, + pub condition: String, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)] +#[cfg_attr(feature = "bincode", derive(Encode, Decode))] +#[serde(rename_all = "camelCase")] +pub enum SwitchStatementHitPolicy { + #[default] + First, + Collect, +} diff --git a/core/engine/tests/engine.rs b/core/engine/tests/engine.rs index f0b64c4e..8f94e519 100644 --- a/core/engine/tests/engine.rs +++ b/core/engine/tests/engine.rs @@ -1,16 +1,17 @@ +use std::fs; use std::io::Read; use std::ops::Deref; use std::path::Path; use std::sync::Arc; use serde::Deserialize; -use serde_json::json; +use serde_json::{json, Value}; use zen_engine::loader::{LoaderError, MemoryLoader}; -use zen_engine::model::DecisionNodeKind; +use zen_engine::model::{DecisionContent, DecisionNodeKind}; use zen_engine::{DecisionEngine, EvaluationError, EvaluationOptions}; -use crate::support::{create_fs_loader, load_raw_test_data, load_test_data}; +use crate::support::{create_fs_loader, load_raw_test_data, load_test_data, test_data_root}; mod support; @@ -180,3 +181,58 @@ async fn engine_function_imports() { assert!(result.dayjs_valid); assert!(result.moment_valid); } + +#[tokio::test] +async fn engine_switch_node() { + let engine = DecisionEngine::new(create_fs_loader()); + + let switch_node_r = engine + .evaluate("switch-node.json", &json!({ "color": "yellow" })) + .await; + + let table = switch_node_r.unwrap(); + println!("{table:?}"); +} + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn engine_graph_tests() { + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct TestCase { + input: Value, + output: Value, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct TestData { + tests: Vec, + #[serde(flatten)] + decision_content: DecisionContent, + } + + let engine = DecisionEngine::default(); + + let graphs_path = Path::new(test_data_root().as_str()).join("graphs"); + let file_list = std::fs::read_dir(graphs_path).unwrap(); + for maybe_file in file_list { + let Ok(file) = maybe_file else { + panic!("Failed to read DirEntry {maybe_file:?}"); + }; + + let file_name = file.file_name().to_str().map(|s| s.to_string()).unwrap(); + let file_contents = fs::read_to_string(file.path()).expect("valid file data"); + let test_data: TestData = serde_json::from_str(&file_contents).expect("Valid JSON"); + + let decision = engine.create_decision(test_data.decision_content.into()); + for test_case in test_data.tests { + let result = decision.evaluate(&test_case.input).await.unwrap().result; + let input = test_case.input; + assert_eq!( + test_case.output, result, + "Decision file: {file_name}.\nInput:\n {input:#?}" + ); + } + } +} diff --git a/test-data/graphs/aml.json b/test-data/graphs/aml.json new file mode 100644 index 00000000..35320d41 --- /dev/null +++ b/test-data/graphs/aml.json @@ -0,0 +1,537 @@ +{ + "tests": [ + { + "input": { + "transaction": { + "id": "123456789", + "timestamp": "2023-11-30T12:34:56Z", + "amountUSD": 100.50, + "currency": "EUR", + "status": "pending" + }, + "customer": { + "id": "987654321", + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "address": { + "street": "123 Main St", + "city": "Anytown", + "state": "CA", + "zipCode": "12345", + "country": "USA" + } + }, + "merchant": { + "id": "567890123", + "name": "SuperMart", + "category": "Retail", + "reputation": 0.81, + "location": { + "country": "RU" + } + } + }, + "output": { + "breakdown": { + "amber": 0, + "green": 3, + "red": 1 + }, + "flags": { + "eurSanctions": "red", + "globalSanctions": "green", + "merchantReputation": "green", + "transactionAmount": "green" + }, + "overallFlag": "red" + } + }, + { + "input": { + "transaction": { + "id": "123456789", + "timestamp": "2023-11-30T12:34:56Z", + "amountUSD": 30000.50, + "currency": "USD", + "status": "pending" + }, + "customer": { + "id": "987654321", + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "address": { + "street": "123 Main St", + "city": "Anytown", + "state": "CA", + "zipCode": "12345", + "country": "USA" + } + }, + "merchant": { + "id": "567890123", + "name": "SuperMart", + "category": "Retail", + "reputation": 0.55, + "location": { + "street": "456 High St", + "city": "AnotherTown", + "state": "CA", + "zipCode": "54321", + "country": "USA" + } + } + }, + "output": { + "breakdown": { + "amber": 2, + "green": 2, + "red": 0 + }, + "flags": { + "globalSanctions": "green", + "merchantReputation": "amber", + "transactionAmount": "amber", + "usdSanction": "green" + }, + "overallFlag": "amber" + } + }, + { + "input": { + "transaction": { + "id": "123456789", + "timestamp": "2023-11-30T12:34:56Z", + "amountUSD": 30000, + "currency": "EUR", + "status": "pending" + }, + "customer": { + "id": "987654321", + "firstName": "John", + "lastName": "Doe", + "email": "john.doe@example.com", + "tier": "enterprise", + "address": { + "country": "FR" + } + }, + "merchant": { + "id": "567890123", + "name": "SuperMart", + "category": "Retail", + "reputation": 0.55, + "location": { + "street": "456 High St", + "city": "AnotherTown", + "state": "CA", + "zipCode": "54321", + "country": "VE" + } + } + }, + "output": { + "breakdown": { + "amber": 2, + "green": 2, + "red": 0 + }, + "flags": { + "eurSanctions": "amber", + "globalSanctions": "green", + "merchantReputation": "amber", + "transactionAmount": "green" + }, + "overallFlag": "amber" + } + } + ], + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "df092198-208d-4526-82a2-2ff4359d9001", + "type": "inputNode", + "position": { + "x": 90, + "y": 330 + }, + "name": "Request" + }, + { + "id": "f7e19179-48b7-4d94-b085-ea3d6c9e829e", + "type": "switchNode", + "position": { + "x": 400, + "y": 440 + }, + "name": "Sanctions", + "content": { + "hitPolicy": "collect", + "statements": [ + { + "id": "5bffb0f5-a69d-46ba-9122-08bae9f6fe92", + "condition": "transaction.currency == \"USD\"" + }, + { + "id": "13737427-cb8c-4c32-bde5-e49ef6fa03e0", + "condition": "transaction.currency == \"EUR\"" + }, + { + "id": "aa630466-3bb1-45e3-9ae8-12eb57451f55", + "condition": "" + } + ] + } + }, + { + "id": "9bc09d87-84ce-4f4c-9c53-317688ee6cc5", + "type": "decisionTableNode", + "position": { + "x": 700, + "y": 520 + }, + "name": "USD Sanctions", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "e03b1f07-7d3a-4a64-9aa5-aca83e4cfa01", + "name": "Merchant country", + "type": "expression", + "field": "merchant.location.country" + } + ], + "outputs": [ + { + "id": "1959963a-2aec-4988-87a5-bfc590ca08c4", + "name": "Output", + "type": "expression", + "field": "flags.usdSanction" + } + ], + "rules": [ + { + "_id": "fca1348e-026f-4356-a443-617fa99909eb", + "e03b1f07-7d3a-4a64-9aa5-aca83e4cfa01": "\"RU\", \"CU\", \"KP\", \"VE\"", + "1959963a-2aec-4988-87a5-bfc590ca08c4": "\"red\"" + }, + { + "_id": "26b794b7-26ce-4e51-9432-cf1d3580a638", + "e03b1f07-7d3a-4a64-9aa5-aca83e4cfa01": "", + "1959963a-2aec-4988-87a5-bfc590ca08c4": "\"green\"" + } + ] + } + }, + { + "id": "3187c4af-565f-482c-805f-9099e9d56fc1", + "type": "decisionTableNode", + "position": { + "x": 700, + "y": 620 + }, + "name": "EUR Sanctions", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "90adfc05-ffdf-490f-ab47-cede8dd899f3", + "name": "Location", + "type": "expression", + "field": "merchant.location.country" + } + ], + "outputs": [ + { + "id": "e49398d3-cc2b-4e5a-81b0-e50fd020b5c0", + "name": "Output", + "type": "expression", + "field": "flags.eurSanctions" + } + ], + "rules": [ + { + "_id": "ff990579-78ff-4a25-9706-eae402a26c4e", + "90adfc05-ffdf-490f-ab47-cede8dd899f3": "\"RU\", \"NK\"", + "e49398d3-cc2b-4e5a-81b0-e50fd020b5c0": "\"red\"" + }, + { + "_id": "380670ff-4a6a-48d6-b430-8cbb6186438b", + "90adfc05-ffdf-490f-ab47-cede8dd899f3": "\"VE\", \"IR\"", + "e49398d3-cc2b-4e5a-81b0-e50fd020b5c0": "\"amber\"" + }, + { + "_id": "5fb3c366-54d5-4a17-bbd0-88f2d65ce99d", + "90adfc05-ffdf-490f-ab47-cede8dd899f3": "", + "e49398d3-cc2b-4e5a-81b0-e50fd020b5c0": "\"green\"" + } + ] + } + }, + { + "id": "fd8f6200-f236-4d00-859e-9a732e553176", + "type": "decisionTableNode", + "position": { + "x": 700, + "y": 720 + }, + "name": "Global Sanctions", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "c31240e0-8761-4c31-9f79-5210a656f820", + "name": "Input", + "type": "expression" + } + ], + "outputs": [ + { + "id": "156ad457-da15-468b-9444-978f37c28463", + "name": "Output", + "type": "expression", + "field": "flags.globalSanctions" + } + ], + "rules": [ + { + "_id": "e19c68ab-6db3-41b2-baef-8b5cca4254a9", + "c31240e0-8761-4c31-9f79-5210a656f820": "\"RU\", \"NK\"", + "156ad457-da15-468b-9444-978f37c28463": "\"red\"" + }, + { + "_id": "380eee74-94aa-4321-a673-878386dc3200", + "c31240e0-8761-4c31-9f79-5210a656f820": "", + "156ad457-da15-468b-9444-978f37c28463": "\"green\"" + } + ] + } + }, + { + "id": "204af8f6-325f-41cc-a5bd-ee99905d8c1f", + "type": "functionNode", + "position": { + "x": 1060, + "y": 360 + }, + "name": "Aggregator", + "content": "/**\n * @param input\n * @param {{\n * dayjs: import('dayjs')\n * Big: import('big.js').BigConstructor\n * }} helpers\n */\nconst handler = (input, { dayjs, Big }) => {\n const flags = input.flags || {};\n const countFlags = (flag) => Object.values(flags).reduce((acc, curr) => {\n return curr == flag ? acc + 1 : acc;\n }, 0);\n\n const breakdown = {\n red: countFlags('red'),\n amber: countFlags('amber'),\n green: countFlags('green')\n };\n\n let overallFlag = 'green';\n if (breakdown.amber > 0) {\n overallFlag = 'amber';\n }\n if (breakdown.red > 0) {\n overallFlag = 'red';\n }\n\n return {\n ...input,\n breakdown,\n overallFlag,\n };\n}" + }, + { + "id": "ac9cf968-98b2-4ba5-a481-28da1e32b649", + "type": "decisionTableNode", + "position": { + "x": 460, + "y": 220 + }, + "name": "Transaction Amount", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "a713c7ef-1db6-4fa4-aca4-590c4092c742", + "name": "Customer Tier", + "type": "expression", + "field": "customer.tier" + }, + { + "id": "360ba5d7-25b0-44ff-9cce-3f365634a42f", + "type": "expression", + "field": "transaction.amountUSD", + "name": "Transaction Amount USD" + } + ], + "outputs": [ + { + "id": "245ca04d-fd91-4c2b-80a5-660ec16d5ac1", + "name": "Transaction amount flag", + "type": "expression", + "field": "flags.transactionAmount" + } + ], + "rules": [ + { + "_id": "3819029a-2446-47ed-ae6c-268c23f06ad2", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "\"enterprise\"", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "> 50_000", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"amber\"" + }, + { + "_id": "831b4587-4f1b-494c-97c3-da2861991779", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "\"enterprise\"", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"green\"" + }, + { + "_id": "94556461-324d-4a4b-a039-2b703203ae64", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "\"business\"", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "> 100_000", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"red\"" + }, + { + "_id": "e4170bfe-9ceb-49d4-9ef8-c56e2d3501e9", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "\"business\"", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "> 20_000", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"amber\"" + }, + { + "_id": "0156a3ca-aca4-42a0-8d6a-f55290501573", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "\"business\"", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"green\"" + }, + { + "_id": "26fbd01d-e355-49c9-88ad-7f5b00d4118a", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "> 50_000", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"red\"" + }, + { + "_id": "65f91c4f-bf69-40fa-86c6-36ad71abbc1b", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "> 10_000", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"amber\"" + }, + { + "_id": "58cf804e-ad94-4ed6-99fe-8263ae8cf7cd", + "a713c7ef-1db6-4fa4-aca4-590c4092c742": "", + "360ba5d7-25b0-44ff-9cce-3f365634a42f": "", + "245ca04d-fd91-4c2b-80a5-660ec16d5ac1": "\"green\"" + } + ] + } + }, + { + "id": "57b828f3-e0ff-4e3a-9549-7dae4eeba55d", + "type": "decisionTableNode", + "position": { + "x": 460, + "y": 330 + }, + "name": "Merchant Reputation", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "c658ae97-99c8-46fe-bc3a-64c14ea8c475", + "name": "Merchant reputation", + "type": "expression", + "field": "merchant.reputation" + } + ], + "outputs": [ + { + "id": "71bf3b59-455d-4a6e-894c-4e3a7cf7cbde", + "name": "Merchant Reputation Flag", + "type": "expression", + "field": "flags.merchantReputation" + } + ], + "rules": [ + { + "_id": "e8a05a05-d7f7-4272-94a1-c5aa84b0feaf", + "c658ae97-99c8-46fe-bc3a-64c14ea8c475": "(0.8..1.0]", + "71bf3b59-455d-4a6e-894c-4e3a7cf7cbde": "\"green\"" + }, + { + "_id": "cdee900f-a1b1-4a0c-8aca-c305c19a33dd", + "c658ae97-99c8-46fe-bc3a-64c14ea8c475": "(0.5..0.8]", + "71bf3b59-455d-4a6e-894c-4e3a7cf7cbde": "\"amber\"" + }, + { + "_id": "5c58e04a-94b8-40ec-8709-39847e2d4704", + "c658ae97-99c8-46fe-bc3a-64c14ea8c475": "", + "71bf3b59-455d-4a6e-894c-4e3a7cf7cbde": "\"red\"" + } + ] + } + }, + { + "id": "71285ae0-45cf-4300-afe7-de39e0f81590", + "type": "outputNode", + "position": { + "x": 1320, + "y": 360 + }, + "name": "Response" + } + ], + "edges": [ + { + "id": "284924aa-b0e8-4161-bfc0-4cadb7a581a6", + "sourceId": "df092198-208d-4526-82a2-2ff4359d9001", + "type": "edge", + "targetId": "f7e19179-48b7-4d94-b085-ea3d6c9e829e" + }, + { + "id": "bbacf049-12b4-406d-bf36-1b68a9640992", + "sourceId": "f7e19179-48b7-4d94-b085-ea3d6c9e829e", + "type": "edge", + "targetId": "9bc09d87-84ce-4f4c-9c53-317688ee6cc5", + "sourceHandle": "5bffb0f5-a69d-46ba-9122-08bae9f6fe92" + }, + { + "id": "79e399d4-57ba-46ec-8f74-12260081043f", + "sourceId": "f7e19179-48b7-4d94-b085-ea3d6c9e829e", + "type": "edge", + "targetId": "3187c4af-565f-482c-805f-9099e9d56fc1", + "sourceHandle": "13737427-cb8c-4c32-bde5-e49ef6fa03e0" + }, + { + "id": "da7e7f8b-1e05-4189-afa4-42423be11367", + "sourceId": "f7e19179-48b7-4d94-b085-ea3d6c9e829e", + "type": "edge", + "targetId": "fd8f6200-f236-4d00-859e-9a732e553176", + "sourceHandle": "aa630466-3bb1-45e3-9ae8-12eb57451f55" + }, + { + "id": "5624c75f-9f3c-4739-983d-9cf509967201", + "sourceId": "9bc09d87-84ce-4f4c-9c53-317688ee6cc5", + "type": "edge", + "targetId": "204af8f6-325f-41cc-a5bd-ee99905d8c1f" + }, + { + "id": "6d2d4d98-901a-4304-82e1-bc67219308a8", + "sourceId": "3187c4af-565f-482c-805f-9099e9d56fc1", + "type": "edge", + "targetId": "204af8f6-325f-41cc-a5bd-ee99905d8c1f" + }, + { + "id": "f0c921b9-9b8c-43ab-9215-ed9b3af54d88", + "sourceId": "fd8f6200-f236-4d00-859e-9a732e553176", + "type": "edge", + "targetId": "204af8f6-325f-41cc-a5bd-ee99905d8c1f" + }, + { + "id": "1fd5ad15-4bdf-451c-9bdc-f34424382e0b", + "sourceId": "df092198-208d-4526-82a2-2ff4359d9001", + "type": "edge", + "targetId": "57b828f3-e0ff-4e3a-9549-7dae4eeba55d" + }, + { + "id": "fd6caf40-ef8f-4a2e-bf05-a42bb7f33236", + "sourceId": "df092198-208d-4526-82a2-2ff4359d9001", + "type": "edge", + "targetId": "ac9cf968-98b2-4ba5-a481-28da1e32b649" + }, + { + "id": "74c48816-e17d-4d44-83da-e05940e778c7", + "sourceId": "ac9cf968-98b2-4ba5-a481-28da1e32b649", + "type": "edge", + "targetId": "204af8f6-325f-41cc-a5bd-ee99905d8c1f" + }, + { + "id": "ac21ce5c-0a87-4f23-8006-0d75b782f5dd", + "sourceId": "57b828f3-e0ff-4e3a-9549-7dae4eeba55d", + "type": "edge", + "targetId": "204af8f6-325f-41cc-a5bd-ee99905d8c1f" + }, + { + "id": "0af5670d-eb0f-4051-a129-843488525e43", + "sourceId": "204af8f6-325f-41cc-a5bd-ee99905d8c1f", + "type": "edge", + "targetId": "71285ae0-45cf-4300-afe7-de39e0f81590" + } + ] +} \ No newline at end of file diff --git a/test-data/graphs/company-analysis.json b/test-data/graphs/company-analysis.json new file mode 100644 index 00000000..fa21d3af --- /dev/null +++ b/test-data/graphs/company-analysis.json @@ -0,0 +1,390 @@ +{ + "tests": [{ + "input": { + "companyInformation": { + "country": { + "code": "US" + }, + "experian": { + "legalStatus": "LTD" + } + } + }, + "output": { + "flag": { + "companyType": "green", + "country": "green", + "turnover": "red" + }, + "overall": "red" + } + }, { + "input": { + "companyInformation": { + "country": { + "code": "FR" + }, + "financialDetails": { + "turnover": 250000 + }, + "experian": { + "legalStatus": "LTD" + } + } + }, + "output": { + "flag": { + "companyType": "green", + "country": "amber", + "turnover": "amber" + }, + "overall": "amber" + } + }, { + "input": { + "companyInformation": { + "country": { + "code": "FR" + }, + "financialDetails": { + "turnover": 10000001 + }, + "experian": { + "legalStatus": "LTD" + } + } + }, + "output": { + "flag": { + "companyType": "green", + "country": "amber", + "turnover": "green" + }, + "overall": "green" + } + }], + "contentType": "application/vnd.gorules.decision", + "edges": [ + { + "id": "3e749d7f-97d5-45c9-917f-20ae877c3bde", + "type": "edge", + "sourceId": "4e7e6bb9-f128-41e7-8cc5-b9d79670b96a", + "targetId": "abaaa033-5516-440c-b211-35f1d616ad9f" + }, + { + "id": "d00252c1-9a54-4599-940c-c9c1c3bb6800", + "type": "edge", + "sourceId": "4e7e6bb9-f128-41e7-8cc5-b9d79670b96a", + "targetId": "46fbad36-4bbe-44ac-833f-d30e0d37d8d7" + }, + { + "id": "f5be27b5-4eea-40f4-abce-31081a0caf65", + "type": "edge", + "sourceId": "4e7e6bb9-f128-41e7-8cc5-b9d79670b96a", + "targetId": "d6925cde-b3c9-4b7f-8652-7380dacea6a4" + }, + { + "id": "3000e420-4846-4f86-8ed2-f595d204672e", + "type": "edge", + "sourceId": "46fbad36-4bbe-44ac-833f-d30e0d37d8d7", + "targetId": "95aa8f3c-f371-4e48-beb3-0b5775d2a814" + }, + { + "id": "638c35a9-eef7-4d90-bf1e-58ba10734d98", + "type": "edge", + "sourceId": "d6925cde-b3c9-4b7f-8652-7380dacea6a4", + "targetId": "95aa8f3c-f371-4e48-beb3-0b5775d2a814" + }, + { + "id": "30f2247a-140c-40f3-8160-a4e61d56da8e", + "type": "edge", + "sourceId": "af6cdac4-2019-4a0f-9715-2ecfb27e0bfc", + "targetId": "86ce04c9-b4dd-4513-ae2b-7f585ceb224a" + }, + { + "id": "822d2eeb-5a9e-4670-b181-4af0ed6dd9b9", + "type": "edge", + "sourceId": "86ce04c9-b4dd-4513-ae2b-7f585ceb224a", + "targetId": "95aa8f3c-f371-4e48-beb3-0b5775d2a814" + }, + { + "id": "61cc8c10-57c2-411d-a88c-90176ebb8593", + "type": "edge", + "sourceId": "d6925cde-b3c9-4b7f-8652-7380dacea6a4", + "targetId": "af6cdac4-2019-4a0f-9715-2ecfb27e0bfc" + }, + { + "id": "2011fc65-fd8c-421f-8123-d1fb5f1accb3", + "type": "edge", + "sourceId": "46fbad36-4bbe-44ac-833f-d30e0d37d8d7", + "targetId": "af6cdac4-2019-4a0f-9715-2ecfb27e0bfc" + }, + { + "id": "d65fb53d-0fef-47ff-aeac-b8cbac5af7be", + "type": "edge", + "sourceId": "abaaa033-5516-440c-b211-35f1d616ad9f", + "targetId": "af6cdac4-2019-4a0f-9715-2ecfb27e0bfc" + }, + { + "id": "7cfe568d-9d34-45bf-ba9d-5ce0ad47d35e", + "type": "edge", + "sourceId": "abaaa033-5516-440c-b211-35f1d616ad9f", + "targetId": "95aa8f3c-f371-4e48-beb3-0b5775d2a814" + } + ], + "nodes": [ + { + "id": "4e7e6bb9-f128-41e7-8cc5-b9d79670b96a", + "name": "Request", + "type": "inputNode", + "position": { + "x": 70, + "y": 160 + } + }, + { + "id": "d6925cde-b3c9-4b7f-8652-7380dacea6a4", + "name": "Company Type", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "D8T0xuyYLC", + "Yi49Ln4-V_": "\"green\"", + "nd30YgUKve": "\"INC\",\"LTD\",\"LLC\"" + }, + { + "_id": "Ewgtm_21qr", + "Yi49Ln4-V_": "\"amber\"", + "nd30YgUKve": "" + } + ], + "inputs": [ + { + "id": "nd30YgUKve", + "name": "Company Type", + "type": "expression", + "field": "companyInformation.experian.legalStatus" + } + ], + "outputs": [ + { + "id": "Yi49Ln4-V_", + "name": "Flag CompanyType", + "type": "expression", + "field": "flag.companyType" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 380, + "y": 270 + } + }, + { + "id": "46fbad36-4bbe-44ac-833f-d30e0d37d8d7", + "name": "Turnover", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "fJxWqBVUNk", + "2Zxqan3BtC": "", + "6xj5CMIFv9": "> 1_000_000", + "rrW6s3l7vU": "\"green\"" + }, + { + "_id": "lqBoqkvWHA", + "2Zxqan3BtC": "", + "6xj5CMIFv9": "[200_000..1_000_000]", + "rrW6s3l7vU": "\"amber\"" + }, + { + "_id": "YO3K4Q1iuU", + "2Zxqan3BtC": "", + "6xj5CMIFv9": "< 200_000", + "rrW6s3l7vU": "\"red\"" + }, + { + "_id": "SY7uwJEPqS", + "2Zxqan3BtC": "", + "6xj5CMIFv9": "", + "rrW6s3l7vU": "\"red\"" + } + ], + "inputs": [ + { + "id": "6xj5CMIFv9", + "name": "Turnover (LY)", + "type": "expression", + "field": "companyInformation.financialDetails.turnover" + }, + { + "id": "2Zxqan3BtC", + "name": "CompanyInformation Country Code", + "type": "expression", + "field": "companyInformation.country.code" + } + ], + "outputs": [ + { + "id": "rrW6s3l7vU", + "name": "Flag Turnover", + "type": "expression", + "field": "flag.turnover" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 380, + "y": 160 + } + }, + { + "id": "abaaa033-5516-440c-b211-35f1d616ad9f", + "name": "Country", + "type": "decisionTableNode", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "z87lDk-Xar", + "name": "Company Country", + "type": "expression", + "field": "companyInformation.country.code" + } + ], + "outputs": [ + { + "id": "I493u7jDPg", + "name": "Flag Country", + "type": "expression", + "field": "flag.country" + } + ], + "rules": [ + { + "_id": "TOi2qECISd", + "I493u7jDPg": "\"green\"", + "z87lDk-Xar": "\"US\",\"IE\",\"GB\",\"CA\", \"MX\"" + }, + { + "_id": "aGPqCdh2tU", + "I493u7jDPg": "\"amber\"", + "z87lDk-Xar": "\"FR\", \"DE\"" + }, + { + "_id": "VYiuLkrLWb", + "I493u7jDPg": "\"red\"", + "z87lDk-Xar": "" + } + ] + }, + "position": { + "x": 380, + "y": 50 + } + }, + { + "id": "af6cdac4-2019-4a0f-9715-2ecfb27e0bfc", + "name": "Overall Mapper", + "type": "functionNode", + "content": "/**\n* @param {import('gorules').Input} input\n* @param {{\n* moment: import('dayjs')\n* env: Record\n* }} helpers\n*/\nconst handler = (input, { moment, env }) => {\n const count = (flag) => Object.values(input?.flag || {}).reduce((acc, curr) => {\n if (curr === flag) return acc + 1;\n return acc;\n }, 0);\n\n return {\n critical: count('critical'),\n red: count('red'),\n amber: count('amber'),\n green: count('green')\n };\n}", + "position": { + "x": 630, + "y": 270 + } + }, + { + "id": "86ce04c9-b4dd-4513-ae2b-7f585ceb224a", + "name": "Overall", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "P0RQ3NFWfc", + "AczIUwvClr": "", + "KsBwLhAedP": "> 0", + "QJttqyV2FB": "", + "ek5q9WgLL9": "\"red\"", + "iFQl1CKB5S": "" + }, + { + "_id": "UGrT2iHE61", + "AczIUwvClr": "> 0", + "KsBwLhAedP": "", + "QJttqyV2FB": "", + "ek5q9WgLL9": "\"red\"", + "iFQl1CKB5S": "" + }, + { + "_id": "SBBO_aWujh", + "AczIUwvClr": "", + "KsBwLhAedP": "", + "QJttqyV2FB": "> 1", + "ek5q9WgLL9": "\"amber\"", + "iFQl1CKB5S": "" + }, + { + "_id": "N2jO9BnXHY", + "AczIUwvClr": "", + "KsBwLhAedP": "", + "QJttqyV2FB": "", + "ek5q9WgLL9": "\"green\"", + "iFQl1CKB5S": "" + } + ], + "inputs": [ + { + "id": "AczIUwvClr", + "name": "Red", + "type": "expression", + "field": "red" + }, + { + "id": "QJttqyV2FB", + "name": "Amber", + "type": "expression", + "field": "amber" + }, + { + "id": "iFQl1CKB5S", + "name": "Green", + "type": "expression", + "field": "green" + }, + { + "id": "KsBwLhAedP", + "name": "Critical", + "type": "expression", + "field": "critical" + } + ], + "outputs": [ + { + "id": "ek5q9WgLL9", + "name": "Overall", + "type": "expression", + "field": "overall" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 880, + "y": 270 + } + }, + { + "id": "95aa8f3c-f371-4e48-beb3-0b5775d2a814", + "name": "Response", + "type": "outputNode", + "position": { + "x": 1120, + "y": 160 + } + } + ] +} \ No newline at end of file diff --git a/test-data/graphs/decision-table-discounts.json b/test-data/graphs/decision-table-discounts.json new file mode 100644 index 00000000..09b3580c --- /dev/null +++ b/test-data/graphs/decision-table-discounts.json @@ -0,0 +1,168 @@ +{ + "tests": [ + { + "input": { + "code": "WELCOME_2023", + "order": {"createdAt": "2023-01-15"}, + "customer": { + "joinDate": "2023-02-01", + "pastPurchases": [] + } + }, + "output": [ + { + "discount": {"percentage": 15} + } + ] + }, + { + "input": { + "code": "BACK_TO_BACK", + "order": {"createdAt": "2023-01-20"}, + "customer": { + "joinDate": "2023-01-10", + "pastPurchases": [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}] + } + }, + "output": [ + { + "discount": {"flat": 25} + } + ] + }, + { + "input": { + "code": "EARLY_BIRD", + "order": {"createdAt": "2022-01-15"}, + "customer": { + "joinDate": "2019-12-01", + "pastPurchases": [] + } + }, + "output": [ + { + "discount": {"percentage": 10} + } + ] + } + ], + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "5eefa8a4-56a9-46b1-a265-7ddbddde3441", + "type": "inputNode", + "position": { + "x": 240, + "y": 280 + }, + "name": "Request" + }, + { + "id": "638d767d-ae08-4ba3-9db3-97c6c4e4c6f5", + "type": "decisionTableNode", + "position": { + "x": 520, + "y": 280 + }, + "name": "Discounts", + "content": { + "hitPolicy": "collect", + "inputs": [ + { + "id": "6fd35e3e-584b-4149-b4bf-5f2d258f40e7", + "type": "expression", + "field": "code", + "name": "Code" + }, + { + "id": "a1700de6-fe35-4cec-b66f-0897d2604a3f", + "name": "Order Date", + "type": "expression", + "field": "date(order.createdAt)" + }, + { + "id": "813dace4-0b55-4409-adb2-b4739a653302", + "type": "expression", + "field": "date(customer.joinDate)", + "name": "Customer Join Date" + }, + { + "id": "f34f2c8b-eba9-4ebe-aeec-384bce69e3aa", + "type": "expression", + "field": "len(customer.pastPurchases)", + "name": "Customer Past Purchases" + } + ], + "outputs": [ + { + "field": "discount.percentage", + "id": "16d637be-1093-4468-b29f-236666bf2f26", + "name": "Discount Percentage", + "type": "expression" + }, + { + "id": "bccbb1bc-f0da-4b1e-b186-122ba4a193ac", + "type": "expression", + "field": "discount.flat", + "name": "Discount Flat" + } + ], + "rules": [ + { + "_id": "eabd4e2e-5c14-4d4d-ada6-b3cdc547ae99", + "_description": "Welcome bonus of 15%", + "6fd35e3e-584b-4149-b4bf-5f2d258f40e7": "\"WELCOME_2023\"", + "a1700de6-fe35-4cec-b66f-0897d2604a3f": "", + "813dace4-0b55-4409-adb2-b4739a653302": "> date(\"2023-01-01\")", + "f34f2c8b-eba9-4ebe-aeec-384bce69e3aa": "0", + "16d637be-1093-4468-b29f-236666bf2f26": "15", + "bccbb1bc-f0da-4b1e-b186-122ba4a193ac": "" + }, + { + "_id": "d80dee00-07a7-42fb-bc83-262eeee9fbdb", + "_description": "Recurring customer 25$ discount", + "6fd35e3e-584b-4149-b4bf-5f2d258f40e7": "\"BACK_TO_BACK\"", + "a1700de6-fe35-4cec-b66f-0897d2604a3f": "[date(\"2023-01-01\")..date(\"2023-02-01\")]", + "813dace4-0b55-4409-adb2-b4739a653302": "", + "f34f2c8b-eba9-4ebe-aeec-384bce69e3aa": "> 10", + "16d637be-1093-4468-b29f-236666bf2f26": "", + "bccbb1bc-f0da-4b1e-b186-122ba4a193ac": "25" + }, + { + "_id": "fcfae4b3-5c86-4efe-91cd-ca4bc1fa7c7c", + "_description": "Old customer 10%", + "6fd35e3e-584b-4149-b4bf-5f2d258f40e7": "\"EARLY_BIRD\"", + "a1700de6-fe35-4cec-b66f-0897d2604a3f": "", + "813dace4-0b55-4409-adb2-b4739a653302": "< date(\"2020-01-01\")", + "f34f2c8b-eba9-4ebe-aeec-384bce69e3aa": "", + "16d637be-1093-4468-b29f-236666bf2f26": "10", + "bccbb1bc-f0da-4b1e-b186-122ba4a193ac": "" + } + ] + } + }, + { + "id": "c87d26e6-f3a2-47ad-9310-cbd1899cc68a", + "type": "outputNode", + "position": { + "x": 830, + "y": 280 + }, + "name": "Response" + } + ], + "edges": [ + { + "id": "3b07d6c5-6949-4e5d-a8c6-f5cf44dbba0a", + "sourceId": "5eefa8a4-56a9-46b1-a265-7ddbddde3441", + "type": "edge", + "targetId": "638d767d-ae08-4ba3-9db3-97c6c4e4c6f5" + }, + { + "id": "69e558e4-4c20-4a53-9230-c068ebb19830", + "sourceId": "638d767d-ae08-4ba3-9db3-97c6c4e4c6f5", + "type": "edge", + "targetId": "c87d26e6-f3a2-47ad-9310-cbd1899cc68a" + } + ] +} \ No newline at end of file diff --git a/test-data/graphs/decision-table-shipping.json b/test-data/graphs/decision-table-shipping.json new file mode 100644 index 00000000..44ce3b47 --- /dev/null +++ b/test-data/graphs/decision-table-shipping.json @@ -0,0 +1,398 @@ +{ + "tests": [ + { + "input": { + "cart": { + "items": [ + {"unitPrice": 500, "quantity": 2}, + {"unitPrice": 300, "quantity": 3} + ] + }, + "customer": { + "tags": ["premium"], + "country": "US" + } + }, + "output": { + "price": 15, + "group": "PREMIUM_HIGH" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 300, "quantity": 1}, + {"unitPrice": 100, "quantity": 2} + ] + }, + "customer": { + "tags": ["premium"], + "country": "US" + } + }, + "output": { + "price": 20, + "group": "PREMIUM_MEDIUM" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 300, "quantity": 1}, + {"unitPrice": 100, "quantity": 1} + ] + }, + "customer": { + "tags": ["premium"], + "country": "US" + } + }, + "output": { + "price": 25, + "group": "PREMIUM_LOW" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 1500, "quantity": 2}, + {"unitPrice": 800, "quantity": 3} + ] + }, + "customer": { + "tags": [], + "country": "US" + } + }, + "output": { + "price": 25, + "group": "US_HIGH" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 300, "quantity": 2}, + {"unitPrice": 50, "quantity": 3} + ] + }, + "customer": { + "tags": [], + "country": "US" + } + }, + "output": { + "price": 35, + "group": "US_MEDIUM" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 200, "quantity": 1}, + {"unitPrice": 125, "quantity": 2} + ] + }, + "customer": { + "tags": [], + "country": "US" + } + }, + "output": { + "price": 40, + "group": "US_LOW" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 700, "quantity": 1}, + {"unitPrice": 900, "quantity": 2} + ] + }, + "customer": { + "tags": [], + "country": "IE" + } + }, + "output": { + "price": 20, + "group": "UK_HIGH" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 400, "quantity": 1}, + {"unitPrice": 200, "quantity": 2} + ] + }, + "customer": { + "tags": [], + "country": "GB" + } + }, + "output": { + "price": 30, + "group": "UK_MEDIUM" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 200, "quantity": 1}, + {"unitPrice": 100, "quantity": 1} + ] + }, + "customer": { + "tags": [], + "country": "IE" + } + }, + "output": { + "price": 35, + "group": "UK_LOW" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 1500, "quantity": 1}, + {"unitPrice": 1200, "quantity": 2} + ] + }, + "customer": { + "tags": [], + "country": "ROW" + } + }, + "output": { + "price": 35, + "group": "ROW_HIGH" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 400, "quantity": 2}, + {"unitPrice": 199.99, "quantity": 1} + ] + }, + "customer": { + "tags": [], + "country": "ROW" + } + }, + "output": { + "price": 45, + "group": "ROW_MEDIUM" + } + }, + { + "input": { + "cart": { + "items": [ + {"unitPrice": 499.99, "quantity": 1} + ] + }, + "customer": { + "tags": [], + "country": "ROW" + } + }, + "output": { + "price": 50, + "group": "ROW_LOW" + } + } + ], + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "4c688656-05ba-4503-a582-02e6e4a5a135", + "type": "inputNode", + "position": { + "x": 220, + "y": 260 + }, + "name": "Request" + }, + { + "id": "2938f93f-1dce-4f85-a401-44c224d9237f", + "type": "outputNode", + "position": { + "x": 730, + "y": 260 + }, + "name": "Response" + }, + { + "id": "405841da-b7bb-4f4e-824f-21f1f4bc6fd9", + "type": "decisionTableNode", + "position": { + "x": 460, + "y": 260 + }, + "name": "Shipping Fees", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "ea23eec9-76c5-4b62-821c-aec504545506", + "name": "Totals", + "type": "expression", + "field": "sum(map(cart.items, #.unitPrice * #.quantity))" + }, + { + "id": "184cc5f5-f459-41cf-814b-79f6ffea695d", + "type": "expression", + "field": "customer.tags", + "name": "Customer Tags" + }, + { + "id": "d89ca9da-581e-4392-a990-24cde9c33a2b", + "type": "expression", + "field": "customer.country", + "name": "Customer Country" + } + ], + "outputs": [ + { + "field": "price", + "id": "f01ad940-81f0-4164-9868-5423fb66cca9", + "name": "Price", + "type": "expression" + }, + { + "id": "adf3d882-64d8-4cf9-a208-023f586f8697", + "type": "expression", + "field": "group", + "name": "Group" + } + ], + "rules": [ + { + "_id": "ab7bcf2b-4594-4347-a085-5dac0601d7e5", + "ea23eec9-76c5-4b62-821c-aec504545506": ">= 1_000", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "contains($, \"premium\")", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "", + "f01ad940-81f0-4164-9868-5423fb66cca9": "15", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"PREMIUM_HIGH\"" + }, + { + "_id": "4a2ae2e9-9ad7-45b2-9fad-db26e3cab0c8", + "ea23eec9-76c5-4b62-821c-aec504545506": "[500..1_000)", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "contains($, \"premium\")", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "", + "f01ad940-81f0-4164-9868-5423fb66cca9": "20", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"PREMIUM_MEDIUM\"" + }, + { + "_id": "b7aaeca2-7e42-4fb2-9867-eb4e43ca911d", + "ea23eec9-76c5-4b62-821c-aec504545506": "", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "contains($, \"premium\")", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "", + "f01ad940-81f0-4164-9868-5423fb66cca9": "25", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"PREMIUM_LOW\"" + }, + { + "_id": "a179f314-512c-4cc6-a892-5f1e0b6b979c", + "ea23eec9-76c5-4b62-821c-aec504545506": ">= 1_000", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "\"US\"", + "f01ad940-81f0-4164-9868-5423fb66cca9": "25", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"US_HIGH\"" + }, + { + "_id": "db1f0c0e-e48a-4f9f-95fe-04e143cfd029", + "ea23eec9-76c5-4b62-821c-aec504545506": "[500..1_000)", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "\"US\"", + "f01ad940-81f0-4164-9868-5423fb66cca9": "35", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"US_MEDIUM\"" + }, + { + "_id": "3110b1b5-6552-48c4-8a78-417ee6185125", + "ea23eec9-76c5-4b62-821c-aec504545506": "", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "\"US\"", + "f01ad940-81f0-4164-9868-5423fb66cca9": "40", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"US_LOW\"" + }, + { + "_id": "6ad80268-5400-45f4-977f-07515cc0d4a0", + "ea23eec9-76c5-4b62-821c-aec504545506": ">= 1_000", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "\"GB\", \"IE\"", + "f01ad940-81f0-4164-9868-5423fb66cca9": "20", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"UK_HIGH\"" + }, + { + "_id": "b814770e-c35b-406b-ba0a-39e2832853bc", + "ea23eec9-76c5-4b62-821c-aec504545506": "[500..1_000)", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "\"GB\", \"IE\"", + "f01ad940-81f0-4164-9868-5423fb66cca9": "30", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"UK_MEDIUM\"" + }, + { + "_id": "705783d4-e015-47d6-98fc-84f6544b3ce1", + "ea23eec9-76c5-4b62-821c-aec504545506": "", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "\"GB\", \"IE\"", + "f01ad940-81f0-4164-9868-5423fb66cca9": "35", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"UK_LOW\"" + }, + { + "_id": "889c2ab2-d495-4299-9809-3aac70352c9b", + "ea23eec9-76c5-4b62-821c-aec504545506": ">= 1_000", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "", + "f01ad940-81f0-4164-9868-5423fb66cca9": "35", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"ROW_HIGH\"" + }, + { + "_id": "3c32d0b0-32a1-465c-9f50-643f8f2a0700", + "ea23eec9-76c5-4b62-821c-aec504545506": "[500..1_000)", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "", + "f01ad940-81f0-4164-9868-5423fb66cca9": "45", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"ROW_MEDIUM\"" + }, + { + "_id": "5449fa1d-78f1-463f-a4e7-6ce8018adf1f", + "ea23eec9-76c5-4b62-821c-aec504545506": "", + "184cc5f5-f459-41cf-814b-79f6ffea695d": "", + "d89ca9da-581e-4392-a990-24cde9c33a2b": "", + "f01ad940-81f0-4164-9868-5423fb66cca9": "50", + "adf3d882-64d8-4cf9-a208-023f586f8697": "\"ROW_LOW\"" + } + ] + } + } + ], + "edges": [ + { + "id": "1ded87cb-d27c-40ba-a31a-8a7127303f6c", + "sourceId": "4c688656-05ba-4503-a582-02e6e4a5a135", + "type": "edge", + "targetId": "405841da-b7bb-4f4e-824f-21f1f4bc6fd9" + }, + { + "id": "2a3ed2d7-ab77-4935-968e-11642ebd4f75", + "sourceId": "405841da-b7bb-4f4e-824f-21f1f4bc6fd9", + "type": "edge", + "targetId": "2938f93f-1dce-4f85-a401-44c224d9237f" + } + ] +} \ No newline at end of file diff --git a/test-data/graphs/insurance-breakdown.json b/test-data/graphs/insurance-breakdown.json new file mode 100644 index 00000000..a59c7329 --- /dev/null +++ b/test-data/graphs/insurance-breakdown.json @@ -0,0 +1,421 @@ +{ + "tests": [ + { + "input": { + "coverage": { + "pi": "1M", + "pl": "10M" + }, + "country": "CA" + }, + "output": { + "breakdown": [ + { + "key": "premium", + "title": "Premium", + "value": 83 + }, + { + "key": "gst", + "title": "GST", + "value": 10.3 + }, + { + "key": "brokerFee", + "title": "Broker Fee", + "value": 20 + } + ] + } + }, + { + "input": { + "coverage": { + "pi": "1M", + "pl": "10M" + } + }, + "output": { + "breakdown": [ + { + "key": "premium", + "title": "Premium", + "value": 83 + }, + { + "key": "gst", + "title": "GST", + "value": 0 + }, + { + "key": "brokerFee", + "title": "Broker Fee", + "value": 20 + } + ] + } + }, + { + "input": { + "coverage": { + "pi": "2M", + "pl": "20M" + } + }, + "output": { + "breakdown": [ + { + "key": "premium", + "title": "Premium", + "value": 125 + }, + { + "key": "gst", + "title": "GST", + "value": 0 + }, + { + "key": "brokerFee", + "title": "Broker Fee", + "value": 45 + } + ] + } + }, + { + "input": { + "coverage": { + "pi": "2M", + "pl": "10M" + }, + "country": "CA" + }, + "output": { + "breakdown": [ + { + "key": "premium", + "title": "Premium", + "value": 95 + }, + { + "key": "gst", + "title": "GST", + "value": 12 + }, + { + "key": "brokerFee", + "title": "Broker Fee", + "value": 25 + } + ] + } + }, + { + "input": { + "coverage": { + "pi": "2M", + "pl": "20M" + }, + "country": "CA" + }, + "output": { + "breakdown": [ + { + "key": "premium", + "title": "Premium", + "value": 125 + }, + { + "key": "gst", + "title": "GST", + "value": 17 + }, + { + "key": "brokerFee", + "title": "Broker Fee", + "value": 45 + } + ] + } + } + ], + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "4b197c9b-8db6-4d67-8e64-2ea7d262b1c6", + "name": "Request", + "type": "inputNode", + "position": { + "x": 80, + "y": 330 + } + }, + { + "id": "52d501fa-5089-471a-81e1-9d604b6b864d", + "name": "Response", + "type": "outputNode", + "position": { + "x": 870, + "y": 330 + } + }, + { + "id": "ebec39cc-ac87-4145-ae5d-744759a37268", + "name": "PI Base", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "900c2af2-8b75-414f-b365-97fbf2ddf28f", + "b45e7cc2-5fa7-4ace-814e-3eac8ff8cfc9": "63", + "ec711970-6b90-492f-8b5b-4a6e7cbf3088": "\"1M\"" + }, + { + "_id": "9b10a56d-ee99-40f3-aa30-85ed38f60ec7", + "b45e7cc2-5fa7-4ace-814e-3eac8ff8cfc9": "75", + "ec711970-6b90-492f-8b5b-4a6e7cbf3088": "\"2M\"" + }, + { + "_id": "91ffd81f-0fa4-4f99-b4fe-09c84ce0780c", + "b45e7cc2-5fa7-4ace-814e-3eac8ff8cfc9": "0", + "ec711970-6b90-492f-8b5b-4a6e7cbf3088": "" + } + ], + "inputs": [ + { + "id": "ec711970-6b90-492f-8b5b-4a6e7cbf3088", + "name": "Coverage PI", + "type": "expression", + "field": "coverage.pi" + } + ], + "outputs": [ + { + "id": "b45e7cc2-5fa7-4ace-814e-3eac8ff8cfc9", + "name": "Base PI", + "type": "expression", + "field": "base.pi" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 370, + "y": 210 + } + }, + { + "id": "831e22b5-93c4-4963-9d21-8af157dd1606", + "name": "PL Base", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "e4ffd222-ef9c-4ab7-a5ef-6f6ff0c6f1c2", + "6d179b45-a0b7-4c85-8324-73058f3def42": "\"10M\"", + "bfc690c9-d11e-42ee-b141-0112c3b02ee0": "20" + }, + { + "_id": "5a8986b1-27a3-4961-ab6d-e32fb773e083", + "6d179b45-a0b7-4c85-8324-73058f3def42": "\"20M\"", + "bfc690c9-d11e-42ee-b141-0112c3b02ee0": "50" + }, + { + "_id": "d6560ee2-26c8-4863-bc8c-a4717cde4a1d", + "6d179b45-a0b7-4c85-8324-73058f3def42": "", + "bfc690c9-d11e-42ee-b141-0112c3b02ee0": "0" + } + ], + "inputs": [ + { + "id": "6d179b45-a0b7-4c85-8324-73058f3def42", + "name": "Coverage PL", + "type": "expression", + "field": "coverage.pl" + } + ], + "outputs": [ + { + "id": "bfc690c9-d11e-42ee-b141-0112c3b02ee0", + "name": "Base PL", + "type": "expression", + "field": "base.pl" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 370, + "y": 330 + } + }, + { + "id": "b9d38a6e-2d57-483c-9765-66d97b940a04", + "name": "GST", + "type": "decisionTableNode", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "5ca5e8d1-8339-483b-9c81-cdd34a4fb953", + "name": "Country", + "type": "expression", + "field": "country" + }, + { + "id": "8342de09-7a13-4d3b-82f9-156e2381817d", + "name": "State", + "type": "expression", + "field": "state" + } + ], + "outputs": [ + { + "id": "49135c04-c19b-40c3-8a7c-0de08ffaa713", + "name": "GST (%)", + "type": "expression", + "field": "tax.gst" + } + ], + "rules": [ + { + "_id": "35babb81-b350-4445-853f-3877892910f9", + "49135c04-c19b-40c3-8a7c-0de08ffaa713": "10", + "5ca5e8d1-8339-483b-9c81-cdd34a4fb953": "\"CA\"", + "8342de09-7a13-4d3b-82f9-156e2381817d": "" + }, + { + "_id": "6670eccf-b410-4e6d-9979-da9f6bddc75d", + "49135c04-c19b-40c3-8a7c-0de08ffaa713": "0", + "5ca5e8d1-8339-483b-9c81-cdd34a4fb953": "", + "8342de09-7a13-4d3b-82f9-156e2381817d": "" + } + ] + }, + "position": { + "x": 370, + "y": 450 + } + }, + { + "id": "453ed4ec-8754-4768-823a-0741e12926ee", + "name": "Aggregator", + "type": "functionNode", + "content": "/**\n * @param input\n * @param {{\n * dayjs: import('dayjs')\n * Big: import('big.js').BigConstructor\n * }} helpers\n */\nconst handler = (input, { dayjs, Big }) => {\n const round = (val) => {\n if (val === undefined) return 0;\n return val?.toFixed !== undefined ? val.round(2).toNumber() : val\n }\n\n const getPercentage = (num, perc) => {\n return Big(num).times(Big(perc).div(100))\n }\n\n const basePi = Big(input.base.pi);\n const piGst = getPercentage(basePi, input.tax.gst);\n\n const basePl = Big(input.base.pl);\n const plGst = getPercentage(basePl, input.tax.gst);\n\n const brokerFee = Big(input.fee.broker);\n const brokerFeeGst = getPercentage(brokerFee, input.tax.gst);\n\n const base = basePi.add(basePl);\n const gst = piGst.add(plGst).add(brokerFeeGst);\n\n return {\n breakdown: [{\n title: 'Premium',\n key: 'premium',\n value: round(base)\n }, {\n title: 'GST',\n key: 'gst',\n value: round(gst)\n }, {\n title: 'Broker Fee',\n key: 'brokerFee',\n value: round(brokerFee)\n }]\n }\n}", + "position": { + "x": 630, + "y": 330 + } + }, + { + "id": "0b998a2f-2b8d-45b4-8cea-13c731d0cd0d", + "name": "Fees", + "type": "decisionTableNode", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "d49e2534-a500-4596-a575-220fc7da2bfc", + "name": "Contracts", + "type": "expression" + } + ], + "outputs": [ + { + "id": "503a8564-eab4-473a-ba23-d1dc351368ac", + "name": "Broker Fee", + "type": "expression", + "field": "fee.broker" + } + ], + "rules": [ + { + "_id": "629a0bff-c452-474c-b5a0-0b5c0b7d0880", + "d49e2534-a500-4596-a575-220fc7da2bfc": "coverage.pi == '1M' and coverage.pl == '10M'", + "503a8564-eab4-473a-ba23-d1dc351368ac": "20" + }, + { + "_id": "03ac3c59-9d38-431a-965f-d8da913b0100", + "d49e2534-a500-4596-a575-220fc7da2bfc": "coverage.pi == '2M' and coverage.pl == '10M'", + "503a8564-eab4-473a-ba23-d1dc351368ac": "25" + }, + { + "_id": "e9f771aa-a2c1-4e6d-b541-fbef6a20e637", + "d49e2534-a500-4596-a575-220fc7da2bfc": "coverage.pi == '1M' and coverage.pl == '20M'", + "503a8564-eab4-473a-ba23-d1dc351368ac": "40" + }, + { + "_id": "4b4e7a3c-17f9-451b-9099-d938de7bfbbb", + "d49e2534-a500-4596-a575-220fc7da2bfc": "coverage.pi == '2M' and coverage.pl == '20M'", + "503a8564-eab4-473a-ba23-d1dc351368ac": "45" + }, + { + "_id": "ca9d6c4c-c013-426a-a23a-930da2e5941b", + "d49e2534-a500-4596-a575-220fc7da2bfc": "", + "503a8564-eab4-473a-ba23-d1dc351368ac": "0" + } + ] + }, + "position": { + "x": 370, + "y": 580 + } + } + ], + "edges": [ + { + "id": "e5c60566-130b-48e3-a572-406ace13e24d", + "type": "edge", + "sourceId": "4b197c9b-8db6-4d67-8e64-2ea7d262b1c6", + "targetId": "831e22b5-93c4-4963-9d21-8af157dd1606" + }, + { + "id": "5554b69a-de2a-4986-9067-b5a3fb5ab27c", + "type": "edge", + "sourceId": "4b197c9b-8db6-4d67-8e64-2ea7d262b1c6", + "targetId": "ebec39cc-ac87-4145-ae5d-744759a37268" + }, + { + "id": "8b81e148-8ba3-408f-864d-639dba3e8231", + "type": "edge", + "sourceId": "4b197c9b-8db6-4d67-8e64-2ea7d262b1c6", + "targetId": "b9d38a6e-2d57-483c-9765-66d97b940a04" + }, + { + "id": "03f1c9ac-b748-48c8-b6ca-7bbde5638dd4", + "type": "edge", + "sourceId": "831e22b5-93c4-4963-9d21-8af157dd1606", + "targetId": "453ed4ec-8754-4768-823a-0741e12926ee" + }, + { + "id": "abf74448-ec29-4c85-bb34-c591059054f2", + "type": "edge", + "sourceId": "b9d38a6e-2d57-483c-9765-66d97b940a04", + "targetId": "453ed4ec-8754-4768-823a-0741e12926ee" + }, + { + "id": "3772f66b-1f80-45cd-baac-a7962f955aea", + "type": "edge", + "sourceId": "ebec39cc-ac87-4145-ae5d-744759a37268", + "targetId": "453ed4ec-8754-4768-823a-0741e12926ee" + }, + { + "id": "de650378-2163-4984-9f3a-5eaa94f401cc", + "type": "edge", + "sourceId": "453ed4ec-8754-4768-823a-0741e12926ee", + "targetId": "52d501fa-5089-471a-81e1-9d604b6b864d" + }, + { + "id": "8a72d8a5-3962-44f7-9bdb-611efaabd822", + "type": "edge", + "sourceId": "4b197c9b-8db6-4d67-8e64-2ea7d262b1c6", + "targetId": "0b998a2f-2b8d-45b4-8cea-13c731d0cd0d" + }, + { + "id": "24ce6bc8-36c2-49f7-bb2b-6f1d38455ee0", + "type": "edge", + "sourceId": "0b998a2f-2b8d-45b4-8cea-13c731d0cd0d", + "targetId": "453ed4ec-8754-4768-823a-0741e12926ee" + } + ] +} \ No newline at end of file diff --git a/test-data/graphs/merch-bags.json b/test-data/graphs/merch-bags.json new file mode 100644 index 00000000..8ca012bc --- /dev/null +++ b/test-data/graphs/merch-bags.json @@ -0,0 +1,171 @@ +{ + "tests": [ + { + "input": { + "origin": "JFK", + "class": "economy" + }, + "output": { + "code": "BAG10", + "price": 12 + } + }, + { + "input": { + "origin": "JFK", + "destination": "LAX", + "class": "economy" + }, + "output": { + "code": "BAG10", + "price": 10 + } + }, + { + "input": { + "origin": "JFK", + "destination": "LAX", + "class": "business" + }, + "output": { + "code": "BAG23", + "included": true, + "price": 0 + } + } + ], + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "36aa02a4-df14-4c54-a852-175e749d5860", + "name": "Request", + "type": "inputNode", + "position": { + "x": 30, + "y": 140 + } + }, + { + "id": "5d03a351-786f-4f7d-9b2b-709dc0d81460", + "name": "Bags", + "type": "decisionTableNode", + "content": { + "hitPolicy": "first", + "inputs": [ + { + "id": "a9711b12-1d29-40a3-a48f-2c03df5c8aff", + "name": "Origin", + "type": "expression", + "field": "origin" + }, + { + "id": "UmZtXogtD7", + "name": "Destination", + "type": "expression", + "field": "destination" + }, + { + "id": "b2e2476f-5340-4d66-aec0-9a282be0e716", + "name": "Class", + "type": "expression", + "field": "class" + } + ], + "outputs": [ + { + "id": "DxiL62dzK7", + "name": "Code", + "type": "expression", + "field": "code" + }, + { + "id": "b3bbcf27-5b30-4c05-aedf-2828b6274cdb", + "name": "Price", + "type": "expression", + "field": "price" + }, + { + "id": "3d548469-f407-449a-9ba4-be6e1631612a", + "name": "Included", + "type": "expression", + "field": "included" + } + ], + "rules": [ + { + "_id": "a882b24f-b796-435e-a116-d7e6e5214ff6", + "DxiL62dzK7": "\"BAG10\"", + "UmZtXogtD7": "\"LAX\"", + "3d548469-f407-449a-9ba4-be6e1631612a": "", + "a9711b12-1d29-40a3-a48f-2c03df5c8aff": "\"JFK\"", + "b2e2476f-5340-4d66-aec0-9a282be0e716": "\"economy\"", + "b3bbcf27-5b30-4c05-aedf-2828b6274cdb": "10" + }, + { + "_id": "a6083cab-8dfe-46ae-acf3-823e02ca3f3b", + "DxiL62dzK7": "\"BAG10\"", + "UmZtXogtD7": "", + "3d548469-f407-449a-9ba4-be6e1631612a": "", + "a9711b12-1d29-40a3-a48f-2c03df5c8aff": "\"JFK\"", + "b2e2476f-5340-4d66-aec0-9a282be0e716": "\"economy\"", + "b3bbcf27-5b30-4c05-aedf-2828b6274cdb": "12" + }, + { + "_id": "c1c432ee-0715-4a6e-af7e-600f7456427a", + "DxiL62dzK7": "\"BAG23\"", + "UmZtXogtD7": "", + "3d548469-f407-449a-9ba4-be6e1631612a": "", + "a9711b12-1d29-40a3-a48f-2c03df5c8aff": "\"DEN\"", + "b2e2476f-5340-4d66-aec0-9a282be0e716": "\"economy\"", + "b3bbcf27-5b30-4c05-aedf-2828b6274cdb": "22" + }, + { + "_id": "673f808b-02c2-4055-94c3-e4503578c285", + "DxiL62dzK7": "\"BAG23\"", + "UmZtXogtD7": "", + "3d548469-f407-449a-9ba4-be6e1631612a": "", + "a9711b12-1d29-40a3-a48f-2c03df5c8aff": "\"DEN\"", + "b2e2476f-5340-4d66-aec0-9a282be0e716": "\"economy\"", + "b3bbcf27-5b30-4c05-aedf-2828b6274cdb": "25" + }, + { + "_id": "731388b9-9e94-499a-b201-3f107e7e8a54", + "DxiL62dzK7": "\"BAG23\"", + "UmZtXogtD7": "", + "3d548469-f407-449a-9ba4-be6e1631612a": "true", + "a9711b12-1d29-40a3-a48f-2c03df5c8aff": "", + "b2e2476f-5340-4d66-aec0-9a282be0e716": "\"business\", \"first\"", + "b3bbcf27-5b30-4c05-aedf-2828b6274cdb": "0" + } + ] + }, + "position": { + "x": 290, + "y": 140 + } + }, + { + "id": "f42391bc-6183-4b12-8eaa-6b56510c17ef", + "name": "Response", + "type": "outputNode", + "position": { + "x": 580, + "y": 140 + } + } + ], + "edges": [ + { + "id": "56fd0fb1-0964-4c11-881d-f42114b43ad8", + "type": "edge", + "sourceId": "36aa02a4-df14-4c54-a852-175e749d5860", + "targetId": "5d03a351-786f-4f7d-9b2b-709dc0d81460" + }, + { + "id": "d148ef92-e6e2-486f-9867-7faf8459b111", + "type": "edge", + "sourceId": "5d03a351-786f-4f7d-9b2b-709dc0d81460", + "targetId": "f42391bc-6183-4b12-8eaa-6b56510c17ef" + } + ] +} \ No newline at end of file diff --git a/test-data/graphs/multi-switch.json b/test-data/graphs/multi-switch.json new file mode 100644 index 00000000..06d6eaeb --- /dev/null +++ b/test-data/graphs/multi-switch.json @@ -0,0 +1,498 @@ +{ + "tests": [{ + "input": { + "company": { + "type": "LLC" + } + }, + "output": { + "flag": { + "turnover": "red" + } + } + }, { + "input": { + "company": { + "type": "LLC", + "secretaries": ["first", "second"] + } + }, + "output": { + "flag": { + "secretaries": "amber", + "turnover": "red" + } + } + }, { + "input": { + "company": { + "type": "LLC", + "secretaries": ["first", "second"], + "directors": ["first", "second"] + } + }, + "output": { + "flag": { + "directors": "red", + "secretaries": "amber", + "turnover": "red" + } + } + }, { + "input": { + "company": { + "type": "Corporation", + "secretaries": ["first", "second"], + "directors": ["first", "second"], + "turnover": 10000000 + } + }, + "output": { + "flag": { + "secretaries": "red", + "turnover": "amber" + } + } + }, { + "input": { + "company": { + "type": "Corporation", + "secretaries": ["first", "second"], + "directors": ["first", "second"] + } + }, + "output": { + "flag": { + "secretaries": "red", + "turnover": "red" + } + } + }], + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "2ee16c8c-fb12-4f20-9813-67bad6f4eb14", + "name": "Model Turnover LLC", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "39e37b47-4f40-43f0-becd-d029d7ab8eb1", + "e083f703-2a6a-495d-88d5-7e5ad71c3a58": "\"green\"", + "fa0fd31a-8865-43fb-8a60-b729c640140a": "> 1000000" + }, + { + "_id": "bf6212bb-f3fb-418a-87b2-a4c4c448c6e1", + "e083f703-2a6a-495d-88d5-7e5ad71c3a58": "\"amber\"", + "fa0fd31a-8865-43fb-8a60-b729c640140a": "[200000..1000000]" + }, + { + "_id": "952300fd-e4d9-4301-8a5a-4eda1d01d8ee", + "e083f703-2a6a-495d-88d5-7e5ad71c3a58": "\"red\"", + "fa0fd31a-8865-43fb-8a60-b729c640140a": "" + } + ], + "inputs": [ + { + "id": "fa0fd31a-8865-43fb-8a60-b729c640140a", + "name": "Input", + "type": "expression", + "field": "company.turnover" + } + ], + "outputs": [ + { + "id": "e083f703-2a6a-495d-88d5-7e5ad71c3a58", + "name": "Output", + "type": "expression", + "field": "flag.turnover" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 1230, + "y": 140 + } + }, + { + "id": "2b06eb6e-f3ed-4a9e-9f36-a0de372d97fe", + "name": "switchNode 1", + "type": "switchNode", + "content": { + "hitPolicy": "collect", + "statements": [ + { + "id": "2ce01e42-e125-4184-9a8d-2a7ccf8daf14", + "condition": "company.secretaries != null" + }, + { + "id": "3fba0ac2-8498-4421-bb1d-c0c8cabd9c39", + "condition": "" + } + ] + }, + "position": { + "x": 830, + "y": 260 + } + }, + { + "id": "49f76396-88ae-4808-a6db-65091d5be0c7", + "name": "Model 1a", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "c6aaa4e1-cd0f-4c2a-a5d8-6d0101c27728", + "c2ba0ab0-2797-4d33-85bb-55e4c12424b2": "\"green\"", + "d34e312e-3c73-4e7f-a384-8c7b63d226cc": "> 2" + }, + { + "_id": "c6cc83a2-3b57-4c21-b2e9-a5753c1714cc", + "c2ba0ab0-2797-4d33-85bb-55e4c12424b2": "\"amber\"", + "d34e312e-3c73-4e7f-a384-8c7b63d226cc": "> 1" + }, + { + "_id": "bfde2c94-4736-4e34-acb5-7cf9972d513f", + "c2ba0ab0-2797-4d33-85bb-55e4c12424b2": "\"red\"", + "d34e312e-3c73-4e7f-a384-8c7b63d226cc": "" + } + ], + "inputs": [ + { + "id": "d34e312e-3c73-4e7f-a384-8c7b63d226cc", + "name": "Input", + "type": "expression", + "field": "len(company.secretaries)" + } + ], + "outputs": [ + { + "id": "c2ba0ab0-2797-4d33-85bb-55e4c12424b2", + "name": "Output", + "type": "expression", + "field": "flag.secretaries" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 1230, + "y": -70 + } + }, + { + "id": "71100c8d-ae90-4529-904e-b3741298b739", + "name": "Model 2b", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "37095f5e-ab97-45b0-a252-dd270e618347", + "529a8358-a078-4416-a82c-87a177d674e9": "\"green\"", + "9705e5a5-a590-40da-b1a0-43e6af121e1b": "> 2" + }, + { + "_id": "3877b676-e4ff-4479-910d-8fa9d32809d6", + "529a8358-a078-4416-a82c-87a177d674e9": "\"red\"", + "9705e5a5-a590-40da-b1a0-43e6af121e1b": "" + } + ], + "inputs": [ + { + "id": "9705e5a5-a590-40da-b1a0-43e6af121e1b", + "name": "Input", + "type": "expression", + "field": "len(company.secretaries)" + } + ], + "outputs": [ + { + "id": "529a8358-a078-4416-a82c-87a177d674e9", + "name": "Output", + "type": "expression", + "field": "flag.secretaries" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 1230, + "y": 340 + } + }, + { + "id": "84b0e11b-8c9d-46f3-ac34-f674f3b98068", + "name": "Response", + "type": "outputNode", + "position": { + "x": 1540, + "y": -70 + } + }, + { + "id": "ab1b450a-ce21-4d99-8de6-4e4deeb37d75", + "name": "Model 1b", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "784d6cd6-e1fb-48b0-9254-c264cdecf963", + "014f4025-ddb0-48c3-9e79-aa60368b485b": "> 2", + "af543c04-78da-4661-8a10-9d259afe517a": "\"green\"" + }, + { + "_id": "171ec41f-ac79-4926-a24f-c8d227879989", + "014f4025-ddb0-48c3-9e79-aa60368b485b": "", + "af543c04-78da-4661-8a10-9d259afe517a": "\"red\"" + }, + { + "_id": "fb0638bf-8912-46c3-8569-9116ed3b23f2", + "014f4025-ddb0-48c3-9e79-aa60368b485b": "", + "af543c04-78da-4661-8a10-9d259afe517a": "" + } + ], + "inputs": [ + { + "id": "014f4025-ddb0-48c3-9e79-aa60368b485b", + "name": "Input", + "type": "expression", + "field": "len(company.directors)" + } + ], + "outputs": [ + { + "id": "af543c04-78da-4661-8a10-9d259afe517a", + "name": "Output", + "type": "expression", + "field": "flag.directors" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 1230, + "y": 40 + } + }, + { + "id": "d29b5884-76fc-4c55-8646-3d3417e5b366", + "name": "Response", + "type": "outputNode", + "position": { + "x": 1540, + "y": 440 + } + }, + { + "id": "dc7b8739-e234-4363-afe9-df156f082f6f", + "name": "switchNode 1", + "type": "switchNode", + "content": { + "hitPolicy": "collect", + "statements": [ + { + "id": "e672a228-ff6b-4859-a017-ab5e5c125b60", + "condition": "company.secretaries != null" + }, + { + "id": "5ba3e324-a4fa-4e5f-8523-db21b47b32b6", + "condition": "company.directors != null" + }, + { + "id": "931eda5b-a780-428b-9a0a-e3eb6283bab4", + "condition": "" + } + ] + }, + "position": { + "x": 830, + "y": -80 + } + }, + { + "id": "de6cc00d-ef1b-46f5-9beb-9285d468c39d", + "name": "switchNode 1", + "type": "switchNode", + "content": { + "hitPolicy": "first", + "statements": [ + { + "id": "6499e0bb-2cda-4a5f-9246-d48e7d2177fb", + "condition": "company.type == 'LLC'" + }, + { + "id": "597a39af-4be4-4626-8de9-c110527a49bb", + "condition": "company.type == 'Corporation'" + }, + { + "id": "8319f702-f71a-46da-9372-5e5424dda469", + "condition": "" + } + ] + }, + "position": { + "x": 300, + "y": 140 + } + }, + { + "id": "fecde070-38cf-4656-81d7-3a2cb6e38f8f", + "name": "Request", + "type": "inputNode", + "position": { + "x": -30, + "y": 140 + } + }, + { + "id": "ff35357b-2a9f-4eb0-aaee-edb56e3a0ca2", + "name": "Model Turnover Corp", + "type": "decisionTableNode", + "content": { + "rules": [ + { + "_id": "df314172-11a3-4b52-a688-ed81c8755e4c", + "e083f703-2a6a-495d-88d5-7e5ad71c3a58": "\"green\"", + "fa0fd31a-8865-43fb-8a60-b729c640140a": "> 10000000" + }, + { + "_id": "6a967f72-ee4d-407d-ad16-d0695170222e", + "e083f703-2a6a-495d-88d5-7e5ad71c3a58": "\"amber\"", + "fa0fd31a-8865-43fb-8a60-b729c640140a": "[1000000..10000000]" + }, + { + "_id": "4b7953b5-a8dc-4a1e-9412-32a038eab1bf", + "e083f703-2a6a-495d-88d5-7e5ad71c3a58": "\"red\"", + "fa0fd31a-8865-43fb-8a60-b729c640140a": "" + } + ], + "inputs": [ + { + "id": "fa0fd31a-8865-43fb-8a60-b729c640140a", + "name": "Input", + "type": "expression", + "field": "company.turnover" + } + ], + "outputs": [ + { + "id": "e083f703-2a6a-495d-88d5-7e5ad71c3a58", + "name": "Output", + "type": "expression", + "field": "flag.turnover" + } + ], + "hitPolicy": "first" + }, + "position": { + "x": 1230, + "y": 440 + } + }, + { + "id": "2dce41a5-bec2-4362-9fea-013779b3262b", + "name": "Response", + "type": "outputNode", + "position": { + "x": 610, + "y": 360 + } + } + ], + "edges": [ + { + "id": "4e2890ab-2153-47a4-95f5-7ef2cf982d51", + "type": "edge", + "sourceId": "ab1b450a-ce21-4d99-8de6-4e4deeb37d75", + "targetId": "84b0e11b-8c9d-46f3-ac34-f674f3b98068" + }, + { + "id": "56be46a8-d550-47af-adc4-33fe822b48d3", + "type": "edge", + "sourceId": "dc7b8739-e234-4363-afe9-df156f082f6f", + "targetId": "ab1b450a-ce21-4d99-8de6-4e4deeb37d75", + "sourceHandle": "5ba3e324-a4fa-4e5f-8523-db21b47b32b6" + }, + { + "id": "59ded4c8-3c04-44e7-bd9e-9390cc8e6e6e", + "type": "edge", + "sourceId": "de6cc00d-ef1b-46f5-9beb-9285d468c39d", + "targetId": "dc7b8739-e234-4363-afe9-df156f082f6f", + "sourceHandle": "6499e0bb-2cda-4a5f-9246-d48e7d2177fb" + }, + { + "id": "763fa4fb-8edc-4edc-a790-fe36867f067a", + "type": "edge", + "sourceId": "de6cc00d-ef1b-46f5-9beb-9285d468c39d", + "targetId": "2b06eb6e-f3ed-4a9e-9f36-a0de372d97fe", + "sourceHandle": "597a39af-4be4-4626-8de9-c110527a49bb" + }, + { + "id": "aeae444a-36ec-4d82-a568-07af4f472616", + "type": "edge", + "sourceId": "71100c8d-ae90-4529-904e-b3741298b739", + "targetId": "d29b5884-76fc-4c55-8646-3d3417e5b366" + }, + { + "id": "c636694e-7ac8-45ba-8fa0-cc3bd2a21865", + "type": "edge", + "sourceId": "49f76396-88ae-4808-a6db-65091d5be0c7", + "targetId": "84b0e11b-8c9d-46f3-ac34-f674f3b98068" + }, + { + "id": "d8cd3546-48a5-49ea-8351-b1814b4552df", + "type": "edge", + "sourceId": "fecde070-38cf-4656-81d7-3a2cb6e38f8f", + "targetId": "de6cc00d-ef1b-46f5-9beb-9285d468c39d" + }, + { + "id": "e7d65d51-674d-4ce1-bc7e-1ba8e63e8333", + "type": "edge", + "sourceId": "dc7b8739-e234-4363-afe9-df156f082f6f", + "targetId": "49f76396-88ae-4808-a6db-65091d5be0c7", + "sourceHandle": "e672a228-ff6b-4859-a017-ab5e5c125b60" + }, + { + "id": "f4be274b-d88c-4fff-8f3f-9deb31df9775", + "type": "edge", + "sourceId": "2b06eb6e-f3ed-4a9e-9f36-a0de372d97fe", + "targetId": "71100c8d-ae90-4529-904e-b3741298b739", + "sourceHandle": "2ce01e42-e125-4184-9a8d-2a7ccf8daf14" + }, + { + "id": "455542ce-2c87-4374-baa8-875ce60107f8", + "type": "edge", + "sourceId": "ff35357b-2a9f-4eb0-aaee-edb56e3a0ca2", + "targetId": "d29b5884-76fc-4c55-8646-3d3417e5b366" + }, + { + "id": "70a9923c-b664-4b56-a838-0212e29b1079", + "type": "edge", + "sourceId": "2ee16c8c-fb12-4f20-9813-67bad6f4eb14", + "targetId": "84b0e11b-8c9d-46f3-ac34-f674f3b98068" + }, + { + "id": "321a5e3f-4900-40e1-9003-e331aaa43fa5", + "type": "edge", + "sourceId": "dc7b8739-e234-4363-afe9-df156f082f6f", + "targetId": "2ee16c8c-fb12-4f20-9813-67bad6f4eb14", + "sourceHandle": "931eda5b-a780-428b-9a0a-e3eb6283bab4" + }, + { + "id": "3b620c91-ae91-4ff7-a4a2-87950feff73c", + "type": "edge", + "sourceId": "2b06eb6e-f3ed-4a9e-9f36-a0de372d97fe", + "targetId": "ff35357b-2a9f-4eb0-aaee-edb56e3a0ca2", + "sourceHandle": "3fba0ac2-8498-4421-bb1d-c0c8cabd9c39" + }, + { + "id": "b8e3f866-3528-43cd-a228-30c2d11fca57", + "type": "edge", + "sourceId": "de6cc00d-ef1b-46f5-9beb-9285d468c39d", + "targetId": "2dce41a5-bec2-4362-9fea-013779b3262b", + "sourceHandle": "8319f702-f71a-46da-9372-5e5424dda469" + } + ] +} \ No newline at end of file diff --git a/test-data/switch-node.json b/test-data/switch-node.json new file mode 100644 index 00000000..be0ef4f6 --- /dev/null +++ b/test-data/switch-node.json @@ -0,0 +1,167 @@ +{ + "contentType": "application/vnd.gorules.decision", + "nodes": [ + { + "id": "ca98730e-a40f-4601-98cc-b5a57429596d", + "type": "inputNode", + "position": { + "x": 70, + "y": 150 + }, + "name": "Request" + }, + { + "id": "7b80ff11-c196-4f6f-826f-571ddde1015e", + "type": "outputNode", + "position": { + "x": 1020, + "y": 120 + }, + "name": "Response" + }, + { + "id": "46ec83d6-44e3-4436-b57f-915e0718c1ca", + "type": "outputNode", + "position": { + "x": 1020, + "y": 250 + }, + "name": "Response" + }, + { + "id": "9dc1082f-fbe7-4bee-a32a-bcc11be71985", + "type": "switchNode", + "position": { + "x": 380, + "y": 150 + }, + "name": "switchNode 1", + "content": { + "statements": [ + { + "id": "06bac0d2-9da9-444a-8c14-75bfd165ee5f", + "condition": "color == 'yellow'" + }, + { + "id": "3784c59e-7f38-40f6-a0d2-58ebc7f24d4c", + "condition": "color == 'red'" + }, + { + "id": "9db25b58-02d9-45a1-ad03-77e5c5b5cf6d", + "condition": "" + } + ] + } + }, + { + "id": "05e8efcc-9151-4681-97a3-ffc46d9678f2", + "type": "expressionNode", + "position": { + "x": 740, + "y": 120 + }, + "name": "EN1", + "content": { + "expressions": [ + { + "id": "f8e969ad-87a6-4392-9a4e-b41c858f6bd2", + "key": "path1", + "value": "true" + } + ] + } + }, + { + "id": "6d3d30cf-263c-4a47-b85f-4d07cd6e319b", + "type": "expressionNode", + "position": { + "x": 740, + "y": 380 + }, + "name": "EN3", + "content": { + "expressions": [ + { + "id": "9c0a323e-fec9-4a14-bd52-bbde8c7fcdc0", + "key": "path3", + "value": "true" + } + ] + } + }, + { + "id": "ffd7380c-861e-4f1a-ab33-e287dbd17e85", + "type": "expressionNode", + "position": { + "x": 740, + "y": 250 + }, + "name": "EN2", + "content": { + "expressions": [ + { + "id": "8cca0067-ae52-437a-8b92-451eb1dc54f6", + "key": "path2", + "value": "true" + } + ] + } + }, + { + "id": "825acd18-dc9d-4d98-ab4f-d57bb29d191a", + "type": "outputNode", + "position": { + "x": 1020, + "y": 380 + }, + "name": "Response" + } + ], + "edges": [ + { + "id": "af35139a-48d5-4f54-a835-b6a49dc80c76", + "sourceId": "ca98730e-a40f-4601-98cc-b5a57429596d", + "type": "edge", + "targetId": "9dc1082f-fbe7-4bee-a32a-bcc11be71985" + }, + { + "id": "edef36ef-4f0f-4a70-b625-50b71f2af4b8", + "sourceId": "9dc1082f-fbe7-4bee-a32a-bcc11be71985", + "type": "edge", + "targetId": "05e8efcc-9151-4681-97a3-ffc46d9678f2", + "sourceHandle": "06bac0d2-9da9-444a-8c14-75bfd165ee5f" + }, + { + "id": "1d9e31bb-4a64-4546-993a-1bbbeb699eb4", + "sourceId": "05e8efcc-9151-4681-97a3-ffc46d9678f2", + "type": "edge", + "targetId": "7b80ff11-c196-4f6f-826f-571ddde1015e" + }, + { + "id": "79570832-2285-471e-bd65-e45b730b5071", + "sourceId": "9dc1082f-fbe7-4bee-a32a-bcc11be71985", + "type": "edge", + "targetId": "ffd7380c-861e-4f1a-ab33-e287dbd17e85", + "sourceHandle": "3784c59e-7f38-40f6-a0d2-58ebc7f24d4c" + }, + { + "id": "9757cd62-3d39-4814-b322-c965a37d0c9f", + "sourceId": "ffd7380c-861e-4f1a-ab33-e287dbd17e85", + "type": "edge", + "targetId": "46ec83d6-44e3-4436-b57f-915e0718c1ca" + }, + { + "id": "b73856ef-9921-4a1c-b776-3310ec3a9692", + "sourceId": "9dc1082f-fbe7-4bee-a32a-bcc11be71985", + "type": "edge", + "targetId": "6d3d30cf-263c-4a47-b85f-4d07cd6e319b", + "sourceHandle": "9db25b58-02d9-45a1-ad03-77e5c5b5cf6d" + }, + { + "id": "9a058ad7-ffe5-4efb-a5ad-428af59a7839", + "sourceId": "6d3d30cf-263c-4a47-b85f-4d07cd6e319b", + "type": "edge", + "targetId": "825acd18-dc9d-4d98-ab4f-d57bb29d191a" + } + ] +} \ No newline at end of file