mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 00:02:18 +00:00
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:
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>>")]
|
||||
|
||||
@@ -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,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)]
|
||||
|
||||
@@ -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,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,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")?;
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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}""#))
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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};
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ahash::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
pub(crate) mod json_map;
|
||||
@@ -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
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 } => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
+242
-237
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user