feat: rc variable (#246)

* feat: rc variable

* feat: implement variable in engine

* add exports

* fix bindings and improve clone accuracy

* fix tests, fix bindings

* fix fmt

* improve code
This commit is contained in:
stefan-gorules
2024-09-30 18:34:54 +02:00
committed by GitHub
parent 06df117694
commit 91598166ce
55 changed files with 1148 additions and 1399 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
use serde_json::Value;
use std::ffi::{c_char, c_void, CStr, CString};
use std::marker::{PhantomData, PhantomPinned};
use std::ops::{Deref, DerefMut};
use zen_engine::Decision;
use crate::custom_node::DynamicCustomNode;
@@ -69,13 +69,13 @@ pub extern "C" fn zen_decision_evaluate(
return ZenResult::error(ZenError::InvalidArgument);
};
let Ok(context) = serde_json::from_str(str_context) else {
let Ok(context) = serde_json::from_str::<Value>(str_context) else {
return ZenResult::error(ZenError::JsonDeserializationFailed);
};
let zen_decision = unsafe { &*(decision as *mut ZenDecision) };
let maybe_result =
tokio_runtime().block_on(zen_decision.evaluate_with_opts(&context, options.into()));
tokio_runtime().block_on(zen_decision.evaluate_with_opts(context.into(), options.into()));
let result = match maybe_result {
Ok(r) => r,
Err(e) => return ZenResult::from(&e),
+3 -3
View File
@@ -1,8 +1,8 @@
use serde_json::Value;
use std::ffi::{c_char, CStr, CString};
use std::marker::{PhantomData, PhantomPinned};
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use zen_engine::{DecisionEngine, EvaluationOptions};
use crate::custom_node::DynamicCustomNode;
@@ -121,7 +121,7 @@ pub extern "C" fn zen_engine_evaluate(
};
let cstr_context = unsafe { CStr::from_ptr(context) };
let Ok(val_context) = serde_json::from_slice(cstr_context.to_bytes()) else {
let Ok(val_context) = serde_json::from_slice::<Value>(cstr_context.to_bytes()) else {
return ZenResult::error(ZenError::JsonDeserializationFailed);
};
@@ -129,7 +129,7 @@ pub extern "C" fn zen_engine_evaluate(
let maybe_result = tokio_runtime().block_on(zen_engine.evaluate_with_opts(
str_key,
&val_context,
val_context.into(),
options.into(),
));
let result = match maybe_result {
+7 -7
View File
@@ -1,7 +1,7 @@
use std::ffi::CString;
use libc::{c_char, c_int};
use serde_json::Value;
use zen_expression::{evaluate_expression, evaluate_unary_expression};
use crate::error::ZenError;
@@ -21,11 +21,11 @@ pub extern "C" fn zen_evaluate_expression(
return ZenResult::error(ZenError::InvalidArgument);
};
let Ok(context_val) = serde_json::from_slice(context_cstr.to_bytes()) else {
let Ok(context_val) = serde_json::from_slice::<Value>(context_cstr.to_bytes()) else {
return ZenResult::error(ZenError::JsonDeserializationFailed);
};
let maybe_result = evaluate_expression(expression_str, &context_val);
let maybe_result = evaluate_expression(expression_str, context_val.into());
let result = match maybe_result {
Ok(r) => r,
Err(err) => return ZenResult::from(&err),
@@ -55,11 +55,11 @@ pub extern "C" fn zen_evaluate_unary_expression(
return ZenResult::error(ZenError::InvalidArgument);
};
let Ok(context_val) = serde_json::from_slice(context_cstr.to_bytes()) else {
let Ok(context_val) = serde_json::from_slice::<Value>(context_cstr.to_bytes()) else {
return ZenResult::error(ZenError::JsonDeserializationFailed);
};
let maybe_result = evaluate_unary_expression(expression_str, &context_val);
let maybe_result = evaluate_unary_expression(expression_str, context_val.into());
let result = match maybe_result {
Ok(r) => r,
Err(err) => return ZenResult::from(&err),
@@ -86,11 +86,11 @@ pub extern "C" fn zen_evaluate_template(
return ZenResult::error(ZenError::InvalidArgument);
};
let Ok(context_val) = serde_json::from_slice(context_cstr.to_bytes()) else {
let Ok(context_val) = serde_json::from_slice::<Value>(context_cstr.to_bytes()) else {
return ZenResult::error(ZenError::JsonDeserializationFailed);
};
let result = match zen_tmpl::render(template_str, &context_val) {
let result = match zen_tmpl::render(template_str, context_val.into()) {
Ok(r) => r,
Err(err) => {
return ZenResult::error(ZenError::TemplateEngineError {
+2 -2
View File
@@ -31,7 +31,7 @@ impl CustomNodeAdapter for CustomNode {
let promise: Promise<ZenEngineHandlerResponse> = function
.clone()
.call_async(ZenEngineHandlerRequest {
input: request.input.clone(),
input: request.input.to_value(),
node: node_data,
})
.await
@@ -40,7 +40,7 @@ impl CustomNodeAdapter for CustomNode {
let result = promise.await.map_err(|err| anyhow!(err.reason))?;
Ok(NodeResponse {
output: result.output,
output: result.output.into(),
trace_data: result.trace_data,
})
}
+3 -2
View File
@@ -39,13 +39,14 @@ impl ZenDecision {
async move {
decision
.evaluate_with_opts(
&context,
context.into(),
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
},
)
.await
.map(ZenEngineResponse::from)
}
})
.await
@@ -54,7 +55,7 @@ impl ZenDecision {
anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string()))
})?;
Ok(ZenEngineResponse::from(result))
Ok(result)
}
#[napi(ts_return_type = "Promise<SafeResult<ZenEngineResponse>>")]
+3 -2
View File
@@ -118,13 +118,14 @@ impl ZenEngine {
graph
.evaluate_with_opts(
key,
&context,
context.into(),
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
},
)
.await
.map(ZenEngineResponse::from)
}
})
.await
@@ -133,7 +134,7 @@ impl ZenEngine {
anyhow!(serde_json::to_string(e.as_ref()).unwrap_or_else(|_| e.to_string()))
})?;
Ok(ZenEngineResponse::from(result))
Ok(result)
}
#[napi]
+7 -5
View File
@@ -7,8 +7,9 @@ pub fn evaluate_expression_sync(expression: String, context: Option<Value>) -> n
let ctx = context.unwrap_or(Value::Null);
Ok(
zen_expression::evaluate_expression(expression.as_str(), &ctx)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?,
zen_expression::evaluate_expression(expression.as_str(), ctx.into())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?
.to_value(),
)
}
@@ -16,7 +17,7 @@ pub fn evaluate_expression_sync(expression: String, context: Option<Value>) -> n
#[napi]
pub fn evaluate_unary_expression_sync(expression: String, context: Value) -> napi::Result<bool> {
Ok(
zen_expression::evaluate_unary_expression(expression.as_str(), &context)
zen_expression::evaluate_unary_expression(expression.as_str(), context.into())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?,
)
}
@@ -24,8 +25,9 @@ pub fn evaluate_unary_expression_sync(expression: String, context: Value) -> nap
#[allow(dead_code)]
#[napi]
pub fn render_template_sync(template: String, context: Value) -> napi::Result<Value> {
Ok(zen_tmpl::render(template.as_str(), &context)
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?)
Ok(zen_tmpl::render(template.as_str(), context.into())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?
.to_value())
}
#[allow(dead_code)]
+6 -5
View File
@@ -7,6 +7,7 @@ use serde_json::Value;
use zen_engine::handler::custom_node_adapter::CustomDecisionNode;
use zen_engine::{DecisionGraphResponse, DecisionGraphTrace};
use zen_expression::Variable;
#[napi(object)]
pub struct ZenEngineTrace {
@@ -23,8 +24,8 @@ impl From<DecisionGraphTrace> for ZenEngineTrace {
Self {
id: value.id,
name: value.name,
input: value.input,
output: value.output,
input: value.input.to_value(),
output: value.output.to_value(),
performance: value.performance,
trace_data: value.trace_data,
}
@@ -42,7 +43,7 @@ impl From<DecisionGraphResponse> for ZenEngineResponse {
fn from(value: DecisionGraphResponse) -> Self {
Self {
performance: value.performance,
result: value.result,
result: value.result.to_value(),
trace: value.trace.map(|opt| {
opt.into_iter()
.map(|(key, value)| (key, ZenEngineTrace::from(value)))
@@ -104,10 +105,10 @@ impl ZenEngineHandlerRequest {
return Ok(selected_value);
};
let template_value = zen_tmpl::render(template.as_str(), &self.input)
let template_value = zen_tmpl::render(template.as_str(), Variable::from(&self.input))
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
Ok(template_value)
Ok(template_value.to_value())
}
#[napi(ts_return_type = "unknown")]
+5 -5
View File
@@ -5,7 +5,7 @@ use pyo3::types::PyDict;
use pyo3::{pyclass, pymethods, PyAny, PyObject, PyResult, Python, ToPyObject};
use pyo3_asyncio::tokio;
use pythonize::depythonize;
use serde_json::Value;
use zen_engine::{Decision, EvaluationOptions};
use crate::custom_node::PyCustomNode;
@@ -26,7 +26,7 @@ impl From<Decision<PyDecisionLoader, PyCustomNode>> for PyZenDecision {
#[pymethods]
impl PyZenDecision {
pub fn evaluate(&self, py: Python, ctx: &PyDict, opts: Option<&PyDict>) -> PyResult<PyObject> {
let context = depythonize(ctx).context("Failed to convert dict")?;
let context: Value = depythonize(ctx).context("Failed to convert dict")?;
let options: PyZenEvaluateOptions = if let Some(op) = opts {
depythonize(op).context("Failed to convert dict")?
} else {
@@ -35,7 +35,7 @@ impl PyZenDecision {
let decision = self.0.clone();
let result = futures::executor::block_on(decision.evaluate_with_opts(
&context,
context.into(),
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
@@ -55,7 +55,7 @@ impl PyZenDecision {
ctx: &PyDict,
opts: Option<&PyDict>,
) -> PyResult<&PyAny> {
let context = depythonize(ctx).context("Failed to convert dict")?;
let context: Value = depythonize(ctx).context("Failed to convert dict")?;
let options: PyZenEvaluateOptions = if let Some(op) = opts {
depythonize(op).context("Failed to convert dict")?
} else {
@@ -65,7 +65,7 @@ impl PyZenDecision {
let decision = self.0.clone();
tokio::future_into_py(py, async move {
let result = futures::executor::block_on(decision.evaluate_with_opts(
&context,
context.into(),
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
+6 -6
View File
@@ -6,7 +6,7 @@ use pyo3::{pyclass, pymethods, PyAny, PyObject, PyResult, Python, ToPyObject};
use pyo3_asyncio::tokio;
use pythonize::depythonize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use zen_engine::model::DecisionContent;
use zen_engine::{DecisionEngine, EvaluationOptions};
@@ -82,7 +82,7 @@ impl PyZenEngine {
ctx: &PyDict,
opts: Option<&PyDict>,
) -> PyResult<PyObject> {
let context = depythonize(ctx).context("Failed to convert dict")?;
let context: Value = depythonize(ctx).context("Failed to convert dict")?;
let options: PyZenEvaluateOptions = if let Some(op) = opts {
depythonize(op).context("Failed to convert dict")?
} else {
@@ -92,7 +92,7 @@ impl PyZenEngine {
let graph = self.graph.clone();
let result = futures::executor::block_on(graph.evaluate_with_opts(
key,
&context,
context.into(),
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
@@ -113,7 +113,7 @@ impl PyZenEngine {
ctx: &PyDict,
opts: Option<&PyDict>,
) -> PyResult<&PyAny> {
let context = depythonize(ctx).context("Failed to convert dict")?;
let context: Value = depythonize(ctx).context("Failed to convert dict")?;
let options: PyZenEvaluateOptions = if let Some(op) = opts {
depythonize(op).context("Failed to convert dict")?
} else {
@@ -124,7 +124,7 @@ impl PyZenEngine {
tokio::future_into_py(py, async move {
let result = futures::executor::block_on(graph.evaluate_with_opts(
key,
&context,
context.into(),
EvaluationOptions {
max_depth: options.max_depth,
trace: options.trace,
@@ -148,7 +148,7 @@ impl PyZenEngine {
Ok(PyZenDecision::from(decision))
}
pub fn get_decision<'py>(&'py self, py: Python<'py>, key: String) -> PyResult<PyZenDecision> {
pub fn get_decision<'py>(&'py self, _py: Python<'py>, key: String) -> PyResult<PyZenDecision> {
let decision = futures::executor::block_on(self.graph.get_decision(&key))
.context("Failed to find decision with given key")?;
+5 -5
View File
@@ -18,17 +18,17 @@ pub fn evaluate_expression(
.context("Failed to convert context")?
.unwrap_or(Value::Null);
let result = zen_expression::evaluate_expression(expression.as_str(), &context)
let result = zen_expression::evaluate_expression(expression.as_str(), context.into())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
Ok(PyValue(result).to_object(py))
Ok(PyValue(result.to_value()).to_object(py))
}
#[pyfunction]
pub fn evaluate_unary_expression(expression: String, ctx: &PyDict) -> PyResult<bool> {
let context: Value = depythonize(ctx).context("Failed to convert context")?;
let result = zen_expression::evaluate_unary_expression(expression.as_str(), &context)
let result = zen_expression::evaluate_unary_expression(expression.as_str(), context.into())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
Ok(result)
@@ -38,8 +38,8 @@ pub fn evaluate_unary_expression(expression: String, ctx: &PyDict) -> PyResult<b
pub fn render_template(py: Python, template: String, ctx: &PyDict) -> PyResult<PyObject> {
let context: Value = depythonize(ctx).context("Failed to convert context")?;
let result = zen_tmpl::render(template.as_str(), &context)
let result = zen_tmpl::render(template.as_str(), context.into())
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
Ok(PyValue(result).to_object(py))
Ok(PyValue(result.to_value()).to_object(py))
}
+8 -8
View File
@@ -4,12 +4,12 @@ use pyo3::{pyclass, pymethods, PyObject, PyResult, Python, ToPyObject};
use serde::Serialize;
use serde_json::Value;
use crate::value::{value_to_object, PyValue};
use zen_engine::handler::custom_node_adapter::{
CustomDecisionNode as BaseCustomDecisionNode, CustomNodeRequest,
};
use zen_engine::handler::node::NodeResponse;
use crate::value::{value_to_object, PyValue};
use zen_expression::Variable;
#[derive(Serialize)]
struct CustomDecisionNode {
@@ -50,10 +50,10 @@ impl PyNodeRequest {
let node_val = serde_json::to_value(&inner_node).unwrap();
Ok(Self {
input: value_to_object(py, &value.input),
input: value_to_object(py, &value.input.to_value()),
node: value_to_object(py, &node_val),
inner_input: value.input.clone(),
inner_input: value.input.to_value(),
inner_node,
})
}
@@ -73,10 +73,10 @@ impl PyNodeRequest {
return Ok(PyValue(selected_value).to_object(py));
};
let template_value = zen_tmpl::render(template.as_str(), &self.inner_input)
let template_value = zen_tmpl::render(template.as_str(), Variable::from(&self.inner_input))
.map_err(|e| anyhow!(serde_json::to_string(&e).unwrap_or_else(|_| e.to_string())))?;
Ok(PyValue(template_value).to_object(py))
Ok(PyValue(template_value.to_value()).to_object(py))
}
fn get_field_raw(&self, py: Python, path: String) -> PyResult<PyObject> {
@@ -102,7 +102,7 @@ pub struct PyNodeResponse {
impl From<NodeResponse> for PyNodeResponse {
fn from(value: NodeResponse) -> Self {
Self {
output: value.output,
output: value.output.to_value(),
trace_data: value.trace_data,
}
}
@@ -111,7 +111,7 @@ impl From<NodeResponse> for PyNodeResponse {
impl From<PyNodeResponse> for NodeResponse {
fn from(value: PyNodeResponse) -> Self {
Self {
output: value.output,
output: value.output.into(),
trace_data: value.trace_data,
}
}
+2
View File
@@ -11,6 +11,7 @@ repository = "https://github.com/gorules/zen.git"
doctest = false
[dependencies]
ahash = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
bincode = { workspace = true, optional = true }
@@ -19,6 +20,7 @@ serde_json = { workspace = true, features = ["arbitrary_precision"] }
serde = { workspace = true, features = ["derive"] }
once_cell = { workspace = true }
json_dotpath = { workspace = true }
rust_decimal = { workspace = true, features = ["maths-nopanic"] }
fixedbitset = "0.4.2"
tokio = { workspace = true, features = ["sync", "time"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
+8 -7
View File
@@ -1,11 +1,12 @@
use criterion::{criterion_group, criterion_main, Bencher, Criterion};
use serde_json::{json, Value};
use serde_json::json;
use std::path::Path;
use std::sync::Arc;
use tokio::runtime::Runtime;
use zen_engine::handler::custom_node_adapter::NoopCustomNode;
use zen_engine::loader::{FilesystemLoader, FilesystemLoaderOptions};
use zen_engine::DecisionEngine;
use zen_expression::variable::Variable;
fn create_graph() -> DecisionEngine<FilesystemLoader, NoopCustomNode> {
let cargo_root = Path::new(env!("CARGO_MANIFEST_DIR"));
@@ -21,32 +22,32 @@ fn create_graph() -> DecisionEngine<FilesystemLoader, NoopCustomNode> {
DecisionEngine::new(Arc::new(loader), Arc::new(NoopCustomNode::default()))
}
fn bench_decision(b: &mut Bencher, key: &str, context: Value) {
fn bench_decision(b: &mut Bencher, key: &str, context: Variable) {
let rt = Runtime::new().unwrap();
let graph = create_graph();
let decision = rt.block_on(graph.get_decision(key)).unwrap();
b.to_async(&rt).iter(|| async {
criterion::black_box(decision.evaluate(&context).await.unwrap());
criterion::black_box(decision.evaluate(context.clone()).await.unwrap());
});
}
fn bench_loader(b: &mut Bencher, key: &str, context: Value) {
fn bench_loader(b: &mut Bencher, key: &str, context: Variable) {
let rt = Runtime::new().unwrap();
let graph = create_graph();
b.to_async(&rt).iter(|| async {
criterion::black_box(graph.evaluate(key, &context).await.unwrap());
criterion::black_box(graph.evaluate(key, context.clone()).await.unwrap());
});
}
fn bench_functions(c: &mut Criterion) {
c.bench_function("loader/table", |b| {
bench_loader(b, "table.json", json!({ "input": 15 }));
bench_loader(b, "table.json", json!({ "input": 15 }).into());
});
c.bench_function("decision/table", |b| {
bench_decision(b, "table.json", json!({ "input": 15 }));
bench_decision(b, "table.json", json!({ "input": 15 }).into());
});
}
+3 -4
View File
@@ -1,13 +1,12 @@
use std::sync::Arc;
use serde_json::Value;
use crate::engine::EvaluationOptions;
use crate::handler::custom_node_adapter::{CustomNodeAdapter, NoopCustomNode};
use crate::handler::graph::{DecisionGraph, DecisionGraphConfig, DecisionGraphResponse};
use crate::loader::{CachedLoader, DecisionLoader, NoopLoader};
use crate::model::DecisionContent;
use crate::{DecisionGraphValidationError, EvaluationError};
use zen_expression::variable::Variable;
/// Represents a JDM decision which can be evaluated
#[derive(Debug, Clone)]
@@ -71,7 +70,7 @@ where
/// Evaluates a decision using an in-memory reference stored in struct
pub async fn evaluate(
&self,
context: &Value,
context: Variable,
) -> Result<DecisionGraphResponse, Box<EvaluationError>> {
self.evaluate_with_opts(context, Default::default()).await
}
@@ -79,7 +78,7 @@ where
/// Evaluates a decision using in-memory reference with advanced options
pub async fn evaluate_with_opts(
&self,
context: &Value,
context: Variable,
options: EvaluationOptions,
) -> Result<DecisionGraphResponse, Box<EvaluationError>> {
let mut decision_graph = DecisionGraph::try_new(DecisionGraphConfig {
+3 -4
View File
@@ -1,14 +1,13 @@
use std::future::Future;
use std::sync::Arc;
use serde_json::Value;
use crate::decision::Decision;
use crate::handler::custom_node_adapter::{CustomNodeAdapter, NoopCustomNode};
use crate::handler::graph::DecisionGraphResponse;
use crate::loader::{ClosureLoader, DecisionLoader, LoaderResponse, LoaderResult, NoopLoader};
use crate::model::DecisionContent;
use crate::EvaluationError;
use zen_expression::variable::Variable;
/// Structure used for generating and evaluating JDM decisions
#[derive(Debug, Clone)]
@@ -76,7 +75,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionEngine
pub async fn evaluate<K>(
&self,
key: K,
context: &Value,
context: Variable,
) -> Result<DecisionGraphResponse, Box<EvaluationError>>
where
K: AsRef<str>,
@@ -89,7 +88,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionEngine
pub async fn evaluate_with_opts<K>(
&self,
key: K,
context: &Value,
context: Variable,
options: EvaluationOptions,
) -> Result<DecisionGraphResponse, Box<EvaluationError>>
where
@@ -4,13 +4,14 @@ use anyhow::anyhow;
use json_dotpath::DotPaths;
use serde::Serialize;
use serde_json::Value;
use zen_expression::variable::Variable;
use zen_tmpl::TemplateRenderError;
pub trait CustomNodeAdapter {
fn handle(
&self,
request: CustomNodeRequest<'_>,
) -> impl std::future::Future<Output = NodeResult> + Send;
) -> impl std::future::Future<Output = NodeResult>;
}
#[derive(Default, Debug)]
@@ -25,7 +26,7 @@ impl CustomNodeAdapter for NoopCustomNode {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomNodeRequest<'a> {
pub input: &'a Value,
pub input: Variable,
pub node: CustomDecisionNode<'a>,
}
@@ -34,27 +35,27 @@ impl<'a> TryFrom<&'a NodeRequest<'a>> for CustomNodeRequest<'a> {
fn try_from(value: &'a NodeRequest<'a>) -> Result<Self, Self::Error> {
Ok(Self {
input: &value.input,
input: value.input.clone(),
node: value.node.try_into()?,
})
}
}
impl<'a> CustomNodeRequest<'a> {
pub fn get_field(&self, path: &str) -> Result<Option<Value>, TemplateRenderError> {
pub fn get_field(&self, path: &str) -> Result<Option<Variable>, TemplateRenderError> {
let Some(selected_value) = self.get_field_raw(path) else {
return Ok(None);
};
let Value::String(template) = selected_value else {
let Variable::String(template) = selected_value else {
return Ok(Some(selected_value));
};
let template_value = zen_tmpl::render(template.as_str(), &self.input)?;
let template_value = zen_tmpl::render(template.as_ref(), self.input.clone())?;
Ok(Some(template_value))
}
fn get_field_raw(&self, path: &str) -> Option<Value> {
fn get_field_raw(&self, path: &str) -> Option<Variable> {
self.node.config.dot_get(path).ok().flatten()
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionHandle
.with_function(self.js_function.clone());
let result = sub_tree
.evaluate(&request.input)
.evaluate(request.input.clone())
.await
.map_err(|e| e.source)?;
+10 -13
View File
@@ -1,12 +1,10 @@
use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
use crate::model::DecisionNodeKind;
use std::collections::HashMap;
use ahash::{HashMap, HashMapExt};
use crate::util::json_map::FlatJsonMap;
use anyhow::{anyhow, Context};
use serde::Serialize;
use serde_json::Value;
use zen_expression::variable::ToVariable;
use zen_expression::variable::Variable;
use zen_expression::Isolate;
pub struct ExpressionHandler<'a> {
@@ -33,10 +31,10 @@ impl<'a> ExpressionHandler<'a> {
_ => Err(anyhow!("Unexpected node type")),
}?;
let mut result = FlatJsonMap::with_capacity(content.expressions.len());
let result = Variable::empty_object();
let mut trace_map = self.trace.then(|| HashMap::<&str, ExpressionTrace>::new());
self.isolate.set_environment(&request.input);
self.isolate.set_environment(request.input.depth_clone(1));
for expression in &content.expressions {
let value = self.evaluate_expression(&expression.value)?;
if let Some(tmap) = &mut trace_map {
@@ -48,21 +46,20 @@ impl<'a> ExpressionHandler<'a> {
);
}
self.isolate.update_environment(|arena, env| {
self.isolate.update_environment(|env| {
let Some(environment) = env else {
return;
};
let key = format!("$.{}", &expression.key);
let _ =
environment.dot_insert(arena, key.as_str(), value.to_variable(arena).unwrap());
let _ = environment.dot_insert(key.as_str(), value.clone());
});
result.insert(&expression.key, value);
result.dot_insert(&expression.key, value);
}
let output = result.to_json().context("Conversion to JSON failed")?;
Ok(NodeResponse {
output,
output: result,
trace_data: trace_map
.map(|tm| serde_json::to_value(tm))
.transpose()
@@ -70,7 +67,7 @@ impl<'a> ExpressionHandler<'a> {
})
}
fn evaluate_expression(&mut self, expression: &'a str) -> anyhow::Result<Value> {
fn evaluate_expression(&mut self, expression: &'a str) -> anyhow::Result<Variable> {
self.isolate
.run_standard(expression)
.with_context(|| format!(r#"Failed to evaluate expression: "{expression}""#))
+6 -7
View File
@@ -1,15 +1,14 @@
use std::sync::Arc;
use rquickjs::promise::MaybePromise;
use rquickjs::{async_with, AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::handler::function::error::{FunctionError, FunctionResult, ResultExt};
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
use crate::handler::function::module::console::{Console, Log};
use crate::handler::function::module::ModuleLoader;
use crate::handler::function::serde::JsValue;
use rquickjs::promise::MaybePromise;
use rquickjs::{async_with, AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module};
use serde::{Deserialize, Serialize};
use zen_expression::variable::Variable;
pub struct FunctionConfig {
pub(crate) listeners: Option<Vec<Box<dyn RuntimeListener>>>,
@@ -49,7 +48,7 @@ impl Function {
};
}
return Ok(());
Ok(())
}
async fn dispatch_event(&self, event: RuntimeEvent) -> FunctionResult {
@@ -121,5 +120,5 @@ impl Function {
#[derive(Serialize, Deserialize)]
pub struct HandlerResponse {
pub logs: Vec<Log>,
pub data: Value,
pub data: Variable,
}
+17 -17
View File
@@ -1,3 +1,6 @@
use crate::handler::function::error::ResultExt;
use crate::handler::function::module::export_default;
use crate::handler::function::serde::JsValue;
use reqwest::header::{HeaderMap, HeaderName};
use reqwest::Method;
use rquickjs::module::{Declarations, Exports, ModuleDef};
@@ -5,10 +8,7 @@ use rquickjs::prelude::{Async, Func, Opt};
use rquickjs::{CatchResultExt, Ctx, FromJs, IntoAtom, IntoJs, Object, Value};
use std::str::FromStr;
use std::sync::OnceLock;
use crate::handler::function::error::ResultExt;
use crate::handler::function::module::export_default;
use crate::handler::function::serde::JsValue;
use zen_expression::variable::Variable;
pub(crate) struct HttpResponse<'js> {
data: Value<'js>,
@@ -62,7 +62,7 @@ async fn execute_http<'js>(
)?;
}
let data: serde_json::Value = response.json().await.or_throw(&ctx)?;
let data: Variable = response.json().await.or_throw(&ctx)?;
Ok(HttpResponse {
data: JsValue(data).into_js(&ctx)?,
@@ -91,12 +91,12 @@ impl<'js> FromJs<'js> for HttpConfig {
let value = JsValue::from_js(ctx, value)?;
let str_value = match value.0 {
serde_json::Value::Null => None,
serde_json::Value::Bool(b) => Some(b.to_string()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::String(s) => Some(s),
serde_json::Value::Array(_) => None,
serde_json::Value::Object(_) => None,
Variable::Null => None,
Variable::Bool(b) => Some(b.to_string()),
Variable::Number(n) => Some(n.to_string()),
Variable::String(s) => Some(s.to_string()),
Variable::Array(_) => None,
Variable::Object(_) => None,
};
let key_value = key.to_string()?;
@@ -121,12 +121,12 @@ impl<'js> FromJs<'js> for HttpConfig {
let value = JsValue::from_js(ctx, value)?;
let str_value = match value.0 {
serde_json::Value::Null => None,
serde_json::Value::Bool(b) => Some(b.to_string()),
serde_json::Value::Number(n) => Some(n.to_string()),
serde_json::Value::String(s) => Some(s),
serde_json::Value::Array(_) => None,
serde_json::Value::Object(_) => None,
Variable::Null => None,
Variable::Bool(b) => Some(b.to_string()),
Variable::Number(n) => Some(n.to_string()),
Variable::String(s) => Some(s.to_string()),
Variable::Array(_) => None,
Variable::Object(_) => None,
};
let key = key.to_string()?;
+10 -12
View File
@@ -2,10 +2,6 @@ use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use rquickjs::module::{Declarations, Exports, ModuleDef};
use rquickjs::prelude::{Async, Func, Opt};
use rquickjs::{CatchResultExt, Ctx, Function, Object};
use crate::handler::custom_node_adapter::CustomNodeAdapter;
use crate::handler::function::error::{FunctionResult, ResultExt};
use crate::handler::function::listener::{RuntimeEvent, RuntimeListener};
@@ -13,6 +9,9 @@ use crate::handler::function::module::export_default;
use crate::handler::function::serde::JsValue;
use crate::handler::graph::{DecisionGraph, DecisionGraphConfig};
use crate::loader::DecisionLoader;
use rquickjs::module::{Declarations, Exports, ModuleDef};
use rquickjs::prelude::{Async, Func, Opt};
use rquickjs::{CatchResultExt, Ctx, Function, Object};
pub(crate) struct ZenListener<Loader, Adapter> {
pub loader: Arc<Loader>,
@@ -68,11 +67,10 @@ impl<Loader: DecisionLoader + 'static, Adapter: CustomNodeAdapter + 'static> Run
})
.or_throw(&ctx)?;
let response =
sub_tree.evaluate(&context.0).await.or_throw(&ctx)?;
return rquickjs::Result::Ok(JsValue(
serde_json::to_value(response).or_throw(&ctx)?,
));
let response = sub_tree.evaluate(context.0).await.or_throw(&ctx)?;
let k = serde_json::to_value(response).or_throw(&ctx)?.into();
return rquickjs::Result::Ok(JsValue(k));
}
},
)),
@@ -89,7 +87,7 @@ fn evaluate_expression<'js>(
expression: String,
context: JsValue,
) -> rquickjs::Result<JsValue> {
let s = zen_expression::evaluate_expression(expression.as_str(), &context.0).or_throw(&ctx)?;
let s = zen_expression::evaluate_expression(expression.as_str(), context.0).or_throw(&ctx)?;
Ok(JsValue(s))
}
@@ -99,8 +97,8 @@ fn evaluate_unary_expression<'js>(
expression: String,
context: JsValue,
) -> rquickjs::Result<bool> {
let s = zen_expression::evaluate_unary_expression(expression.as_str(), &context.0)
.or_throw(&ctx)?;
let s =
zen_expression::evaluate_unary_expression(expression.as_str(), context.0).or_throw(&ctx)?;
Ok(s)
}
+59 -52
View File
@@ -1,84 +1,87 @@
use crate::handler::function::error::ResultExt;
use itertools::Itertools;
use ahash::{HashMap, HashMapExt};
use rquickjs::{Ctx, FromJs, IntoAtom, IntoJs, Type, Value as QValue};
use serde_json::{json, Map, Number, Value};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde_json::json;
use std::rc::Rc;
use zen_expression::variable::Variable;
#[derive(Debug)]
pub(crate) struct JsValue(pub(crate) Value);
pub(crate) struct JsValue(pub(crate) Variable);
impl<'js> FromJs<'js> for JsValue {
fn from_js(ctx: &Ctx<'js>, v: QValue<'js>) -> rquickjs::Result<Self> {
let computed_value = match v.type_of() {
Type::Uninitialized | Type::Undefined | Type::Null => Value::Null,
Type::Bool => Value::Bool(v.as_bool().or_throw_msg(ctx, "failed to convert to bool")?),
Type::Int => Value::Number(Number::from(
Type::Uninitialized | Type::Undefined | Type::Null => Variable::Null,
Type::Bool => {
Variable::Bool(v.as_bool().or_throw_msg(ctx, "failed to convert to bool")?)
}
Type::Int => Variable::Number(Decimal::from(
v.as_int().or_throw_msg(ctx, "failed to convert to int")?,
)),
Type::BigInt => Value::Number(Number::from(
v.as_big_int()
Type::BigInt => Variable::Number(Decimal::from(
v.into_big_int()
.map(|b| b.clone().to_i64().ok())
.flatten()
.or_throw_msg(ctx, "failed to convert to number")?,
)),
Type::Float => Value::Number(
v.as_float()
.map(|n| Number::from_f64(n))
.flatten()
.or_throw_msg(ctx, "failed to convert to number")?,
Type::Float => Variable::Number(
Decimal::try_from(
v.as_float()
.or_throw_msg(ctx, "failed to convert to number")?,
)
.or_throw_msg(ctx, "failed to convert to number")?,
),
Type::String => Value::String(
v.as_string()
Type::String => Variable::String(
v.into_string()
.map(|s| s.to_string().ok())
.flatten()
.map(|s| Rc::from(s.as_str()))
.or_throw_msg(ctx, "failed to convert to string")?,
),
Type::Array => {
let arr = v
.as_array()
.into_array()
.or_throw_msg(ctx, "failed to convert to array")?;
let js_arr: Vec<Value> = arr
.iter::<QValue>()
.map_ok(|n| JsValue::from_js(ctx, n.clone()).map(|js_val| js_val.0))
.flatten()
.try_collect()
.or_throw(ctx)?;
let mut js_arr = Vec::with_capacity(arr.len());
for x in arr.into_iter() {
js_arr.push(JsValue::from_js(ctx, x.or_throw(ctx)?).or_throw(ctx)?.0)
}
Value::Array(js_arr)
Variable::from_array(js_arr)
}
Type::Object => {
let object = v
.as_object()
.into_object()
.or_throw_msg(ctx, "failed to convert to object")?;
let js_object: Map<String, Value> = object
.props::<String, QValue>()
.map_ok(|(key, value)| {
JsValue::from_js(ctx, value.clone()).map(|js_val| (key, js_val.0))
})
.flatten()
.try_collect()
.or_throw(ctx)?;
let mut js_object = HashMap::with_capacity(object.len());
for p in object.props() {
let (k, v) = p.or_throw(ctx)?;
js_object.insert(k, JsValue::from_js(ctx, v).or_throw(ctx)?.0);
}
Value::Object(js_object)
Variable::from_object(js_object)
}
Type::Exception => {
let exception = v
.as_exception()
.into_exception()
.or_throw_msg(ctx, "failed to convert to exception")?;
let message = exception.message().unwrap_or_default();
let description = exception.to_string();
json!({ "message": message, "description": description })
json!({ "message": message, "description": description }).into()
}
Type::Function => json!("[Function]"),
Type::Module => json!("[Module]"),
Type::Constructor => json!("[Constructor]"),
Type::Symbol => json!("[Symbol]"),
Type::Unknown => json!("[Unknown]"),
Type::Function => json!("[Function]").into(),
Type::Module => json!("[Module]").into(),
Type::Constructor => json!("[Constructor]").into(),
Type::Symbol => json!("[Symbol]").into(),
Type::Unknown => json!("[Unknown]").into(),
Type::Promise => {
let promise = v.as_promise().or_throw(ctx)?;
let promise = v.into_promise().or_throw(ctx)?;
let val: JsValue = promise.finish()?;
val.0
}
@@ -91,26 +94,30 @@ impl<'js> FromJs<'js> for JsValue {
impl<'js> IntoJs<'js> for JsValue {
fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<QValue<'js>> {
let res = match self.0 {
Value::Null => QValue::new_null(ctx.clone()),
Value::Bool(b) => QValue::new_bool(ctx.clone(), b),
Value::Number(n) => QValue::new_number(
Variable::Null => QValue::new_null(ctx.clone()),
Variable::Bool(b) => QValue::new_bool(ctx.clone(), b),
Variable::Number(n) => QValue::new_number(
ctx.clone(),
n.as_f64()
n.to_f64()
.or_throw_msg(ctx, "failed to convert float to number")?,
),
Value::String(str) => str.into_js(ctx)?,
Value::Array(arr) => {
Variable::String(str) => str.into_js(ctx)?,
Variable::Array(a) => {
let qarr = rquickjs::Array::new(ctx.clone())?;
for (idx, item) in arr.into_iter().enumerate() {
qarr.set(idx, JsValue(item))?;
let arr = a.borrow();
for (idx, item) in arr.iter().enumerate() {
qarr.set(idx, JsValue(item.clone()))?;
}
qarr.into_value()
}
Value::Object(map) => {
Variable::Object(o) => {
let qmap = rquickjs::Object::new(ctx.clone())?;
for (key, value) in map.into_iter() {
qmap.set(key.into_atom(ctx)?, JsValue(value))?;
let obj = o.borrow();
for (key, value) in obj.iter() {
qmap.set(key.into_atom(ctx)?, JsValue(value.clone()))?;
}
qmap.into_value()
@@ -1,15 +1,16 @@
use std::fmt::Debug;
use crate::handler::function::serde::JsValue;
use anyhow::Context as _;
use rquickjs::{Context, Ctx, Error as QError, FromJs, Module, Runtime};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::Debug;
use std::rc::Rc;
use zen_expression::variable::Variable;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EvaluateResponse {
pub output: Value,
pub output: Variable,
pub log: Vec<Value>,
}
@@ -58,7 +59,7 @@ impl Script {
fn map_js_error(ctx: &Ctx, e: QError) -> anyhow::Error {
let error = JsValue::from_js(&ctx, ctx.catch())
.map(|v| v.0)
.unwrap_or(Value::String(e.to_string()));
.unwrap_or(Variable::String(Rc::from(e.to_string().as_str())));
anyhow::Error::msg(error.to_string())
}
+42 -46
View File
@@ -1,15 +1,3 @@
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use anyhow::anyhow;
use petgraph::algo::is_cyclic_directed;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::Value;
use thiserror::Error;
use crate::handler::custom_node_adapter::{CustomNodeAdapter, CustomNodeRequest};
use crate::handler::decision::DecisionHandler;
use crate::handler::expression::ExpressionHandler;
@@ -25,6 +13,17 @@ use crate::handler::traversal::{GraphWalker, StableDiDecisionGraph};
use crate::loader::DecisionLoader;
use crate::model::{DecisionContent, DecisionNodeKind, FunctionNodeContent};
use crate::{EvaluationError, NodeError};
use ahash::{HashMap, HashMapExt};
use anyhow::anyhow;
use petgraph::algo::is_cyclic_directed;
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
use serde_json::Value;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use thiserror::Error;
use zen_expression::variable::Variable;
pub struct DecisionGraph<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> {
graph: StableDiDecisionGraph<'a>,
@@ -139,7 +138,10 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
.count()
}
pub async fn evaluate(&mut self, context: &Value) -> Result<DecisionGraphResponse, NodeError> {
pub async fn evaluate(
&mut self,
context: Variable,
) -> Result<DecisionGraphResponse, NodeError> {
let root_start = Instant::now();
self.validate().map_err(|e| NodeError {
@@ -184,8 +186,8 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
DecisionNodeKind::InputNode => {
walker.set_node_data(nid, context.clone());
trace!({
input: Value::Null,
output: Value::Null,
input: Variable::Null,
output: Variable::Null,
name: node.name.clone(),
id: node.id.clone(),
performance: None,
@@ -194,8 +196,8 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
}
DecisionNodeKind::OutputNode => {
trace!({
input: Value::Null,
output: Value::Null,
input: Variable::Null,
output: Variable::Null,
name: node.name.clone(),
id: node.id.clone(),
performance: None,
@@ -219,12 +221,12 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
node_id: node.id.clone(),
})?;
let mut node_request = NodeRequest {
let node_request = NodeRequest {
node,
iteration: self.iteration,
input: walker.incoming_node_data(&self.graph, nid, true),
};
let mut res = match content {
let res = match content {
FunctionNodeContent::Version2(_) => FunctionHandler::new(
function,
self.trace,
@@ -253,8 +255,8 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
}
};
trim_nodes(&mut node_request.input);
trim_nodes(&mut res.output);
node_request.input.dot_remove("$nodes");
res.output.dot_remove("$nodes");
trace!({
input: node_request.input,
@@ -267,13 +269,13 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
walker.set_node_data(nid, res.output);
}
DecisionNodeKind::DecisionNode { .. } => {
let mut node_request = NodeRequest {
let node_request = NodeRequest {
node,
iteration: self.iteration,
input: walker.incoming_node_data(&self.graph, nid, true),
};
let mut res = DecisionHandler::new(
let res = DecisionHandler::new(
self.trace,
self.max_depth,
self.loader.clone(),
@@ -287,8 +289,8 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
node_id: node.id.to_string(),
})?;
trim_nodes(&mut node_request.input);
trim_nodes(&mut res.output);
node_request.input.dot_remove("$nodes");
res.output.dot_remove("$nodes");
trace!({
input: node_request.input,
@@ -301,13 +303,13 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
walker.set_node_data(nid, res.output);
}
DecisionNodeKind::DecisionTableNode { .. } => {
let mut node_request = NodeRequest {
let node_request = NodeRequest {
node,
iteration: self.iteration,
input: walker.incoming_node_data(&self.graph, nid, true),
};
let mut res = DecisionTableHandler::new(self.trace)
let res = DecisionTableHandler::new(self.trace)
.handle(&node_request)
.await
.map_err(|e| NodeError {
@@ -315,8 +317,8 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
source: e.into(),
})?;
trim_nodes(&mut node_request.input);
trim_nodes(&mut res.output);
node_request.input.dot_remove("$nodes");
res.output.dot_remove("$nodes");
trace!({
input: node_request.input,
@@ -329,13 +331,13 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
walker.set_node_data(nid, res.output);
}
DecisionNodeKind::ExpressionNode { .. } => {
let mut node_request = NodeRequest {
let node_request = NodeRequest {
node,
iteration: self.iteration,
input: walker.incoming_node_data(&self.graph, nid, true),
};
let mut res = ExpressionHandler::new(self.trace)
let res = ExpressionHandler::new(self.trace)
.handle(&node_request)
.await
.map_err(|e| NodeError {
@@ -343,8 +345,8 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
source: e.into(),
})?;
trim_nodes(&mut node_request.input);
trim_nodes(&mut res.output);
node_request.input.dot_remove("$nodes");
res.output.dot_remove("$nodes");
trace!({
input: node_request.input,
@@ -357,13 +359,13 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
walker.set_node_data(nid, res.output);
}
DecisionNodeKind::CustomNode { .. } => {
let mut node_request = NodeRequest {
let node_request = NodeRequest {
node,
iteration: self.iteration,
input: walker.incoming_node_data(&self.graph, nid, true),
};
let mut res = self
let res = self
.adapter
.handle(CustomNodeRequest::try_from(&node_request).unwrap())
.await
@@ -372,8 +374,8 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
source: e.into(),
})?;
trim_nodes(&mut node_request.input);
trim_nodes(&mut res.output);
node_request.input.dot_remove("$nodes");
res.output.dot_remove("$nodes");
trace!({
input: node_request.input,
@@ -395,12 +397,6 @@ impl<'a, L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGr
}
}
fn trim_nodes(val: &mut Value) {
if let Some(obj) = val.as_object_mut() {
obj.remove("$nodes");
}
}
#[derive(Debug, Error)]
pub enum DecisionGraphValidationError {
#[error("Invalid input node count: {0}")]
@@ -449,7 +445,7 @@ impl Serialize for DecisionGraphValidationError {
#[serde(rename_all = "camelCase")]
pub struct DecisionGraphResponse {
pub performance: String,
pub result: Value,
pub result: Variable,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace: Option<HashMap<String, DecisionGraphTrace>>,
}
@@ -457,8 +453,8 @@ pub struct DecisionGraphResponse {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DecisionGraphTrace {
pub input: Value,
pub output: Value,
pub input: Variable,
pub output: Variable,
pub name: String,
pub id: String,
pub performance: Option<String>,
+3 -2
View File
@@ -3,17 +3,18 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt::{Display, Formatter};
use thiserror::Error;
use zen_expression::variable::Variable;
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeResponse {
pub output: Value,
pub output: Variable,
pub trace_data: Option<Value>,
}
#[derive(Debug, Serialize)]
pub struct NodeRequest<'a> {
pub input: Value,
pub input: Variable,
pub iteration: u8,
pub node: &'a DecisionNode,
}
+13 -12
View File
@@ -1,11 +1,10 @@
pub mod zen;
use crate::util::json_map::{FlatJsonMap, JsonMapError};
use serde_json::Value;
use zen_expression::variable::Variable;
#[derive(Debug, Clone)]
pub(crate) enum RowOutputKind {
Value(Value),
Variable(Variable),
}
#[derive(Debug, Default)]
@@ -20,15 +19,17 @@ impl RowOutput {
self.output.push((key.into(), value))
}
pub async fn to_json(&self) -> Result<Value, JsonMapError> {
let map: Vec<(String, Value)> = self
.output
.iter()
.map(|(key, kind)| match kind {
RowOutputKind::Value(value) => (key.clone(), value.clone()),
})
.collect();
pub async fn to_json(&self) -> Variable {
let object = Variable::empty_object();
FlatJsonMap::from(map).to_json()
for (key, kind) in &self.output {
match kind {
RowOutputKind::Variable(variable) => {
object.dot_insert(key.as_str(), variable.clone());
}
}
}
object
}
}
+24 -26
View File
@@ -1,18 +1,17 @@
use ahash::HashMap;
use anyhow::{anyhow, Context};
use std::collections::HashMap;
use serde::Serialize;
use serde_json::Value;
use zen_expression::Isolate;
use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
use crate::handler::table::{RowOutput, RowOutputKind};
use crate::model::{DecisionNodeKind, DecisionTableContent, DecisionTableHitPolicy};
use serde::Serialize;
use zen_expression::variable::Variable;
use zen_expression::Isolate;
#[derive(Debug, Serialize)]
struct RowResult {
rule: Option<HashMap<String, String>>,
reference_map: Option<HashMap<String, Value>>,
reference_map: Option<HashMap<String, Variable>>,
index: usize,
#[serde(skip)]
output: RowOutput,
@@ -38,7 +37,7 @@ impl<'a> DecisionTableHandler<'a> {
_ => Err(anyhow!("Unexpected node type")),
}?;
self.isolate.set_environment(&request.input);
self.isolate.set_environment(request.input.depth_clone(1));
match &content.hit_policy {
DecisionTableHitPolicy::First => self.handle_first_hit(&content).await,
@@ -50,7 +49,7 @@ impl<'a> DecisionTableHandler<'a> {
for i in 0..content.rules.len() {
if let Some(result) = self.evaluate_row(&content, i) {
return Ok(NodeResponse {
output: result.output.to_json().await?,
output: result.output.to_json().await,
trace_data: self
.trace
.then(|| {
@@ -62,7 +61,7 @@ impl<'a> DecisionTableHandler<'a> {
}
Ok(NodeResponse {
output: Value::Null,
output: Variable::Null,
trace_data: None,
})
}
@@ -77,11 +76,11 @@ impl<'a> DecisionTableHandler<'a> {
let mut outputs = Vec::with_capacity(results.len());
for res in &results {
outputs.push(res.output.to_json().await?);
outputs.push(res.output.to_json().await);
}
Ok(NodeResponse {
output: serde_json::to_value(&outputs).context("Failed to parse table row output")?,
output: Variable::from_array(outputs),
trace_data: self
.trace
.then(|| serde_json::to_value(&results).context("Failed to parse trace data"))
@@ -101,20 +100,19 @@ impl<'a> DecisionTableHandler<'a> {
continue;
}
let Some(input_field) = &input.field else {
let result = self.isolate.run_standard(rule_value.as_str()).ok()?;
let is_ok = result.as_bool().unwrap_or(false);
if !is_ok {
return None;
match &input.field {
None => {
let result = self.isolate.run_standard(rule_value.as_str()).ok()?;
if !result.as_bool().unwrap_or(false) {
return None;
}
}
Some(field) => {
self.isolate.set_reference(field.as_str()).ok()?;
if !self.isolate.run_unary(rule_value.as_str()).ok()? {
return None;
}
}
continue;
};
self.isolate.set_reference(input_field.as_str()).ok()?;
let is_ok = self.isolate.run_unary(rule_value.as_str()).ok()?;
if !is_ok {
return None;
}
}
@@ -126,7 +124,7 @@ impl<'a> DecisionTableHandler<'a> {
}
let res = self.isolate.run_standard(rule_value).ok()?;
outputs.push(&output.field, RowOutputKind::Value(res));
outputs.push(&output.field, RowOutputKind::Variable(res));
}
if !self.trace {
@@ -144,7 +142,7 @@ impl<'a> DecisionTableHandler<'a> {
};
let mut expressions: HashMap<String, String> = Default::default();
let mut reference_map: HashMap<String, Value> = Default::default();
let mut reference_map: HashMap<String, Variable> = Default::default();
expressions.insert("_id".to_string(), rule_id.clone());
if let Some(description) = rule.get("_description") {
+48 -52
View File
@@ -1,11 +1,12 @@
use ahash::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 std::collections::HashMap;
use serde_json::json;
use std::rc::Rc;
use std::sync::atomic::Ordering;
use std::time::Instant;
@@ -14,6 +15,7 @@ use crate::model::{
DecisionEdge, DecisionNode, DecisionNodeKind, SwitchStatement, SwitchStatementHitPolicy,
};
use crate::DecisionGraphTrace;
use zen_expression::variable::Variable;
use zen_expression::Isolate;
pub(crate) type StableDiDecisionGraph<'a> = StableDiGraph<&'a DecisionNode, &'a DecisionEdge>;
@@ -21,7 +23,7 @@ pub(crate) type StableDiDecisionGraph<'a> = StableDiGraph<&'a DecisionNode, &'a
pub(crate) struct GraphWalker {
ordered: FixedBitSet,
to_visit: Vec<NodeIndex>,
node_data: HashMap<NodeIndex, Value>,
node_data: HashMap<NodeIndex, Variable>,
iter: usize,
visited_switch_nodes: Vec<NodeIndex>,
@@ -66,12 +68,12 @@ impl GraphWalker {
self.iter += 1;
}
pub fn get_node_data(&self, node_id: NodeIndex) -> Option<&Value> {
self.node_data.get(&node_id)
pub fn get_node_data(&self, node_id: NodeIndex) -> Option<Variable> {
self.node_data.get(&node_id).cloned()
}
pub fn get_all_node_data(&self, g: &StableDiDecisionGraph) -> Value {
let node_values: Map<String, Value> = self
pub fn get_all_node_data(&self, g: &StableDiDecisionGraph) -> Variable {
let node_values = self
.node_data
.iter()
.map(|(idx, value)| {
@@ -80,10 +82,10 @@ impl GraphWalker {
})
.collect();
Value::Object(node_values)
Variable::from_object(node_values)
}
pub fn set_node_data(&mut self, node_id: NodeIndex, value: Value) {
pub fn set_node_data(&mut self, node_id: NodeIndex, value: Variable) {
self.node_data.insert(node_id, value);
}
@@ -92,11 +94,13 @@ impl GraphWalker {
g: &StableDiDecisionGraph,
node_id: NodeIndex,
with_nodes: bool,
) -> Value {
let mut value = self.merge_node_data(g.neighbors_directed(node_id, Incoming));
) -> Variable {
let value = self
.merge_node_data(g.neighbors_directed(node_id, Incoming))
.depth_clone(1);
if self.nodes_in_context {
if let Some(object) = with_nodes.then_some(value.as_object_mut()).flatten() {
if let Some(object_ref) = with_nodes.then_some(value.as_object()).flatten() {
let mut object = object_ref.borrow_mut();
object.insert("$nodes".to_string(), self.get_all_node_data(g));
}
}
@@ -104,16 +108,14 @@ impl GraphWalker {
value
}
pub fn merge_node_data<I>(&self, iter: I) -> Value
pub fn merge_node_data<I>(&self, iter: I) -> Variable
where
I: Iterator<Item = NodeIndex>,
{
let default_map = Value::Object(Map::new());
iter.fold(Value::Object(Map::new()), |mut prev, curr| {
let default_map = Variable::empty_object();
iter.fold(Variable::empty_object(), |mut prev, curr| {
let data = self.node_data.get(&curr).unwrap_or(&default_map);
merge_json(&mut prev, data, true);
prev
prev.merge(data)
})
}
@@ -137,11 +139,12 @@ impl GraphWalker {
if let DecisionNodeKind::SwitchNode { content } = &decision_node.kind {
if !self.visited_switch_nodes.contains(&nid) {
let mut input_data = self.incoming_node_data(g, nid, true);
let input_context = json!({ "$": &input_data });
merge_json(&mut input_data, &input_context, true);
let input_data = self.incoming_node_data(g, nid, true);
let mut isolate = Isolate::with_environment(&input_data);
let env = input_data.depth_clone(1);
env.dot_insert("$", input_data.depth_clone(1));
let mut isolate = Isolate::with_environment(env);
let mut statement_iter = content.statements.iter();
let valid_statements: Vec<&SwitchStatement> = match content.hit_policy {
@@ -154,18 +157,33 @@ impl GraphWalker {
.collect(),
};
let valid_statements_trace: Value = valid_statements
.iter()
.map(|&statement| json!({ "id": &statement.id }))
.collect();
let valid_statements_trace = Variable::from_array(
valid_statements
.iter()
.map(|&statement| {
let v = Variable::empty_object();
v.dot_insert(
"id",
Variable::String(Rc::from(statement.id.as_str())),
);
v
})
.collect(),
);
input_data.dot_remove("$nodes");
if let Some(on_trace) = &mut on_trace {
on_trace(DecisionGraphTrace {
id: decision_node.id.clone(),
name: decision_node.name.clone(),
input: input_data.clone(),
output: input_data.clone(),
input: input_data.shallow_clone(),
output: input_data.shallow_clone(),
performance: Some(format!("{:?}", start.elapsed())),
trace_data: Some(json!({ "statements": valid_statements_trace })),
trace_data: Some(
json!({ "statements": valid_statements_trace }).into(),
),
});
}
@@ -265,25 +283,3 @@ fn remove_edge_recursive(g: &mut StableDiDecisionGraph, edge_id: EdgeIndex) {
}
}
}
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();
}
}
+2 -3
View File
@@ -122,13 +122,11 @@
#![deny(clippy::unwrap_used)]
#![allow(clippy::module_inception)]
mod config;
mod decision;
mod engine;
mod error;
pub mod handler;
mod util;
mod config;
pub mod loader;
#[path = "model/mod.rs"]
pub mod model;
@@ -141,3 +139,4 @@ pub use handler::graph::DecisionGraphResponse;
pub use handler::graph::DecisionGraphTrace;
pub use handler::graph::DecisionGraphValidationError;
pub use handler::node::NodeError;
pub use zen_expression::Variable;
+1 -2
View File
@@ -1,7 +1,6 @@
use std::collections::HashMap;
use ahash::{HashMap, HashMapExt};
use std::future::Future;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::loader::{DecisionLoader, LoaderResponse};
+3 -4
View File
@@ -1,9 +1,8 @@
use std::collections::HashMap;
use std::future::Future;
use std::sync::{Arc, RwLock};
use crate::loader::{DecisionLoader, LoaderError, LoaderResponse};
use crate::model::DecisionContent;
use ahash::HashMap;
use std::future::Future;
use std::sync::{Arc, RwLock};
/// Loads decisions from in-memory hashmap
#[derive(Debug, Default)]
+1 -2
View File
@@ -1,5 +1,4 @@
use std::collections::HashMap;
use ahash::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
-319
View File
@@ -1,319 +0,0 @@
#![allow(dead_code)]
use serde::{Serialize, Serializer};
use serde_json::{Map, Value};
use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
use thiserror::Error;
#[derive(Clone, Default, Debug)]
pub(crate) struct FlatJsonMap {
inner: Vec<(String, Value)>,
}
impl From<Vec<(String, Value)>> for FlatJsonMap {
fn from(value: Vec<(String, Value)>) -> Self {
Self { inner: value }
}
}
impl FlatJsonMap {
pub fn new() -> Self {
Default::default()
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
inner: Vec::with_capacity(capacity),
}
}
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
pub fn insert<T: Into<String>>(&mut self, key: T, value: Value) {
self.inner.push((key.into(), value))
}
pub fn remove<T: AsRef<str>>(&mut self, key: T) {
self.inner.retain(|(k, _)| k == key.as_ref())
}
pub fn reserve(&mut self, additional: usize) {
self.inner.reserve(additional)
}
pub fn to_json(&self) -> Result<Value, JsonMapError> {
let map = self
.inner
.iter()
.enumerate()
.map(|(index, (key, value))| flatten_value(key, value.clone(), index as u32))
.collect::<Vec<BTreeMap<JsonMapKey, Value>>>();
let mut result = BTreeMap::<JsonMapKey, Value>::new();
for inner_map in map {
for (key, value) in inner_map {
match value {
// Unexpected, as we've filtered out all objects in prior step
Value::Object(_) => return Err(JsonMapError::FailedToParse),
Value::Array(arr) => {
let maybe_exist = result.get_mut(&key).map(|a| a.as_array_mut()).flatten();
if let Some(exist) = maybe_exist {
exist.extend_from_slice(&arr);
} else {
result.insert(key, Value::Array(arr));
}
}
_ => {
result.insert(key, value);
}
}
}
}
let mut root = Map::new();
for (key, mut value) in result {
let mut node = &mut root;
let mut segments = key.str.split('.');
let last_segment = segments.next_back().ok_or(JsonMapError::SerdeError)?;
for segment in segments {
node = node
.entry(segment)
.and_modify(|val| {
if !matches!(val, Value::Object(_)) {
let _ = std::mem::replace(val, Value::Object(Map::new()));
}
})
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut()
.ok_or(JsonMapError::SerdeError)?;
}
let entry = node.get_mut(last_segment);
if let Some(mut entry_val) = entry {
match (&mut entry_val, &mut value) {
(Value::Array(arr1), Value::Array(arr2)) => arr1.extend_from_slice(&arr2),
_ => {
let _ = std::mem::replace(entry_val, value);
}
}
} else {
node.insert(last_segment.to_string(), value);
}
}
Ok(Value::Object(root))
}
}
#[derive(Debug, Error)]
pub(crate) enum JsonMapError {
#[error("Failed to parse")]
FailedToParse,
#[error("Unexpected serde error has occurred while deserializing rows")]
SerdeError,
}
#[derive(Debug, Eq, PartialEq)]
struct JsonMapKey {
str: String,
sep_occurrences: u32,
bucket: u32,
}
impl JsonMapKey {
fn new<T: Into<String>>(value: T, bucket: u32) -> Self {
let str = value.into();
let sep_occurrences = str.chars().fold(0, |c, k| if k == '.' { c + 1 } else { c });
Self {
str,
sep_occurrences,
bucket,
}
}
}
impl PartialOrd for JsonMapKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for JsonMapKey {
fn cmp(&self, other: &Self) -> Ordering {
if self.bucket != other.bucket {
return self.bucket.cmp(&other.bucket);
}
if self.sep_occurrences == other.sep_occurrences {
self.str.cmp(&other.str)
} else {
self.sep_occurrences.cmp(&other.sep_occurrences)
}
}
}
impl Serialize for JsonMapKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.str)
}
}
impl Display for JsonMapKey {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.str)
}
}
fn flatten_value(prefix_key: &str, value: Value, bucket: u32) -> BTreeMap<JsonMapKey, Value> {
let mut map = BTreeMap::<JsonMapKey, Value>::new();
match value {
Value::Object(obj) => {
for (key, value) in obj {
if let Value::Object(inner_obj) = value {
let inner_map = flatten_value(&key, Value::Object(inner_obj), bucket);
for (inner_key, inner_value) in inner_map {
map.insert(
JsonMapKey::new(format!("{prefix_key}.{inner_key}"), bucket),
inner_value,
);
}
} else {
map.insert(
JsonMapKey::new(format!("{prefix_key}.{key}"), bucket),
value,
);
}
}
}
_ => {
map.insert(JsonMapKey::new(prefix_key, bucket), value);
}
}
map
}
#[cfg(test)]
mod tests {
use crate::util::json_map::{FlatJsonMap, JsonMapKey};
use serde_json::json;
use std::cmp::Ordering;
macro_rules! key {
($str: expr) => {
JsonMapKey::new($str, 0)
};
($str: expr, $order: expr) => {
JsonMapKey::new($str, $order)
};
}
#[test]
fn test_order() {
assert_eq!(key!("a").cmp(&key!("b")), Ordering::Less);
assert_eq!(key!("a.b").cmp(&key!("b")), Ordering::Greater);
assert_eq!(key!("a").cmp(&key!("b.a")), Ordering::Less);
assert_eq!(key!("a.b.c").cmp(&key!("a.b.c")), Ordering::Equal);
assert_eq!(key!("a.b.c").cmp(&key!("a.b.c", 1)), Ordering::Less);
}
#[test]
fn flatmap_insert_order() {
let mut o = FlatJsonMap::default();
o.insert("a", json!("abc"));
o.insert("a.b", json!("abc"));
o.insert("a.b.c", json!("abc"));
assert_eq!(
o.to_json().unwrap(),
json!({ "a": { "b": { "c": "abc" } } })
);
}
#[test]
fn flatmap_secondary_order() {
let mut o = FlatJsonMap::default();
o.insert("a.first.deleted", json!("deleted"));
o.insert("a.third.firstNested", json!("firstNested"));
o.insert("a.first", json!("first"));
o.insert("a.second", json!("second"));
o.insert("a.third.nested", json!("nested"));
assert_eq!(
o.to_json().unwrap(),
json!({
"a": {
"first": "first",
"second": "second",
"third": {
"firstNested": "firstNested",
"nested": "nested"
}
}
})
);
}
#[test]
fn flatmap_nested() {
let mut o = FlatJsonMap::default();
o.insert("array", json!([1, 2]));
o.insert("array", json!([3, 4]));
o.insert(
"object",
json!({
"a": "a",
"b": "b",
"nested": {
"array": [10, 11]
}
}),
);
o.insert(
"object",
json!({
"b": "c",
"nested": {
"array": [12, 13]
}
}),
);
assert_eq!(
o.to_json().unwrap(),
json!({
"array": [1, 2, 3, 4],
"object": {
"a": "a",
"b": "c",
"nested": {
"array": [10, 11, 12, 13]
}
}
})
);
}
#[test]
fn flatmap_with_capacity() {
let mut a = FlatJsonMap::new();
assert_eq!(a.capacity(), 0);
a.reserve(10);
assert_eq!(a.capacity(), 10);
let mut b = FlatJsonMap::with_capacity(10);
assert_eq!(b.capacity(), 10);
b.reserve(20);
assert_eq!(b.capacity(), 20);
}
}
-1
View File
@@ -1 +0,0 @@
pub(crate) mod json_map;
+6 -5
View File
@@ -14,9 +14,9 @@ async fn decision_from_content() {
let decision = Decision::from(table_content);
let context = json!({ "input": 5 });
let result = decision.evaluate(&context).await;
let result = decision.evaluate(context.into()).await;
assert_eq!(result.unwrap().result, json!({"output": 0}));
assert_eq!(result.unwrap().result, json!({"output": 0}).into());
}
#[tokio::test]
@@ -26,7 +26,7 @@ async fn decision_from_content_recursive() {
let decision = Decision::from(recursive_content);
let context = json!({});
let result = decision.evaluate(&context).await;
let result = decision.evaluate(context.clone().into()).await;
match result.unwrap_err().deref() {
EvaluationError::NodeError(e) => {
assert_eq!(e.node_id, "0b8dcf6b-fc04-47cb-bf82-bda764e6c09b");
@@ -36,7 +36,7 @@ async fn decision_from_content_recursive() {
}
let with_loader = decision.with_loader(Arc::new(create_fs_loader()));
let new_result = with_loader.evaluate(&context).await;
let new_result = with_loader.evaluate(context.clone().into()).await;
match new_result.unwrap_err().deref() {
EvaluationError::NodeError(e) => {
assert_eq!(e.source.to_string(), "Depth limit exceeded")
@@ -55,7 +55,7 @@ fn decision_expression_node() {
"lastName": "Doe"
});
let result = rt.block_on(decision.evaluate(&context));
let result = rt.block_on(decision.evaluate(context.into()));
assert_eq!(
result.unwrap().result,
json!({
@@ -68,6 +68,7 @@ fn decision_expression_node() {
}
}
})
.into()
)
}
+52 -37
View File
@@ -1,5 +1,6 @@
use crate::support::{create_fs_loader, load_raw_test_data, load_test_data, test_data_root};
use serde::Deserialize;
use serde_json::{json, Value};
use serde_json::json;
use std::fs;
use std::io::Read;
use std::ops::Deref;
@@ -8,10 +9,9 @@ use std::sync::Arc;
use tokio::runtime::Builder;
use zen_engine::loader::{LoaderError, MemoryLoader};
use zen_engine::model::{DecisionContent, DecisionNodeKind, FunctionNodeContent};
use zen_engine::Variable;
use zen_engine::{DecisionEngine, EvaluationError, EvaluationOptions};
use crate::support::{create_fs_loader, load_raw_test_data, load_test_data, test_data_root};
mod support;
#[tokio::test]
@@ -22,14 +22,18 @@ async fn engine_memory_loader() {
memory_loader.add("function", load_test_data("function.json"));
let engine = DecisionEngine::default().with_loader(memory_loader.clone());
let table = engine.evaluate("table", &json!({ "input": 12 })).await;
let function = engine.evaluate("function", &json!({ "input": 12 })).await;
let table = engine
.evaluate("table", json!({ "input": 12 }).into())
.await;
let function = engine
.evaluate("function", json!({ "input": 12 }).into())
.await;
memory_loader.remove("function");
let not_found = engine.evaluate("function", &json!({})).await;
let not_found = engine.evaluate("function", json!({}).into()).await;
assert_eq!(table.unwrap().result, json!({"output": 10}));
assert_eq!(function.unwrap().result, json!({"output": 24}));
assert_eq!(table.unwrap().result, json!({"output": 10}).into());
assert_eq!(function.unwrap().result, json!({"output": 24}).into());
assert_eq!(not_found.unwrap_err().to_string(), "Loader error");
}
@@ -37,14 +41,16 @@ async fn engine_memory_loader() {
#[cfg_attr(miri, ignore)]
async fn engine_filesystem_loader() {
let engine = DecisionEngine::default().with_loader(create_fs_loader().into());
let table = engine.evaluate("table.json", &json!({ "input": 12 })).await;
let function = engine
.evaluate("function.json", &json!({ "input": 12 }))
let table = engine
.evaluate("table.json", json!({ "input": 12 }).into())
.await;
let not_found = engine.evaluate("invalid_file", &json!({})).await;
let function = engine
.evaluate("function.json", json!({ "input": 12 }).into())
.await;
let not_found = engine.evaluate("invalid_file", json!({}).into()).await;
assert_eq!(table.unwrap().result, json!({"output": 10}));
assert_eq!(function.unwrap().result, json!({"output": 24}));
assert_eq!(table.unwrap().result, json!({"output": 10}).into());
assert_eq!(function.unwrap().result, json!({"output": 24}).into());
assert_eq!(not_found.unwrap_err().to_string(), "Loader error");
}
@@ -59,12 +65,16 @@ async fn engine_closure_loader() {
}
});
let table = engine.evaluate("table", &json!({ "input": 12 })).await;
let function = engine.evaluate("function", &json!({ "input": 12 })).await;
let not_found = engine.evaluate("invalid_file", &json!({})).await;
let table = engine
.evaluate("table", json!({ "input": 12 }).into())
.await;
let function = engine
.evaluate("function", json!({ "input": 12 }).into())
.await;
let not_found = engine.evaluate("invalid_file", json!({}).into()).await;
assert_eq!(table.unwrap().result, json!({"output": 10}));
assert_eq!(function.unwrap().result, json!({"output": 24}));
assert_eq!(table.unwrap().result, json!({"output": 10}).into());
assert_eq!(function.unwrap().result, json!({"output": 24}).into());
assert_eq!(not_found.unwrap_err().to_string(), "Loader error");
}
@@ -73,7 +83,7 @@ fn engine_noop_loader() {
let rt = Builder::new_current_thread().build().unwrap();
// Default engine is noop
let engine = DecisionEngine::default();
let result = rt.block_on(engine.evaluate("any.json", &json!({})));
let result = rt.block_on(engine.evaluate("any.json", json!({}).into()));
assert_eq!(result.unwrap_err().to_string(), "Loader error");
}
@@ -98,7 +108,9 @@ fn engine_create_decision() {
async fn engine_errors() {
let engine = DecisionEngine::default().with_loader(create_fs_loader().into());
let infinite_fn = engine.evaluate("infinite-function.json", &json!({})).await;
let infinite_fn = engine
.evaluate("infinite-function.json", json!({}).into())
.await;
match infinite_fn.unwrap_err().deref() {
EvaluationError::NodeError(e) => {
assert_eq!(e.node_id, "e0fd96d0-44dc-4f0e-b825-06e56b442d78");
@@ -107,7 +119,9 @@ async fn engine_errors() {
_ => assert!(false, "Wrong error type"),
}
let recursive = engine.evaluate("recursive-table1.json", &json!({})).await;
let recursive = engine
.evaluate("recursive-table1.json", json!({}).into())
.await;
match recursive.unwrap_err().deref() {
EvaluationError::NodeError(e) => {
assert_eq!(e.source.to_string(), "Depth limit exceeded")
@@ -121,10 +135,10 @@ fn engine_with_trace() {
let rt = Builder::new_current_thread().build().unwrap();
let engine = DecisionEngine::default().with_loader(create_fs_loader().into());
let table_r = rt.block_on(engine.evaluate("table.json", &json!({ "input": 12 })));
let table_r = rt.block_on(engine.evaluate("table.json", json!({ "input": 12 }).into()));
let table_opt_r = rt.block_on(engine.evaluate_with_opts(
"table.json",
&json!({ "input": 12 }),
json!({ "input": 12 }).into(),
EvaluationOptions {
trace: Some(true),
max_depth: None,
@@ -163,7 +177,7 @@ async fn engine_function_imports() {
});
let decision = DecisionEngine::default().create_decision(function_content.into());
let response = decision.evaluate(&json!({})).await.unwrap();
let response = decision.evaluate(json!({}).into()).await.unwrap();
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
@@ -174,7 +188,7 @@ async fn engine_function_imports() {
moment_valid: bool,
}
let result = serde_json::from_value::<GraphResult>(response.result).unwrap();
let result = serde_json::from_value::<GraphResult>(response.result.to_value()).unwrap();
assert!(result.bigjs_tests.iter().all(|v| *v));
assert!(result.bigjs_valid);
@@ -182,13 +196,13 @@ async fn engine_function_imports() {
assert!(result.moment_valid);
}
#[test]
fn engine_switch_node() {
let rt = Builder::new_current_thread().build().unwrap();
#[tokio::test]
async fn engine_switch_node() {
let engine = DecisionEngine::default().with_loader(create_fs_loader().into());
let switch_node_r =
rt.block_on(engine.evaluate("switch-node.json", &json!({ "color": "yellow" })));
let switch_node_r = engine
.evaluate("switch-node.json", json!({ "color": "yellow" }).into())
.await;
let table = switch_node_r.unwrap();
println!("{table:?}");
@@ -200,8 +214,8 @@ async fn engine_graph_tests() {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestCase {
input: Value,
output: Value,
input: Variable,
output: Variable,
}
#[derive(Deserialize)]
@@ -227,8 +241,9 @@ async fn engine_graph_tests() {
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;
let input = test_case.input.clone();
let result = decision.evaluate(input.clone()).await.unwrap().result;
assert_eq!(
test_case.output, result,
"Decision file: {file_name}.\nInput:\n {input:#?}"
@@ -246,7 +261,7 @@ async fn engine_function_v2() {
let function_opt_r = engine
.evaluate_with_opts(
"function-v2.json",
&json!({ "input": 12 }),
json!({ "input": 12 }).into(),
EvaluationOptions {
trace: Some(true),
max_depth: None,
@@ -262,7 +277,7 @@ async fn engine_function_v2() {
assert_eq!(
function_opt.result,
json!({ "hello": "world", "multiplied": 24 })
json!({ "hello": "world", "multiplied": 24 }).into()
)
}
}
+7 -7
View File
@@ -2,23 +2,23 @@ use std::ops::Index;
use criterion::{criterion_group, criterion_main, Bencher, Criterion};
use csv::StringRecord;
use serde_json::Value;
use zen_expression::variable::Variable;
use zen_expression::Isolate;
fn bench_unary(b: &mut Bencher, source: &'static str) {
let s: Value = serde_json::from_str(r#"{ "$": "ru" }"#).unwrap();
let s = serde_json::from_str(r#"{ "$": "ru" }"#).unwrap();
let mut isolate = Isolate::with_environment(&s);
let mut isolate = Isolate::with_environment(s);
b.iter(|| {
criterion::black_box(isolate.run_unary(source).unwrap());
})
}
fn bench_standard(b: &mut Bencher, source: &'static str) {
let s: Value = serde_json::from_str(r#"{ "$": "ru" }"#).unwrap();
let s = serde_json::from_str(r#"{ "$": "ru" }"#).unwrap();
let mut isolate = Isolate::with_environment(&s);
let mut isolate = Isolate::with_environment(s);
b.iter(|| {
criterion::black_box(isolate.run_standard(source).unwrap());
})
@@ -42,7 +42,7 @@ fn bench_csv(b: &mut Bencher, kind: BenchmarkKind, csv_data: &'static str) {
struct TestCase {
expression: String,
environment: Option<Value>,
environment: Option<Variable>,
}
let test_cases: Vec<TestCase> = rows
@@ -78,7 +78,7 @@ fn bench_csv(b: &mut Bencher, kind: BenchmarkKind, csv_data: &'static str) {
} in &test_cases
{
if let Some(env) = environment {
isolate.set_environment(env);
isolate.set_environment(env.clone());
}
match kind {
+3 -2
View File
@@ -1,5 +1,6 @@
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::rc::Rc;
use crate::compiler::error::{CompilerError, CompilerResult};
use crate::compiler::{Opcode, TypeCheckKind, TypeConversionKind};
@@ -110,7 +111,7 @@ impl<'arena, 'bytecode_ref> CompilerInner<'arena, 'bytecode_ref> {
Node::Null => Ok(self.emit(Opcode::Push(Variable::Null))),
Node::Bool(v) => Ok(self.emit(Opcode::Push(Variable::Bool(*v)))),
Node::Number(v) => Ok(self.emit(Opcode::Push(Variable::Number(*v)))),
Node::String(v) => Ok(self.emit(Opcode::Push(Variable::String(v)))),
Node::String(v) => Ok(self.emit(Opcode::Push(Variable::String(Rc::from(*v))))),
Node::Pointer => Ok(self.emit(Opcode::Pointer)),
Node::Root => Ok(self.emit(Opcode::FetchRootEnv)),
Node::Array(v) => {
@@ -147,7 +148,7 @@ impl<'arena, 'bytecode_ref> CompilerInner<'arena, 'bytecode_ref> {
self.emit(Opcode::Push(Variable::Number(Decimal::from(parts.len()))));
self.emit(Opcode::Array);
self.emit(Opcode::Push(Variable::String("")));
self.emit(Opcode::Push(Variable::String(Rc::from(""))));
Ok(self.emit(Opcode::Join))
}
Node::Slice { node, to, from } => {
+1 -1
View File
@@ -4,7 +4,7 @@ use strum_macros::Display;
/// Machine code interpreted by VM
#[derive(Debug, PartialEq, Eq, Display)]
pub enum Opcode<'a> {
Push(Variable<'a>),
Push(Variable),
Pop,
Rot,
Fetch,
+10 -7
View File
@@ -1,18 +1,21 @@
use serde_json::Value;
use crate::variable::Variable;
use crate::{Isolate, IsolateError};
/// Evaluates a standard expression
pub fn evaluate_expression(expression: &str, context: &Value) -> Result<Value, IsolateError> {
pub fn evaluate_expression(expression: &str, context: Variable) -> Result<Variable, IsolateError> {
Isolate::with_environment(context).run_standard(expression)
}
/// Evaluates a unary expression; Required: context must be an object with "$" key.
pub fn evaluate_unary_expression(expression: &str, context: &Value) -> Result<bool, IsolateError> {
let Some(context_object) = context.as_object() else {
pub fn evaluate_unary_expression(
expression: &str,
context: Variable,
) -> Result<bool, IsolateError> {
let Some(context_object_ref) = context.as_object() else {
return Err(IsolateError::MissingContextReference);
};
let context_object = context_object_ref.borrow();
if !context_object.contains_key("$") {
return Err(IsolateError::MissingContextReference);
}
@@ -28,8 +31,8 @@ mod test {
#[test]
fn bla() {
let context = json!({ "tax": { "percentage": 10 } });
let tax_amount = evaluate_expression("50 * tax.percentage / 100", &context).unwrap();
let tax_amount = evaluate_expression("50 * tax.percentage / 100", context.into()).unwrap();
assert_eq!(tax_amount, json!(5));
assert_eq!(tax_amount, json!(5).into());
}
}
+27 -46
View File
@@ -1,18 +1,15 @@
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use ahash::AHasher;
use bumpalo::Bump;
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use serde_json::Value;
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use thiserror::Error;
use crate::arena::UnsafeArena;
use crate::compiler::{Compiler, CompilerError};
use crate::lexer::{Lexer, LexerError};
use crate::parser::{Parser, ParserError};
use crate::variable::{ToVariable, Variable};
use crate::variable::Variable;
use crate::vm::{VMError, VM};
type ADefHasher = BuildHasherDefault<AHasher>;
@@ -26,13 +23,12 @@ type ADefHasher = BuildHasherDefault<AHasher>;
pub struct Isolate<'arena> {
lexer: Lexer<'arena>,
compiler: Compiler<'arena>,
vm: VM<'arena>,
vm: VM,
bump: UnsafeArena<'arena>,
reference_bump: UnsafeArena<'arena>,
environment: Option<&'arena mut Variable<'arena>>,
references: HashMap<&'arena str, &'arena Variable<'arena>, ADefHasher>,
environment: Option<Variable>,
references: HashMap<String, Variable, ADefHasher>,
}
impl<'a> Isolate<'a> {
@@ -43,67 +39,60 @@ impl<'a> Isolate<'a> {
vm: VM::new(),
bump: UnsafeArena::new(),
reference_bump: UnsafeArena::new(),
environment: None,
references: Default::default(),
}
}
pub fn with_environment(value: &Value) -> Self {
pub fn with_environment(variable: Variable) -> Self {
let mut isolate = Isolate::new();
isolate.set_environment(value);
isolate.set_environment(variable);
isolate
}
pub fn set_environment(&mut self, value: &Value) {
let bump = self.reference_bump.get();
let new_environment = value.to_variable(bump).unwrap();
self.environment.replace(bump.alloc(new_environment));
pub fn set_environment(&mut self, variable: Variable) {
self.environment.replace(variable);
}
pub fn update_environment<F>(&mut self, mut updater: F)
where
F: FnMut(&'a Bump, &mut Option<&'a mut Variable<'a>>),
F: FnMut(Option<&mut Variable>),
{
let bump = self.reference_bump.get();
updater(bump, &mut self.environment);
updater(self.environment.as_mut());
}
pub fn set_reference(&mut self, reference: &'a str) -> Result<(), IsolateError> {
let bump = self.reference_bump.get();
let reference_value = match self.references.get(reference) {
Some(value) => value,
Some(value) => value.clone(),
None => {
let result = self.run_standard(reference)?;
let value = &*bump.alloc(result.to_variable(bump).unwrap());
self.references.insert(reference, value);
value
self.references
.insert(reference.to_string(), result.clone());
result
}
};
if !matches!(&mut self.environment, Some(Variable::Object(_))) {
self.environment
.replace(bump.alloc(Variable::empty_object(bump)));
self.environment.replace(Variable::empty_object());
}
let Some(Variable::Object(environment_object)) = self.environment else {
let Some(Variable::Object(environment_object_ref)) = &self.environment else {
return Err(IsolateError::ReferenceError);
};
environment_object.insert("$", reference_value.clone_in(bump));
let mut environment_object = environment_object_ref.borrow_mut();
environment_object.insert("$".to_string(), reference_value);
Ok(())
}
pub fn get_reference(&self, reference: &str) -> Option<Value> {
let reference_variable = self.references.get(reference)?;
Some(reference_variable.to_value())
pub fn get_reference(&self, reference: &str) -> Option<Variable> {
self.references.get(reference).cloned()
}
pub fn run_standard(&mut self, source: &'a str) -> Result<Value, IsolateError> {
pub fn run_standard(&mut self, source: &'a str) -> Result<Variable, IsolateError> {
self.bump.with_mut(|b| b.reset());
let bump = self.bump.get();
@@ -128,14 +117,10 @@ impl<'a> Isolate<'a> {
let result = self
.vm
.run(
bytecode,
bump,
self.environment.as_deref().unwrap_or(&Variable::Null),
)
.run(bytecode, self.environment.clone().unwrap_or(Variable::Null))
.map_err(|source| IsolateError::VMError { source })?;
Ok(result.to_value())
Ok(result)
}
pub fn run_unary(&mut self, source: &'a str) -> Result<bool, IsolateError> {
@@ -163,11 +148,7 @@ impl<'a> Isolate<'a> {
let result = self
.vm
.run(
bytecode,
bump,
self.environment.as_deref().unwrap_or(&Variable::Null),
)
.run(bytecode, self.environment.clone().unwrap_or(Variable::Null))
.map_err(|source| IsolateError::VMError { source })?;
result.as_bool().ok_or_else(|| IsolateError::ValueCastError)
+13 -7
View File
@@ -13,13 +13,16 @@
//! # Example
//! Evaluate expression using isolate:
//! ```
//! use zen_expression::{evaluate_expression, json};
//! use zen_expression::evaluate_expression;
//! use zen_expression::variable::Variable;
//! use rust_decimal_macros::dec;
//! use serde_json::json;
//!
//! fn main() {
//! let context = json!({ "tax": { "percentage": 10 } });
//! let tax_amount = evaluate_expression("50 * tax.percentage / 100", &context).unwrap();
//! let tax_amount = evaluate_expression("50 * tax.percentage / 100", context.into()).unwrap();
//!
//! assert_eq!(tax_amount, json!(5));
//! assert_eq!(tax_amount, Variable::Number(dec!(5)));
//! }
//! ```
//!
@@ -28,16 +31,19 @@
//! will re-use allocated memory from previous evaluations, drastically improving performance.
//!
//! ```
//! use zen_expression::{Isolate, json};
//! use zen_expression::Isolate;
//! use zen_expression::variable::Variable;
//! use rust_decimal_macros::dec;
//! use serde_json::json;
//!
//! fn main() {
//! let context = json!({ "tax": { "percentage": 10 } });
//! let mut isolate = Isolate::with_environment(&context);
//! let mut isolate = Isolate::with_environment(context.into());
//!
//! // Fast 🚀
//! for _ in 0..1_000 {
//! let tax_amount = isolate.run_standard("50 * tax.percentage / 100").unwrap();
//! assert_eq!(tax_amount, json!(5));
//! assert_eq!(tax_amount, Variable::Number(dec!(5)));
//! }
//! }
//! ```
@@ -62,4 +68,4 @@ pub mod vm;
pub use function::{evaluate_expression, evaluate_unary_expression};
pub use isolate::{Isolate, IsolateError};
pub use serde_json::json;
pub use variable::Variable;
+94 -34
View File
@@ -1,44 +1,104 @@
use bumpalo::collections::Vec as BumpVec;
use bumpalo::Bump;
use rust_decimal::Decimal;
use serde_json::Value;
use crate::variable::map::BumpMap;
use crate::variable::Variable;
use crate::vm::helpers::date_time;
use crate::vm::VMError;
use chrono::NaiveDateTime;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use serde_json::{Number, Value};
use std::rc::Rc;
pub trait ToVariable<'arena> {
type Error;
fn to_variable(&self, arena: &'arena Bump) -> Result<Variable<'arena>, Self::Error>;
impl From<Value> for Variable {
fn from(value: Value) -> Self {
match value {
Value::Null => Variable::Null,
Value::Bool(b) => Variable::Bool(b),
Value::Number(n) => {
Variable::Number(Decimal::from_str_exact(n.as_str()).expect("Allowed number"))
}
Value::String(s) => Variable::String(Rc::from(s.as_str())),
Value::Array(arr) => {
Variable::from_array(arr.into_iter().map(Variable::from).collect())
}
Value::Object(obj) => Variable::from_object(
obj.into_iter()
.map(|(k, v)| (k, Variable::from(v)))
.collect(),
),
}
}
}
impl<'arena> ToVariable<'arena> for Value {
type Error = ();
fn to_variable(&self, arena: &'arena Bump) -> Result<Variable<'arena>, Self::Error> {
match self {
Value::Null => Ok(Variable::Null),
Value::Bool(v) => Ok(Variable::Bool(*v)),
Value::Number(n) => Ok(Variable::Number(
Decimal::from_str_exact(n.as_str()).map_err(|_| ())?,
)),
Value::String(s) => Ok(Variable::String(arena.alloc_str(s.as_str()))),
Value::Array(arr) => {
let mut vec = BumpVec::with_capacity_in(arr.len(), arena);
for x in arr {
vec.push(x.to_variable(arena)?);
}
Ok(Variable::Array(vec))
impl From<&Value> for Variable {
fn from(value: &Value) -> Self {
match value {
Value::Null => Variable::Null,
Value::Bool(b) => Variable::Bool(*b),
Value::Number(n) => {
Variable::Number(Decimal::from_str_exact(n.as_str()).expect("Allowed number"))
}
Value::Object(obj) => {
let mut m = BumpMap::with_capacity_in(obj.len(), arena);
for (k, v) in obj {
m.insert(&*arena.alloc_str(k.as_str()), v.to_variable(arena)?);
}
Value::String(s) => Variable::String(Rc::from(s.as_str())),
Value::Array(arr) => Variable::from_array(arr.iter().map(Variable::from).collect()),
Value::Object(obj) => Variable::from_object(
obj.iter()
.map(|(k, v)| (k.clone(), Variable::from(v)))
.collect(),
),
}
}
}
Ok(Variable::Object(m))
impl From<Variable> for Value {
fn from(value: Variable) -> Self {
match value {
Variable::Null => Value::Null,
Variable::Bool(b) => Value::Bool(b),
Variable::Number(n) => Value::Number(Number::from_string_unchecked(n.to_string())),
Variable::String(s) => Value::String(s.to_string()),
Variable::Array(arr) => {
let vec = Rc::try_unwrap(arr)
.map(|a| a.into_inner())
.unwrap_or_else(|s| {
let borrowed = s.borrow();
borrowed.clone()
});
Value::Array(vec.into_iter().map(Value::from).collect())
}
Variable::Object(obj) => {
let hmap = Rc::try_unwrap(obj)
.map(|a| a.into_inner())
.unwrap_or_else(|s| {
let borrowed = s.borrow();
borrowed.clone()
});
Value::Object(hmap.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
}
}
}
}
impl TryFrom<&Variable> for NaiveDateTime {
type Error = VMError;
fn try_from(value: &Variable) -> Result<Self, Self::Error> {
match value {
Variable::String(a) => date_time(a),
#[allow(deprecated)]
Variable::Number(a) => NaiveDateTime::from_timestamp_opt(
a.to_i64().ok_or_else(|| VMError::OpcodeErr {
opcode: "DateManipulation".into(),
message: "Failed to extract date".into(),
})?,
0,
)
.ok_or_else(|| VMError::ParseDateTimeErr {
timestamp: a.to_string(),
}),
_ => Err(VMError::OpcodeErr {
opcode: "DateManipulation".into(),
message: "Unsupported type".into(),
}),
}
}
}
+43 -40
View File
@@ -1,22 +1,17 @@
use std::fmt::Formatter;
use std::marker::PhantomData;
use bumpalo::collections::Vec as BumpVec;
use bumpalo::Bump;
use crate::variable::Variable;
use ahash::{HashMap, HashMapExt};
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use serde::de::{DeserializeSeed, Error, MapAccess, SeqAccess, Unexpected, Visitor};
use serde::Deserializer;
use serde::{Deserialize, Deserializer};
use std::fmt::Formatter;
use std::marker::PhantomData;
use std::rc::Rc;
use crate::variable::map::BumpMap;
use crate::variable::Variable;
struct VariableVisitor;
struct VariableVisitor<'arena> {
arena: &'arena Bump,
}
impl<'arena, 'de: 'arena> Visitor<'de> for VariableVisitor<'arena> {
type Value = Variable<'arena>;
impl<'de> Visitor<'de> for VariableVisitor {
type Value = Variable;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str("A valid type")
@@ -60,10 +55,7 @@ impl<'arena, 'de: 'arena> Visitor<'de> for VariableVisitor<'arena> {
where
E: Error,
{
match Decimal::from_str_exact(v) {
Ok(d) => Ok(Variable::Number(d)),
Err(_) => Ok(Variable::String(self.arena.alloc_str(v))),
}
Ok(Variable::String(Rc::from(v)))
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
@@ -77,47 +69,58 @@ impl<'arena, 'de: 'arena> Visitor<'de> for VariableVisitor<'arena> {
where
A: SeqAccess<'de>,
{
let mut vec = BumpVec::with_capacity_in(seq.size_hint().unwrap_or_default(), self.arena);
while let Some(value) = seq.next_element_seed(VariableDeserializer { arena: self.arena })? {
let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or_default());
while let Some(value) = seq.next_element_seed(VariableDeserializer)? {
vec.push(value);
}
Ok(Variable::Array(vec))
Ok(Variable::from_array(vec))
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut m = BumpMap::with_capacity_in(map.size_hint().unwrap_or_default(), self.arena);
while let Some((key, value)) =
map.next_entry_seed(PhantomData, VariableDeserializer { arena: self.arena })?
{
m.insert(&*self.arena.alloc_str(key), value);
let mut m = HashMap::with_capacity(map.size_hint().unwrap_or_default());
let mut first = true;
while let Some((key, value)) = map.next_entry_seed(PhantomData, VariableDeserializer)? {
if first && key == "$serde_json::private::Number" {
return Ok(Variable::Number(
Decimal::from_str_exact(
value
.as_str()
.ok_or_else(|| Error::custom("failed to deserialize number"))?,
)
.map_err(|_| Error::custom("invalid number"))?,
));
}
m.insert(key, value);
first = false;
}
Ok(Variable::Object(m))
Ok(Variable::from_object(m))
}
}
pub struct VariableDeserializer<'arena> {
arena: &'arena Bump,
}
pub struct VariableDeserializer;
impl<'arena> VariableDeserializer<'arena> {
#[allow(dead_code)]
pub fn new_in(arena: &'arena Bump) -> Self {
Self { arena }
}
}
impl<'arena, 'de: 'arena> DeserializeSeed<'de> for VariableDeserializer<'arena> {
type Value = Variable<'arena>;
impl<'de> DeserializeSeed<'de> for VariableDeserializer {
type Value = Variable;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(VariableVisitor { arena: self.arena })
deserializer.deserialize_any(VariableVisitor)
}
}
impl<'de> Deserialize<'de> for Variable {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(VariableVisitor)
}
}
-95
View File
@@ -1,95 +0,0 @@
use std::borrow::Borrow;
use bumpalo::collections::Vec as BumpVec;
use bumpalo::Bump;
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone)]
pub struct BumpMap<'arena, K, V> {
inner: BumpVec<'arena, (K, V)>,
}
impl<'arena, K, V> BumpMap<'arena, K, V> {
pub fn new_in(arena: &'arena Bump) -> Self {
BumpMap {
inner: BumpVec::new_in(arena),
}
}
pub fn with_capacity_in(capacity: usize, arena: &'arena Bump) -> Self {
BumpMap {
inner: BumpVec::with_capacity_in(capacity, arena),
}
}
pub fn from_iter_in<I: IntoIterator<Item = (K, V)>>(iter: I, arena: &'arena Bump) -> Self {
BumpMap {
inner: BumpVec::from_iter_in(iter, arena),
}
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
self.inner.iter().map(|(k, v)| (k, v))
}
fn position_of<Q>(&self, key: &Q) -> Option<usize>
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.inner.iter().position(|(k, _)| k.borrow() == key)
}
pub fn contains_key<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.position_of(key).is_some()
}
pub fn get<Q>(&self, key: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.position_of(key.borrow()).map(|p| &self.inner[p].1)
}
pub fn get_mut<Q>(&mut self, key: &Q) -> Option<&mut V>
where
K: Borrow<Q>,
Q: Eq + ?Sized,
{
self.position_of(key).map(|p| &mut self.inner[p].1)
}
pub fn insert(&mut self, key: K, value: V) -> Option<V>
where
K: Eq,
{
let existing_position = self.position_of(&key);
self.inner.push((key, value));
existing_position.and_then(|p| Some(self.inner.remove(p).1))
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn capacity(&self) -> usize {
self.inner.capacity()
}
pub fn clear(&mut self) {
self.inner.clear()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn into_inner(self) -> BumpVec<'arena, (K, V)> {
self.inner
}
}
+232 -118
View File
@@ -1,64 +1,91 @@
pub use crate::variable::map::BumpMap;
pub use bumpalo::collections::Vec as BumpVec;
use bumpalo::Bump;
use chrono::NaiveDateTime;
use rust_decimal::prelude::ToPrimitive;
use ahash::HashMap;
use rust_decimal::prelude::Zero;
use rust_decimal::Decimal;
use serde_json::{Number, Value};
use strum_macros::Display;
use serde_json::Value;
use std::cell::RefCell;
use std::collections::hash_map::Entry;
use std::fmt::{Debug, Display, Formatter};
use std::ops::Deref;
use std::rc::Rc;
mod conv;
mod de;
mod map;
mod ser;
mod types;
use crate::vm::helpers::date_time;
use crate::vm::VMError;
pub use conv::ToVariable;
pub use de::VariableDeserializer;
pub use types::VariableType;
#[derive(Debug, PartialEq, Eq, Display)]
pub enum Variable<'arena> {
pub(crate) type RcCell<T> = Rc<RefCell<T>>;
#[derive(PartialEq, Eq)]
pub enum Variable {
Null,
Bool(bool),
Number(Decimal),
String(&'arena str),
Array(BumpVec<'arena, Variable<'arena>>),
Object(BumpMap<'arena, &'arena str, Variable<'arena>>),
String(Rc<str>),
Array(RcCell<Vec<Variable>>),
Object(RcCell<HashMap<String, Variable>>),
}
impl<'arena> Variable<'arena> {
pub fn empty_object(arena: &'arena Bump) -> Self {
Variable::Object(BumpMap::new_in(arena))
impl Variable {
pub fn from_array(arr: Vec<Variable>) -> Self {
Self::Array(Rc::new(RefCell::new(arr)))
}
pub fn empty_array(arena: &'arena Bump) -> Self {
Variable::Array(BumpVec::new_in(arena))
pub fn from_object(obj: HashMap<String, Variable>) -> Self {
Self::Object(Rc::new(RefCell::new(obj)))
}
pub fn as_str(&self) -> Option<&'arena str> {
pub fn empty_object() -> Self {
Variable::Object(Default::default())
}
pub fn empty_array() -> Self {
Variable::Array(Default::default())
}
pub fn as_str(&self) -> Option<&str> {
match self {
Variable::String(s) => Some(*s),
Variable::String(s) => Some(s.as_ref()),
_ => None,
}
}
pub fn as_array(&self) -> Option<&BumpVec<'arena, Variable<'arena>>> {
pub fn as_rc_str(&self) -> Option<Rc<str>> {
match self {
Variable::Array(arr) => Some(arr),
Variable::String(s) => Some(s.clone()),
_ => None,
}
}
pub fn as_object(&self) -> Option<&BumpMap<'arena, &'arena str, Variable<'arena>>> {
pub fn as_array(&self) -> Option<RcCell<Vec<Variable>>> {
match self {
Variable::Object(obj) => Some(obj),
Variable::Array(arr) => Some(arr.clone()),
_ => None,
}
}
pub fn is_array(&self) -> bool {
match self {
Variable::Array(_) => true,
_ => false,
}
}
pub fn as_object(&self) -> Option<RcCell<HashMap<String, Variable>>> {
match self {
Variable::Object(obj) => Some(obj.clone()),
_ => None,
}
}
pub fn is_object(&self) -> bool {
match self {
Variable::Object(_) => true,
_ => false,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Variable::Bool(b) => Some(*b),
@@ -66,6 +93,13 @@ impl<'arena> Variable<'arena> {
}
}
pub fn as_number(&self) -> Option<Decimal> {
match self {
Variable::Number(n) => Some(*n),
_ => None,
}
}
pub fn type_name(&self) -> &'static str {
match self {
Variable::Null => "null",
@@ -78,108 +112,188 @@ impl<'arena> Variable<'arena> {
}
pub fn to_value(&self) -> Value {
match self {
Variable::Null => Value::Null,
Variable::Bool(b) => Value::Bool(*b),
Variable::Number(n) => {
Value::Number(Number::from_string_unchecked(n.normalize().to_string()))
}
Variable::String(str) => Value::String(str.to_string()),
Variable::Array(arr) => Value::Array(arr.iter().map(|i| i.to_value()).collect()),
Variable::Object(obj) => Value::Object(
obj.iter()
.map(|(k, v)| (k.to_string(), v.to_value()))
.collect(),
),
}
Value::from(self.shallow_clone())
}
pub fn clone_in<'new>(&self, arena: &'new Bump) -> Variable<'new> {
pub fn dot(&self, key: &str) -> Option<Variable> {
key.split('.')
.try_fold(self.shallow_clone(), |var, part| match var {
Variable::Object(obj) => {
let reference = obj.borrow();
reference.get(part).map(|v| v.shallow_clone())
}
_ => None,
})
}
fn dot_head(&self, key: &str) -> Option<Variable> {
let mut parts = Vec::from_iter(key.split('.'));
parts.pop();
parts
.iter()
.try_fold(self.shallow_clone(), |var, part| match var {
Variable::Object(obj) => {
let mut obj_ref = obj.borrow_mut();
Some(match obj_ref.entry(part.to_string()) {
Entry::Occupied(occ) => occ.get().shallow_clone(),
Entry::Vacant(vac) => vac.insert(Self::empty_object()).shallow_clone(),
})
}
_ => None,
})
}
pub fn dot_remove(&self, key: &str) -> Option<Variable> {
let last_part = key.split('.').last()?;
let head = self.dot_head(key)?;
let Variable::Object(object_ref) = head else {
return None;
};
let mut object = object_ref.borrow_mut();
object.remove(last_part)
}
pub fn dot_insert(&self, key: &str, variable: Variable) -> Option<Variable> {
let last_part = key.split('.').last()?;
let head = self.dot_head(key)?;
let Variable::Object(object_ref) = head else {
return None;
};
let mut object = object_ref.borrow_mut();
object.insert(last_part.to_string(), variable)
}
pub fn merge(&mut self, patch: &Variable) -> Variable {
merge_variables(self, patch, true);
self.shallow_clone()
}
pub fn shallow_clone(&self) -> Self {
match self {
Variable::Null => Variable::Null,
Variable::Bool(b) => Variable::Bool(*b),
Variable::Number(n) => Variable::Number(*n),
Variable::String(s) => Variable::String(arena.alloc_str(s)),
Variable::Array(arr) => Variable::Array(BumpVec::from_iter_in(
arr.iter().map(|v| v.clone_in(arena)),
arena,
)),
Variable::Object(obj) => Variable::Object(BumpMap::from_iter_in(
obj.iter()
.map(|(k, v)| (&*arena.alloc_str(k), v.clone_in(arena))),
arena,
)),
Variable::String(s) => Variable::String(s.clone()),
Variable::Array(a) => Variable::Array(a.clone()),
Variable::Object(o) => Variable::Object(o.clone()),
}
}
pub fn dot(&self, key: &str) -> Option<&Variable<'arena>> {
key.split('.').try_fold(self, |var, part| match var {
Variable::Object(obj) => obj.get(part),
_ => None,
})
}
pub fn dot_mut(&mut self, key: &str) -> Option<&mut Variable<'arena>> {
key.split('.').try_fold(self, |var, part| match var {
Variable::Object(obj) => obj.get_mut(part),
_ => None,
})
}
pub fn dot_insert(
&mut self,
arena: &'arena Bump,
key: &str,
variable: Variable<'arena>,
) -> Option<&mut Variable<'arena>> {
let mut parts: BumpVec<&'arena str> =
BumpVec::from_iter_in(key.split('.').map(|p| &*arena.alloc_str(p)), arena);
let Some(last_part) = parts.pop() else {
return None;
};
let head = parts.iter().try_fold(self, |var, part| match var {
Variable::Object(obj) => {
if obj.contains_key(part) {
obj.get_mut(part)
} else {
obj.insert(part, Self::empty_object(arena));
obj.get_mut(part)
}
pub fn deep_clone(&self) -> Self {
match self {
Variable::Array(a) => {
let arr = a.borrow();
Variable::from_array(arr.iter().map(|v| v.deep_clone()).collect())
}
_ => None,
})?;
let Variable::Object(head_obj) = head else {
return None;
};
head_obj.insert(last_part, variable);
head_obj.get_mut(last_part)
Variable::Object(o) => {
let obj = o.borrow();
Variable::from_object(
obj.iter()
.map(|(k, v)| (k.to_string(), v.deep_clone()))
.collect(),
)
}
_ => self.shallow_clone(),
}
}
}
impl TryFrom<&Variable<'_>> for NaiveDateTime {
type Error = VMError;
fn try_from(value: &Variable<'_>) -> Result<Self, Self::Error> {
match value {
Variable::String(a) => date_time(a),
#[allow(deprecated)]
Variable::Number(a) => NaiveDateTime::from_timestamp_opt(
a.to_i64().ok_or_else(|| VMError::OpcodeErr {
opcode: "DateManipulation".into(),
message: "Failed to extract date".into(),
})?,
0,
)
.ok_or_else(|| VMError::ParseDateTimeErr {
timestamp: a.to_string(),
}),
_ => Err(VMError::OpcodeErr {
opcode: "DateManipulation".into(),
message: "Unsupported type".into(),
}),
pub fn depth_clone(&self, depth: usize) -> Self {
match depth.is_zero() {
true => self.shallow_clone(),
false => match self {
Variable::Array(a) => {
let arr = a.borrow();
Variable::from_array(arr.iter().map(|v| v.depth_clone(depth - 1)).collect())
}
Variable::Object(o) => {
let obj = o.borrow();
Variable::from_object(
obj.iter()
.map(|(k, v)| (k.to_string(), v.depth_clone(depth - 1)))
.collect(),
)
}
_ => self.shallow_clone(),
},
}
}
}
impl Clone for Variable {
fn clone(&self) -> Self {
self.shallow_clone()
}
}
fn merge_variables(doc: &mut Variable, patch: &Variable, top_level: bool) {
if !patch.is_object() && !patch.is_array() && top_level {
return;
}
if doc.is_object() && patch.is_object() {
let map_ref = doc.as_object().unwrap();
let mut map = map_ref.borrow_mut();
let patch_ref = patch.as_object().unwrap();
let patch = patch_ref.borrow();
for (key, value) in patch.deref() {
if value == &Variable::Null {
map.remove(key.as_str());
} else {
let entry = map.entry(key.to_string()).or_insert(Variable::Null);
merge_variables(entry, value, false)
}
}
} else if doc.is_array() && patch.is_array() {
let arr_ref = doc.as_array().unwrap();
let mut arr = arr_ref.borrow_mut();
let patch_ref = patch.as_array().unwrap();
let patch = patch_ref.borrow();
arr.extend(patch.iter().map(|s| s.shallow_clone()));
} else {
*doc = patch.shallow_clone();
}
}
impl Display for Variable {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Variable::Null => write!(f, "null"),
Variable::Bool(b) => match *b {
true => write!(f, "true"),
false => write!(f, "false"),
},
Variable::Number(n) => write!(f, "{n}"),
Variable::String(s) => write!(f, "\"{s}\""),
Variable::Array(arr) => {
let arr = arr.borrow();
let s = arr
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(",");
write!(f, "[{s}]")
}
Variable::Object(obj) => {
let obj = obj.borrow();
let s = obj
.iter()
.map(|(k, v)| format!("\"{k}\":{v}"))
.collect::<Vec<String>>()
.join(",");
write!(f, "{{{s}}}")
}
}
}
}
impl Debug for Variable {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
+11 -19
View File
@@ -1,9 +1,7 @@
use bumpalo::Bump;
use serde::{Serialize, Serializer};
use crate::variable::Variable;
use serde::{ser, Serialize, Serializer};
use crate::variable::{ser, Variable};
impl<'arena> Serialize for Variable<'arena> {
impl Serialize for Variable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
@@ -13,20 +11,14 @@ impl<'arena> Serialize for Variable<'arena> {
Variable::Bool(v) => serializer.serialize_bool(*v),
Variable::Number(v) => ser::Serialize::serialize(v, serializer),
Variable::String(v) => serializer.serialize_str(v),
Variable::Array(v) => serializer.collect_seq(v.iter()),
Variable::Object(v) => serializer.collect_map(v.iter()),
Variable::Array(v) => {
let borrowed = v.borrow();
serializer.collect_seq(borrowed.iter())
}
Variable::Object(v) => {
let borrowed = v.borrow();
serializer.collect_map(borrowed.iter())
}
}
}
}
#[allow(dead_code)]
pub struct VariableSerializer<'arena> {
arena: &'arena Bump,
}
impl<'arena> VariableSerializer<'arena> {
#[allow(dead_code)]
pub fn new_in(arena: &'arena Bump) -> Self {
Self { arena }
}
}
+33 -27
View File
@@ -1,44 +1,50 @@
use bumpalo::Bump;
use crate::variable::Variable;
use ahash::{HashMap, HashMapExt};
use std::rc::Rc;
use crate::variable::{BumpMap, ToVariable, Variable};
pub(crate) struct IntervalObject<'arena> {
pub(crate) left_bracket: &'arena str,
pub(crate) right_bracket: &'arena str,
pub(crate) left: &'arena Variable<'arena>,
pub(crate) right: &'arena Variable<'arena>,
pub(crate) struct IntervalObject {
pub(crate) left_bracket: Rc<str>,
pub(crate) right_bracket: Rc<str>,
pub(crate) left: Variable,
pub(crate) right: Variable,
}
impl<'arena> ToVariable<'arena> for IntervalObject<'arena> {
type Error = ();
impl IntervalObject {
pub fn to_variable(&self) -> Variable {
let mut tree = HashMap::new();
fn to_variable(&self, arena: &'arena Bump) -> Result<Variable<'arena>, Self::Error> {
let mut tree = BumpMap::new_in(arena);
tree.insert(
"_symbol".to_string(),
Variable::String("Interval".to_string().into()),
);
tree.insert(
"left_bracket".to_string(),
Variable::String(self.left_bracket.clone()),
);
tree.insert(
"right_bracket".to_string(),
Variable::String(self.right_bracket.clone()),
);
tree.insert("left".to_string(), self.left.clone());
tree.insert("right".to_string(), self.right.clone());
tree.insert("_symbol", Variable::String("Interval"));
tree.insert("left_bracket", Variable::String(self.left_bracket));
tree.insert("right_bracket", Variable::String(self.right_bracket));
tree.insert("left", self.left.clone_in(arena));
tree.insert("right", self.right.clone_in(arena));
Ok(Variable::Object(tree))
Variable::from_object(tree)
}
}
impl<'a> IntervalObject<'a> {
pub(crate) fn try_from_object(var: &'a Variable<'a>) -> Option<IntervalObject> {
pub(crate) fn try_from_object(var: Variable) -> Option<IntervalObject> {
let Variable::Object(tree) = var else {
return None;
};
if tree.get("_symbol")?.as_str()? != "Interval" {
let tree_ref = tree.borrow();
if tree_ref.get("_symbol")?.as_str()? != "Interval" {
return None;
}
let left_bracket = tree.get("left_bracket")?.as_str()?;
let right_bracket = tree.get("right_bracket")?.as_str()?;
let left = tree.get("left")?;
let right = tree.get("right")?;
let left_bracket = tree_ref.get("left_bracket")?.as_rc_str()?;
let right_bracket = tree_ref.get("right_bracket")?.as_rc_str()?;
let left = tree_ref.get("left")?.clone();
let right = tree_ref.get("right")?.clone();
Some(Self {
left_bracket,
File diff suppressed because it is too large Load Diff
+10 -23
View File
@@ -1,10 +1,9 @@
use std::ops::Index;
use anyhow::Context;
use bumpalo::Bump;
use serde_json::{json, Value};
use zen_expression::variable::ToVariable;
use zen_expression::variable::Variable;
use zen_expression::Isolate;
struct TestEnv {
@@ -601,7 +600,7 @@ fn isolate_standard_test() {
let mut isolate = Isolate::new();
for TestEnv { env, cases } in tests {
isolate.set_environment(&env);
isolate.set_environment(env.into());
for TestCase { expr, result } in cases {
let isolate_result = isolate.run_standard(expr);
@@ -614,7 +613,7 @@ fn isolate_standard_test() {
continue;
};
assert_eq!(result, response, "{}", expr);
assert_eq!(Variable::from(result), response, "{}", expr);
}
}
}
@@ -725,7 +724,7 @@ fn isolate_unary_tests() {
reference,
} in tests
{
isolate.set_environment(&env);
isolate.set_environment(env.into());
isolate.set_reference(reference).unwrap();
for TestCase { expr, result } in cases {
@@ -734,25 +733,12 @@ fn isolate_unary_tests() {
}
}
#[test]
fn variable_serde_test() {
let env = json!({
"customer": {
"groups": ["admin", "user"],
"purchaseAmounts": [100, 200, 400, 800]
},
});
let bump = Bump::new();
let _ = env.to_variable(&bump);
}
#[test]
fn isolate_test_decimals() {
let mut isolate = Isolate::new();
let result = isolate.run_standard("9223372036854775807").unwrap();
assert_eq!(result, Value::from(9223372036854775807i64));
assert_eq!(result.to_value(), Value::from(9223372036854775807i64));
}
#[test]
@@ -777,7 +763,7 @@ fn test_standard_csv() {
let mut isolate = Isolate::new();
if !input_str.is_empty() {
let input: Value = serde_json5::from_str(input_str).unwrap();
isolate.set_environment(&input);
isolate.set_environment(input.into());
}
let maybe_result = isolate
@@ -786,9 +772,10 @@ fn test_standard_csv() {
assert!(maybe_result.is_ok(), "{}", maybe_result.unwrap_err());
let result = maybe_result.unwrap();
let var_output = Variable::from(output);
assert_eq!(
result, output,
"Expression {expression}. Expected: {output}, got: {result}"
result, var_output,
"Expression {expression}. Expected: {var_output}, got: {result}"
);
}
}
@@ -815,7 +802,7 @@ fn test_unary_csv() {
let mut isolate = Isolate::new();
if !input_str.is_empty() {
let input: Value = serde_json5::from_str(input_str).unwrap();
isolate.set_environment(&input);
isolate.set_environment(input.into());
}
let result = isolate
+12 -10
View File
@@ -1,22 +1,23 @@
use colored::Colorize;
use rustyline::config::Configurer;
use rustyline::{DefaultEditor, Result};
use serde_json::{json, Value};
use serde_json::json;
use zen_expression::Isolate;
use zen_expression::{Isolate, Variable};
trait PrettyPrint {
fn pretty_print(&self) -> String;
}
impl PrettyPrint for Value {
impl PrettyPrint for Variable {
fn pretty_print(&self) -> String {
match &self {
Value::Number(num) => format!("{}", num.to_string().yellow()),
Value::String(str) => format!("{}", format!("'{}'", str).green()),
Value::Bool(b) => format!("{}", b.to_string().yellow()),
Value::Null => format!("{}", "null".bold()),
Value::Array(arr) => {
Variable::Number(num) => format!("{}", num.to_string().yellow()),
Variable::String(str) => format!("{}", format!("'{}'", str).green()),
Variable::Bool(b) => format!("{}", b.to_string().yellow()),
Variable::Null => format!("{}", "null".bold()),
Variable::Array(a) => {
let arr = a.borrow();
let elements = arr
.iter()
.map(|i| i.pretty_print())
@@ -24,7 +25,8 @@ impl PrettyPrint for Value {
.join(", ");
format!("[{}]", elements)
}
Value::Object(map) => {
Variable::Object(m) => {
let map = m.borrow();
let elements = map
.iter()
.map(|(key, value)| format!("{}: {}", key, value.pretty_print()))
@@ -49,7 +51,7 @@ fn main() -> Result<()> {
let mut isolate = Isolate::new();
isolate.set_environment(
&json!({ "customer": { "firstName": "John", "lastName": "Doe", "age": 20 }, "hello": true, "$": 10 }),
json!({ "customer": { "firstName": "John", "lastName": "Doe", "age": 20 }, "hello": true, "$": 10 }).into(),
);
let result = isolate.run_standard(line.as_str());
+18 -22
View File
@@ -1,16 +1,16 @@
use std::iter::Peekable;
use std::rc::Rc;
use std::slice::Iter;
use crate::error::TemplateRenderError;
use serde_json::Value;
use zen_expression::Isolate;
use crate::parser::Node;
use zen_expression::variable::Variable;
use zen_expression::Isolate;
#[derive(Debug, PartialEq)]
pub(crate) enum InterpreterResult<'a> {
String(&'a str),
Value(Value),
Variable(Variable),
}
#[derive(Debug)]
@@ -37,7 +37,10 @@ where
}
impl<'source, 'nodes> Interpreter<'source, 'nodes> {
pub(crate) fn collect_for(mut self, context: &Value) -> Result<Value, TemplateRenderError> {
pub(crate) fn collect_for(
mut self,
context: Variable,
) -> Result<Variable, TemplateRenderError> {
self.isolate.set_environment(context);
while let Some(node) = self.cursor.next() {
@@ -48,12 +51,12 @@ impl<'source, 'nodes> Interpreter<'source, 'nodes> {
}
match self.results.len() {
0 => Ok(Value::Null),
0 => Ok(Variable::Null),
1 => {
let item = self.results.remove(0);
match item {
InterpreterResult::Value(val) => Ok(val),
InterpreterResult::String(str) => Ok(Value::String(str.to_string())),
InterpreterResult::Variable(val) => Ok(val),
InterpreterResult::String(str) => Ok(Variable::String(Rc::from(str))),
}
}
_ => {
@@ -62,11 +65,11 @@ impl<'source, 'nodes> Interpreter<'source, 'nodes> {
.into_iter()
.map(|item| match item {
InterpreterResult::String(str) => str.to_string(),
InterpreterResult::Value(value) => value_to_string(value),
InterpreterResult::Variable(value) => var_to_string(value),
})
.collect::<String>();
Ok(Value::String(string_data))
Ok(Variable::String(Rc::from(string_data.as_str())))
}
}
}
@@ -77,21 +80,14 @@ impl<'source, 'nodes> Interpreter<'source, 'nodes> {
fn expression(&mut self, data: &'source str) -> Result<(), TemplateRenderError> {
let result = self.isolate.run_standard(data)?;
self.results.push(InterpreterResult::Value(result));
self.results.push(InterpreterResult::Variable(result));
Ok(())
}
}
fn value_to_string(value: Value) -> String {
match value {
Value::Null => String::from("null"),
Value::Bool(b) => match b {
true => String::from("true"),
false => String::from("false"),
},
Value::Number(n) => n.to_string(),
Value::String(s) => s,
Value::Array(arr) => Value::Array(arr).to_string(),
Value::Object(obj) => Value::Object(obj).to_string(),
fn var_to_string(var: Variable) -> String {
match var {
Variable::String(s) => s.to_string(),
_ => var.to_string(),
}
}
+2 -3
View File
@@ -3,15 +3,14 @@ mod interpreter;
mod lexer;
mod parser;
use serde_json::Value;
use crate::interpreter::Interpreter;
use crate::lexer::Lexer;
use crate::parser::Parser;
use zen_expression::variable::Variable;
pub use crate::error::{ParserError, TemplateRenderError};
pub fn render(template: &str, context: &Value) -> Result<Value, TemplateRenderError> {
pub fn render(template: &str, context: Variable) -> Result<Variable, TemplateRenderError> {
let tokens = Lexer::from(template.trim()).collect();
let nodes = Parser::from(tokens.as_slice()).collect()?;
+5 -9
View File
@@ -1,4 +1,5 @@
use serde_json::{json, Value};
use zen_expression::variable::Variable;
use zen_tmpl::render;
#[test]
@@ -44,8 +45,8 @@ fn test_values_types() {
for test_case in test_cases {
assert_eq!(
render(test_case.template, &test_case.context).unwrap(),
test_case.expected
render(test_case.template, test_case.context.into()).unwrap(),
Variable::from(test_case.expected)
);
}
}
@@ -84,17 +85,12 @@ fn test_interpolation() {
context: json!(null),
expected: json!("[1,2,3] array"),
},
TestCase {
template: "Customer: {{ customer }}",
context: json!({ "customer": { "firstName": "John", "lastName": "Doe" } }),
expected: json!(r#"Customer: {"firstName":"John","lastName":"Doe"}"#),
},
];
for test_case in test_cases {
assert_eq!(
render(test_case.template, &test_case.context).unwrap(),
test_case.expected
render(test_case.template, test_case.context.into()).unwrap(),
Variable::from(test_case.expected)
);
}
}