mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 16:02:18 +00:00
fix: v1 behaviour hardening (#502)
This commit is contained in:
@@ -45,6 +45,7 @@ impl fmt::Display for CompileFailure {
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum EvaluationError {
|
||||
#[error("Loader error")]
|
||||
LoaderError(LoaderError),
|
||||
|
||||
@@ -118,8 +118,6 @@
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
// `deny` rather than `forbid` so `nodes::variable_json` can opt in: reading a
|
||||
// `RefCell` without a guard is the only way to hand jsonschema `'a` borrows.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used)]
|
||||
#![allow(clippy::module_inception)]
|
||||
|
||||
@@ -99,10 +99,14 @@ impl JsonNumber for VariableNumber {
|
||||
}
|
||||
|
||||
fn to_number(&self) -> Cow<'_, serde_json::Number> {
|
||||
let number = self
|
||||
.0
|
||||
.to_f64()
|
||||
.and_then(serde_json::Number::from_f64)
|
||||
let normalized = self.0.normalize().to_string();
|
||||
#[cfg(feature = "arbitrary_precision")]
|
||||
let number = serde_json::Number::from_string_unchecked(normalized);
|
||||
#[cfg(not(feature = "arbitrary_precision"))]
|
||||
let number = normalized
|
||||
.parse()
|
||||
.ok()
|
||||
.or_else(|| self.0.to_f64().and_then(serde_json::Number::from_f64))
|
||||
.unwrap_or_else(|| serde_json::Number::from(0));
|
||||
Cow::Owned(number)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ use serde_json::json;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::Builder;
|
||||
use zen_engine::{Decision, DecisionGraphValidationError, EvaluationError, EvaluationOptions};
|
||||
use zen_engine::{
|
||||
Decision, DecisionGraphValidationError, EvaluationError, EvaluationOptions, Variable,
|
||||
};
|
||||
|
||||
mod support;
|
||||
|
||||
@@ -244,3 +246,163 @@ async fn decision_table_missing_cell_key_is_treated_as_empty() {
|
||||
.unwrap();
|
||||
assert_eq!(traced.result, result.result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(miri, ignore)]
|
||||
async fn node_handlers_do_not_mutate_aliased_inputs() {
|
||||
let content = serde_json::from_value(json!({
|
||||
"nodes": [
|
||||
{ "id": "in", "name": "in", "type": "inputNode", "content": {} },
|
||||
{
|
||||
"id": "e1", "name": "e1", "type": "expressionNode",
|
||||
"content": {
|
||||
"passThrough": true,
|
||||
"expressions": [{ "id": "x1", "key": "first", "value": "a + 1" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sw", "name": "sw", "type": "switchNode",
|
||||
"content": {
|
||||
"hitPolicy": "first",
|
||||
"statements": [{ "id": "s1", "condition": "" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "e2", "name": "e2", "type": "expressionNode",
|
||||
"content": {
|
||||
"passThrough": true,
|
||||
"expressions": [{ "id": "x2", "key": "second", "value": "first + 1" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dt", "name": "dt", "type": "decisionTableNode",
|
||||
"content": {
|
||||
"passThrough": true,
|
||||
"hitPolicy": "first",
|
||||
"inputs": [{ "id": "i1", "name": "A", "field": "a" }],
|
||||
"outputs": [{ "id": "o1", "name": "Result", "field": "dtResult" }],
|
||||
"rules": [{ "_id": "r1", "i1": "", "o1": "'hit'" }]
|
||||
}
|
||||
},
|
||||
{ "id": "out", "name": "out", "type": "outputNode", "content": {} }
|
||||
],
|
||||
"edges": [
|
||||
{ "id": "ed1", "sourceId": "in", "targetId": "e1" },
|
||||
{ "id": "ed2", "sourceId": "e1", "targetId": "sw" },
|
||||
{ "id": "ed3", "sourceId": "sw", "targetId": "e2", "sourceHandle": "s1" },
|
||||
{ "id": "ed4", "sourceId": "e2", "targetId": "dt" },
|
||||
{ "id": "ed5", "sourceId": "dt", "targetId": "out" }
|
||||
]
|
||||
}))
|
||||
.unwrap();
|
||||
let decision = Decision::from(Arc::new(content));
|
||||
|
||||
let input: Variable = json!({ "a": 1, "nested": { "k": "v" } }).into();
|
||||
let snapshot = input.to_value();
|
||||
|
||||
let response = decision
|
||||
.evaluate_with_opts(
|
||||
input.clone(),
|
||||
EvaluationOptions {
|
||||
trace: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(input.to_value(), snapshot);
|
||||
assert_eq!(
|
||||
response.result,
|
||||
json!({ "a": 1, "nested": { "k": "v" }, "first": 2, "second": 3, "dtResult": "hit" })
|
||||
.into()
|
||||
);
|
||||
|
||||
let trace = response.trace.unwrap().into_graph().unwrap();
|
||||
let output_of = |id: &str| trace.get(id).unwrap().output.clone();
|
||||
|
||||
assert_eq!(
|
||||
output_of("in"),
|
||||
json!({ "a": 1, "nested": { "k": "v" } }).into()
|
||||
);
|
||||
assert_eq!(
|
||||
output_of("e1"),
|
||||
json!({ "a": 1, "nested": { "k": "v" }, "first": 2 }).into()
|
||||
);
|
||||
assert_eq!(
|
||||
output_of("sw"),
|
||||
json!({ "a": 1, "nested": { "k": "v" }, "first": 2 }).into()
|
||||
);
|
||||
assert_eq!(
|
||||
output_of("e2"),
|
||||
json!({ "a": 1, "nested": { "k": "v" }, "first": 2, "second": 3 }).into()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg_attr(miri, ignore)]
|
||||
async fn merged_inputs_do_not_mutate_parent_node_outputs() {
|
||||
let content = serde_json::from_value(json!({
|
||||
"nodes": [
|
||||
{ "id": "in", "name": "in", "type": "inputNode", "content": {} },
|
||||
{
|
||||
"id": "pa", "name": "pa", "type": "expressionNode",
|
||||
"content": {
|
||||
"passThrough": true,
|
||||
"expressions": [{ "id": "x1", "key": "pa", "value": "10" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pb", "name": "pb", "type": "expressionNode",
|
||||
"content": {
|
||||
"passThrough": true,
|
||||
"expressions": [{ "id": "x2", "key": "pb", "value": "20" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "join", "name": "join", "type": "expressionNode",
|
||||
"content": {
|
||||
"passThrough": true,
|
||||
"expressions": [{ "id": "x3", "key": "sum", "value": "pa + pb" }]
|
||||
}
|
||||
},
|
||||
{ "id": "out", "name": "out", "type": "outputNode", "content": {} }
|
||||
],
|
||||
"edges": [
|
||||
{ "id": "ed1", "sourceId": "in", "targetId": "pa" },
|
||||
{ "id": "ed2", "sourceId": "in", "targetId": "pb" },
|
||||
{ "id": "ed3", "sourceId": "pa", "targetId": "join" },
|
||||
{ "id": "ed4", "sourceId": "pb", "targetId": "join" },
|
||||
{ "id": "ed5", "sourceId": "join", "targetId": "out" }
|
||||
]
|
||||
}))
|
||||
.unwrap();
|
||||
let decision = Decision::from(Arc::new(content));
|
||||
|
||||
let input: Variable = json!({ "a": 1 }).into();
|
||||
let snapshot = input.to_value();
|
||||
|
||||
let response = decision
|
||||
.evaluate_with_opts(
|
||||
input.clone(),
|
||||
EvaluationOptions {
|
||||
trace: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(input.to_value(), snapshot);
|
||||
assert_eq!(
|
||||
response.result,
|
||||
json!({ "a": 1, "pa": 10, "pb": 20, "sum": 30 }).into()
|
||||
);
|
||||
|
||||
let trace = response.trace.unwrap().into_graph().unwrap();
|
||||
let output_of = |id: &str| trace.get(id).unwrap().output.clone();
|
||||
|
||||
assert_eq!(output_of("in"), json!({ "a": 1 }).into());
|
||||
assert_eq!(output_of("pa"), json!({ "a": 1, "pa": 10 }).into());
|
||||
assert_eq!(output_of("pb"), json!({ "a": 1, "pb": 20 }).into());
|
||||
}
|
||||
|
||||
@@ -476,7 +476,7 @@ pub(crate) mod imp {
|
||||
let a = args.array(0)?;
|
||||
let arr = a.borrow();
|
||||
|
||||
let Some(first) = arr.first() else {
|
||||
let Some(first) = arr.iter().find(|item| !matches!(item, V::Null)) else {
|
||||
return Ok(V::empty_object());
|
||||
};
|
||||
|
||||
|
||||
@@ -161,6 +161,11 @@ flatMap([[1, 2], [3, 4], [5, 6]], #);; [1, 2, 3, 4, 5, 6]
|
||||
keys([10, 11, 12]);;[0, 1, 2]
|
||||
merge([[1, 2], [3, 4], [5]]);;[1, 2, 3, 4, 5]
|
||||
merge([[1, 2], [], [3]]);;[1, 2, 3]
|
||||
merge([null, [1, 2], [3]]);;[1, 2, 3]
|
||||
merge([[1, 2], null, [3]]);;[1, 2, 3]
|
||||
merge([null, {"a": 1}, {"b": 2}]);;{"a": 1, "b": 2}
|
||||
merge([null]);;{}
|
||||
merge([null, null]);;{}
|
||||
merge([]);;{}
|
||||
merge([{"a": 1}, {"b": 2}, {"c": 3}]);;{"a": 1, "b": 2, "c": 3}
|
||||
merge([{"a": 1, "b": 2}, {"b": 3, "c": 4}]);;{"a": 1, "b": 3, "c": 4}
|
||||
@@ -170,6 +175,7 @@ mergeDeep([{"a": {"x": 1}}, {"a": {"y": 2}}]);;{"a": {"x": 1, "y": 2}}
|
||||
mergeDeep([{"a": [1, 2]}, {"a": [3, 4]}]);;{"a": [1, 2, 3, 4]}
|
||||
mergeDeep([{"a": {"b": {"c": 1}}}, {"a": {"b": {"d": 2}}}]);;{"a": {"b": {"c": 1, "d": 2}}}
|
||||
mergeDeep([{"a": 1}, {"a": 2}]);;{"a": 2}
|
||||
mergeDeep([null, {"a": 1}]);;{"a": 1}
|
||||
mergeDeep([]);;{}
|
||||
|
||||
# Dates
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 14 and column 13.
|
Reference in New Issue
Block a user