feat: schema validation (#267)

* feat: schema validation

* fix prettier

* update implementation

* add test cases

* change validator to string

* update tests
This commit is contained in:
stefan-gorules
2024-12-25 13:00:04 +01:00
committed by GitHub
parent 042087836e
commit 2b73856d55
20 changed files with 427 additions and 126 deletions
-1
View File
@@ -7,7 +7,6 @@ members = [
[workspace.dependencies]
ahash = "0.8.11"
bincode = "2.0.0-rc.3"
bumpalo = "3.16.0"
chrono = "0.4.38"
criterion = "0.5.1"
@@ -21,7 +21,6 @@ const makeToml = ({ version }): string => `
anyhow = { workspace = true }
thiserror = { workspace = true }
async-trait = { workspace = true }
bincode = { workspace = true, optional = true }
serde_json = { workspace = true, features = ["arbitrary_precision"] }
serde = { version = "1", features = ["derive"] }
serde_v8 = { version = "0.88.0" }
+1 -4
View File
@@ -14,7 +14,6 @@ doctest = false
ahash = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
bincode = { workspace = true, optional = true }
petgraph = { workspace = true }
serde_json = { workspace = true, features = ["arbitrary_precision"] }
serde = { workspace = true, features = ["derive", "rc"] }
@@ -26,6 +25,7 @@ tokio = { workspace = true, features = ["sync", "time"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
rquickjs = { version = "0.8.1", features = ["macro", "loader", "rust-alloc", "futures", "either", "properties"] }
itertools = { workspace = true }
jsonschema = "0.26.2"
zen-expression = { path = "../expression", version = "0.37.2" }
zen-tmpl = { path = "../template", version = "0.37.2" }
@@ -33,9 +33,6 @@ zen-tmpl = { path = "../template", version = "0.37.2" }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
criterion = { workspace = true, features = ["async_tokio"] }
[features]
bincode = ["dep:bincode"]
[[bench]]
harness = false
name = "engine"
+15 -3
View File
@@ -1,11 +1,11 @@
use std::sync::Arc;
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::util::validator_cache::ValidatorCache;
use crate::{DecisionGraphValidationError, EvaluationError};
use std::sync::Arc;
use zen_expression::variable::Variable;
/// Represents a JDM decision which can be evaluated
@@ -18,6 +18,8 @@ where
content: Arc<DecisionContent>,
loader: Arc<Loader>,
adapter: Arc<CustomNode>,
validator_cache: ValidatorCache,
}
impl From<DecisionContent> for Decision<NoopLoader, NoopCustomNode> {
@@ -26,6 +28,8 @@ impl From<DecisionContent> for Decision<NoopLoader, NoopCustomNode> {
content: value.into(),
loader: NoopLoader::default().into(),
adapter: NoopCustomNode::default().into(),
validator_cache: Default::default(),
}
}
}
@@ -36,6 +40,8 @@ impl From<Arc<DecisionContent>> for Decision<NoopLoader, NoopCustomNode> {
content: value,
loader: NoopLoader::default().into(),
adapter: NoopCustomNode::default().into(),
validator_cache: Default::default(),
}
}
}
@@ -53,6 +59,7 @@ where
loader,
adapter: self.adapter,
content: self.content,
validator_cache: self.validator_cache,
}
}
@@ -64,6 +71,7 @@ where
loader: self.loader,
adapter,
content: self.content,
validator_cache: self.validator_cache,
}
}
@@ -88,9 +96,12 @@ where
loader: Arc::new(CachedLoader::from(self.loader.clone())),
adapter: self.adapter.clone(),
iteration: 0,
validator_cache: Some(self.validator_cache.clone()),
})?;
Ok(decision_graph.evaluate(context).await?)
let response = decision_graph.evaluate(context).await?;
Ok(response)
}
pub fn validate(&self) -> Result<(), DecisionGraphValidationError> {
@@ -101,6 +112,7 @@ where
loader: Arc::new(CachedLoader::from(self.loader.clone())),
adapter: self.adapter.clone(),
iteration: 0,
validator_cache: Some(self.validator_cache.clone()),
})?;
decision_graph.validate()
+49
View File
@@ -1,8 +1,11 @@
use crate::handler::graph::DecisionGraphValidationError;
use crate::handler::node::NodeError;
use crate::loader::LoaderError;
use jsonschema::{ErrorIterator, ValidationError};
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use serde_json::{Map, Value};
use std::iter::once;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -18,6 +21,9 @@ pub enum EvaluationError {
#[error("Invalid graph")]
InvalidGraph(Box<DecisionGraphValidationError>),
#[error("Validation failed")]
Validation(Box<Value>),
}
impl Serialize for EvaluationError {
@@ -55,6 +61,10 @@ impl Serialize for EvaluationError {
map.serialize_entry("type", "InvalidGraph")?;
map.serialize_entry("source", err)?;
}
EvaluationError::Validation(err) => {
map.serialize_entry("type", "Validation")?;
map.serialize_entry("source", err)?;
}
}
map.end()
@@ -90,3 +100,42 @@ impl From<DecisionGraphValidationError> for Box<EvaluationError> {
Box::new(EvaluationError::InvalidGraph(error.into()))
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ValidationErrorJson {
path: String,
message: String,
}
impl<'a> From<ValidationError<'a>> for ValidationErrorJson {
fn from(value: ValidationError<'a>) -> Self {
ValidationErrorJson {
path: value.instance_path.to_string(),
message: format!("{}", value),
}
}
}
impl<'a> From<ErrorIterator<'a>> for Box<EvaluationError> {
fn from(error_iter: ErrorIterator<'a>) -> Self {
let errors: Vec<ValidationErrorJson> = error_iter.into_iter().map(From::from).collect();
let mut json_map = Map::new();
json_map.insert(
"errors".to_string(),
serde_json::to_value(errors).unwrap_or_default(),
);
Box::new(EvaluationError::Validation(Box::new(Value::Object(
json_map,
))))
}
}
impl<'a> From<ValidationError<'a>> for Box<EvaluationError> {
fn from(value: ValidationError<'a>) -> Self {
let iterator: ErrorIterator<'a> = Box::new(once(value));
Box::<EvaluationError>::from(iterator)
}
}
+5
View File
@@ -4,6 +4,7 @@ use crate::handler::graph::{DecisionGraph, DecisionGraphConfig};
use crate::handler::node::{NodeRequest, NodeResponse, NodeResult};
use crate::loader::DecisionLoader;
use crate::model::DecisionNodeKind;
use crate::util::validator_cache::ValidatorCache;
use anyhow::anyhow;
use std::future::Future;
use std::pin::Pin;
@@ -17,6 +18,7 @@ pub struct DecisionHandler<L: DecisionLoader + 'static, A: CustomNodeAdapter + '
adapter: Arc<A>,
max_depth: u8,
js_function: Option<Rc<Function>>,
validator_cache: ValidatorCache,
}
impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionHandler<L, A> {
@@ -26,6 +28,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionHandle
loader: Arc<L>,
adapter: Arc<A>,
js_function: Option<Rc<Function>>,
validator_cache: ValidatorCache,
) -> Self {
Self {
trace,
@@ -33,6 +36,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionHandle
adapter,
max_depth,
js_function,
validator_cache,
}
}
@@ -58,6 +62,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionHandle
adapter: self.adapter.clone(),
iteration: request.iteration + 1,
trace: self.trace,
validator_cache: Some(self.validator_cache.clone()),
})?
.with_function(self.js_function.clone());
@@ -33,7 +33,7 @@ async fn execute_http<'js>(
url: String,
data: Option<JsValue>,
config: Option<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
) -> rquickjs::Result<HttpResponse<'js>> {
static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
let client = HTTP_CLIENT.get_or_init(|| reqwest::Client::new()).clone();
@@ -163,7 +163,7 @@ async fn get<'js>(
ctx: Ctx<'js>,
url: String,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
) -> rquickjs::Result<HttpResponse<'js>> {
execute_http(ctx, Method::GET, url, None, config.0).await
}
@@ -172,7 +172,7 @@ async fn post<'js>(
url: String,
data: JsValue,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
) -> rquickjs::Result<HttpResponse<'js>> {
execute_http(ctx, Method::POST, url, Some(data), config.0).await
}
@@ -181,7 +181,7 @@ async fn patch<'js>(
url: String,
data: JsValue,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
) -> rquickjs::Result<HttpResponse<'js>> {
execute_http(ctx, Method::PATCH, url, Some(data), config.0).await
}
@@ -190,7 +190,7 @@ async fn put<'js>(
url: String,
data: JsValue,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
) -> rquickjs::Result<HttpResponse<'js>> {
execute_http(ctx, Method::PUT, url, Some(data), config.0).await
}
@@ -198,7 +198,7 @@ async fn delete<'js>(
ctx: Ctx<'js>,
url: String,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
) -> rquickjs::Result<HttpResponse<'js>> {
execute_http(ctx, Method::DELETE, url, None, config.0).await
}
@@ -206,7 +206,7 @@ async fn head<'js>(
ctx: Ctx<'js>,
url: String,
config: Opt<HttpConfig>,
) -> rquickjs::Result<HttpResponse> {
) -> rquickjs::Result<HttpResponse<'js>> {
execute_http(ctx, Method::DELETE, url, None, config.0).await
}
@@ -64,6 +64,7 @@ impl<Loader: DecisionLoader + 'static, Adapter: CustomNodeAdapter + 'static> Run
adapter,
iteration: iteration + 1,
trace,
validator_cache: None,
})
.or_throw(&ctx)?;
+77 -13
View File
@@ -12,6 +12,7 @@ use crate::handler::table::zen::DecisionTableHandler;
use crate::handler::traversal::{GraphWalker, StableDiDecisionGraph};
use crate::loader::DecisionLoader;
use crate::model::{DecisionContent, DecisionNodeKind, FunctionNodeContent};
use crate::util::validator_cache::ValidatorCache;
use crate::{EvaluationError, NodeError};
use ahash::{HashMap, HashMapExt};
use anyhow::anyhow;
@@ -34,6 +35,7 @@ pub struct DecisionGraph<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'st
max_depth: u8,
iteration: u8,
runtime: Option<Rc<Function>>,
validator_cache: ValidatorCache,
}
pub struct DecisionGraphConfig<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> {
@@ -43,6 +45,7 @@ pub struct DecisionGraphConfig<L: DecisionLoader + 'static, A: CustomNodeAdapter
pub trace: bool,
pub iteration: u8,
pub max_depth: u8,
pub validator_cache: Option<ValidatorCache>,
}
impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGraph<L, A> {
@@ -80,6 +83,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGraph<
loader: config.loader,
adapter: config.adapter,
max_depth: config.max_depth,
validator_cache: config.validator_cache.unwrap_or_default(),
runtime: None,
})
}
@@ -116,7 +120,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGraph<
}
pub fn validate(&self) -> Result<(), DecisionGraphValidationError> {
let input_count = self.node_kind_count(DecisionNodeKind::InputNode);
let input_count = self.input_node_count();
if input_count != 1 {
return Err(DecisionGraphValidationError::InvalidInputCount(
input_count as u32,
@@ -130,10 +134,10 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGraph<
Ok(())
}
fn node_kind_count(&self, kind: DecisionNodeKind) -> usize {
fn input_node_count(&self) -> usize {
self.graph
.node_weights()
.filter(|weight| weight.kind == kind)
.filter(|weight| matches!(weight.kind, DecisionNodeKind::InputNode { content: _ }))
.count()
}
@@ -194,23 +198,82 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGraph<
}
match &node.kind {
DecisionNodeKind::InputNode => {
walker.set_node_data(nid, context.clone());
trace!({
input: Variable::Null,
output: Variable::Null,
trace_data: None,
});
}
DecisionNodeKind::OutputNode => {
DecisionNodeKind::InputNode { content } => {
trace!({
input: Variable::Null,
output: Variable::Null,
trace_data: None,
});
if let Some(json_schema) = content
.schema
.as_ref()
.map(|s| serde_json::from_str::<Value>(&s).ok())
.flatten()
{
let validator = self
.validator_cache
.get_or_insert(node.id.as_str(), &json_schema)
.await
.map_err(|e| NodeError {
source: e.into(),
node_id: node.id.clone(),
trace: error_trace(&node_traces),
})?;
let context_json = context.to_value();
validator.validate(&context_json).map_err(|e| NodeError {
source: anyhow!(serde_json::to_value(
Into::<Box<EvaluationError>>::into(e)
)
.unwrap_or_default()),
node_id: node.id.clone(),
trace: error_trace(&node_traces),
})?;
}
walker.set_node_data(nid, context.clone());
}
DecisionNodeKind::OutputNode { content } => {
let incoming_data = walker.incoming_node_data(&self.graph, nid, false);
trace!({
input: incoming_data.clone(),
output: Variable::Null,
trace_data: None,
});
if let Some(json_schema) = content
.schema
.as_ref()
.map(|s| serde_json::from_str::<Value>(&s).ok())
.flatten()
{
let validator = self
.validator_cache
.get_or_insert(node.id.as_str(), &json_schema)
.await
.map_err(|e| NodeError {
source: e.into(),
node_id: node.id.clone(),
trace: error_trace(&node_traces),
})?;
let incoming_data_json = incoming_data.to_value();
validator
.validate(&incoming_data_json)
.map_err(|e| NodeError {
source: anyhow!(serde_json::to_value(
Into::<Box<EvaluationError>>::into(e)
)
.unwrap_or_default()),
node_id: node.id.clone(),
trace: error_trace(&node_traces),
})?;
}
return Ok(DecisionGraphResponse {
result: walker.incoming_node_data(&self.graph, nid, false),
result: incoming_data,
performance: format!("{:.1?}", root_start.elapsed()),
trace: node_traces,
});
@@ -297,6 +360,7 @@ impl<L: DecisionLoader + 'static, A: CustomNodeAdapter + 'static> DecisionGraph<
self.loader.clone(),
self.adapter.clone(),
self.runtime.clone(),
self.validator_cache.clone(),
)
.handle(node_request.clone())
.await
+1 -1
View File
@@ -23,9 +23,9 @@ pub struct NodeRequest {
#[derive(Error, Debug)]
pub struct NodeError {
pub node_id: String,
pub trace: Option<Value>,
#[source]
pub source: anyhow::Error,
pub trace: Option<Value>,
}
impl Display for NodeError {
+1 -1
View File
@@ -45,7 +45,7 @@ impl GraphWalker {
self.to_visit
.extend(g.node_identifiers().filter(move |&nid| {
g.node_weight(nid)
.is_some_and(|n| n.kind == DecisionNodeKind::InputNode)
.is_some_and(|n| matches!(n.kind, DecisionNodeKind::InputNode { content: _ }))
}));
}
+40 -70
View File
@@ -5,7 +5,6 @@ use std::sync::Arc;
/// JDM Decision model
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct DecisionContent {
pub nodes: Vec<Arc<DecisionNode>>,
@@ -13,7 +12,6 @@ pub struct DecisionContent {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct DecisionEdge {
pub id: String,
@@ -23,7 +21,6 @@ pub struct DecisionEdge {
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct DecisionNode {
pub id: String,
@@ -40,22 +37,52 @@ impl PartialEq for DecisionNode {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum DecisionNodeKind {
InputNode,
OutputNode,
FunctionNode { content: FunctionNodeContent },
DecisionNode { content: DecisionNodeContent },
DecisionTableNode { content: DecisionTableContent },
ExpressionNode { content: ExpressionNodeContent },
SwitchNode { content: SwitchNodeContent },
CustomNode { content: CustomNodeContent },
InputNode {
#[serde(default)]
content: InputNodeContent,
},
OutputNode {
#[serde(default)]
content: OutputNodeContent,
},
FunctionNode {
content: FunctionNodeContent,
},
DecisionNode {
content: DecisionNodeContent,
},
DecisionTableNode {
content: DecisionTableContent,
},
ExpressionNode {
content: ExpressionNodeContent,
},
SwitchNode {
content: SwitchNodeContent,
},
CustomNode {
content: CustomNodeContent,
},
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct InputNodeContent {
#[serde(default, deserialize_with = "empty_string_is_none")]
pub schema: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct OutputNodeContent {
#[serde(default, deserialize_with = "empty_string_is_none")]
pub schema: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum FunctionNodeContent {
@@ -64,14 +91,12 @@ pub enum FunctionNodeContent {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct FunctionContent {
pub source: String,
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct DecisionNodeContent {
pub key: String,
@@ -80,7 +105,6 @@ pub struct DecisionNodeContent {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct DecisionTableContent {
pub rules: Vec<HashMap<String, String>>,
@@ -92,7 +116,6 @@ pub struct DecisionTableContent {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub enum DecisionTableHitPolicy {
First,
@@ -100,7 +123,6 @@ pub enum DecisionTableHitPolicy {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct DecisionTableInputField {
pub id: String,
@@ -110,7 +132,6 @@ pub struct DecisionTableInputField {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct DecisionTableOutputField {
pub id: String,
@@ -119,7 +140,6 @@ pub struct DecisionTableOutputField {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct ExpressionNodeContent {
pub expressions: Vec<Expression>,
@@ -128,7 +148,6 @@ pub struct ExpressionNodeContent {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct Expression {
pub id: String,
@@ -137,7 +156,6 @@ pub struct Expression {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct SwitchNodeContent {
#[serde(default)]
@@ -146,7 +164,6 @@ pub struct SwitchNodeContent {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct SwitchStatement {
pub id: String,
@@ -154,7 +171,6 @@ pub struct SwitchStatement {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub enum SwitchStatementHitPolicy {
#[default]
@@ -163,7 +179,6 @@ pub enum SwitchStatementHitPolicy {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub struct TransformAttributes {
#[serde(default, deserialize_with = "empty_string_is_none")]
@@ -177,7 +192,6 @@ pub struct TransformAttributes {
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, Default)]
#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
#[serde(rename_all = "camelCase")]
pub enum TransformExecutionMode {
#[default]
@@ -192,50 +206,6 @@ pub struct CustomNodeContent {
pub config: Arc<Value>,
}
#[cfg(feature = "bincode")]
impl ::bincode::Encode for CustomNodeContent {
fn encode<__E: ::bincode::enc::Encoder>(
&self,
encoder: &mut __E,
) -> Result<(), ::bincode::error::EncodeError> {
let config_string = self.config.to_string();
::bincode::Encode::encode(&self.kind, encoder)?;
::bincode::Encode::encode(config_string.as_bytes(), encoder)?;
Ok(())
}
}
#[cfg(feature = "bincode")]
impl ::bincode::Decode for CustomNodeContent {
fn decode<__D: ::bincode::de::Decoder>(
decoder: &mut __D,
) -> Result<Self, ::bincode::error::DecodeError> {
let kind: String = ::bincode::Decode::decode(decoder)?;
let config_string: String = ::bincode::Decode::decode(decoder)?;
let config = serde_json::from_str(config_string.as_str())
.map_err(|_| ::bincode::error::DecodeError::Other("failed to deserialize value"))?;
Ok(Self { kind, config })
}
}
#[cfg(feature = "bincode")]
impl<'__de> ::bincode::BorrowDecode<'__de> for CustomNodeContent {
fn borrow_decode<__D: ::bincode::de::BorrowDecoder<'__de>>(
decoder: &mut __D,
) -> Result<Self, ::bincode::error::DecodeError> {
let kind: String = ::bincode::BorrowDecode::borrow_decode(decoder)?;
let config_string: String = ::bincode::BorrowDecode::borrow_decode(decoder)?;
let config = serde_json::from_str(config_string.as_str())
.map_err(|_| ::bincode::error::DecodeError::Other("failed to deserialize value"))?;
Ok(Self { kind, config })
}
}
fn empty_string_is_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
+1
View File
@@ -1 +1,2 @@
mod transform_attribute;
pub mod validator_cache;
+34
View File
@@ -0,0 +1,34 @@
use crate::EvaluationError;
use ahash::HashMap;
use jsonschema::Validator;
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone, Default, Debug)]
pub struct ValidatorCache {
inner: Arc<RwLock<HashMap<String, Arc<Validator>>>>,
}
impl ValidatorCache {
pub async fn get(&self, key: &str) -> Option<Arc<Validator>> {
let read = self.inner.read().await;
read.get(key).cloned()
}
pub async fn get_or_insert(
&self,
key: &str,
schema: &Value,
) -> Result<Arc<Validator>, Box<EvaluationError>> {
if let Some(v) = self.get(key).await {
return Ok(v);
}
let mut w_shared = self.inner.write().await;
let validator = Arc::new(jsonschema::draft7::new(&schema)?);
w_shared.insert(key.to_string(), validator.clone());
Ok(validator)
}
}
+47 -1
View File
@@ -241,7 +241,7 @@ async fn engine_graph_tests() {
let engine = DecisionEngine::default();
let graphs_path = Path::new(test_data_root().as_str()).join("graphs");
let file_list = std::fs::read_dir(graphs_path).unwrap();
let file_list = fs::read_dir(graphs_path).unwrap();
for maybe_file in file_list {
let Ok(file) = maybe_file else {
panic!("Failed to read DirEntry {maybe_file:?}");
@@ -293,3 +293,49 @@ async fn engine_function_v2() {
)
}
}
#[tokio::test]
async fn test_validation() {
let engine = DecisionEngine::default().with_loader(create_fs_loader().into());
let context_valid = json!({
"color": "red",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@doe.com",
"age": 20
}
});
let context_invalid = json!({
"color": "redd",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john@doe.com",
"age": 20
}
});
assert!(engine
.evaluate("customer-input-schema.json", context_valid.clone().into())
.await
.is_ok());
assert!(engine
.evaluate("customer-input-schema.json", context_invalid.clone().into())
.await
.is_err());
assert!(engine
.evaluate("customer-output-schema.json", context_valid.clone().into())
.await
.is_ok());
assert!(engine
.evaluate(
"customer-output-schema.json",
context_invalid.clone().into()
)
.await
.is_err());
}
-24
View File
@@ -5,30 +5,6 @@ use zen_engine::model::DecisionContent;
mod support;
#[cfg(feature = "bincode")]
mod bincode_tests {
use crate::support::load_test_data;
use bincode::config;
use zen_engine::model::DecisionContent;
#[test]
fn jdm_bincode() {
let decision_content = load_test_data("table.json");
let cache_slice_r = bincode::encode_to_vec(&decision_content, config::standard());
assert!(cache_slice_r.is_ok(), "Bincode serialisation failed");
let cache_slice = cache_slice_r.unwrap();
let decode_res =
bincode::decode_from_slice::<DecisionContent, _>(&cache_slice, config::standard());
assert!(decode_res.is_ok(), "Bincode deserialization failed");
let decoded_decision_content = decode_res.unwrap();
assert_eq!(decoded_decision_content.0, decision_content);
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn jdm_serde() {
@@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Generated schema for Root",
"type": "object",
"properties": {
"color": {
"enum": [
"red",
"blue",
"green"
]
},
"customer": {
"type": "object",
"properties": {
"firstName": {
"description": "Customer first name",
"type": "string",
"minimum": 1
},
"lastName": {
"description": "Customer last name",
"type": "string",
"minimum": 1
},
"email": {
"description": "Customer email",
"type": "string",
"format": "email"
},
"age": {
"description": "Customer age",
"type": "number",
"minimum": 18
}
},
"required": [
"firstName",
"lastName",
"email",
"age"
]
}
},
"required": [
"color",
"customer"
]
}
+34
View File
@@ -0,0 +1,34 @@
{
"contentType": "application/vnd.gorules.decision",
"nodes": [
{
"type": "inputNode",
"id": "4354dede-b4ed-4a57-9b80-45c1e33e2326",
"name": "request",
"content": {
"schema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Generated schema for Root\",\"type\":\"object\",\"properties\":{\"color\":{\"enum\":[\"red\",\"blue\",\"green\"]},\"customer\":{\"type\":\"object\",\"properties\":{\"firstName\":{\"description\":\"Customer first name\",\"type\":\"string\",\"minimum\":1},\"lastName\":{\"description\":\"Customer last name\",\"type\":\"string\",\"minimum\":1},\"email\":{\"description\":\"Customer email\",\"type\":\"string\",\"format\":\"email\"},\"age\":{\"description\":\"Customer age\",\"type\":\"number\",\"minimum\":18}},\"required\":[\"firstName\",\"lastName\",\"email\",\"age\"]}},\"required\":[\"color\",\"customer\"]}"
},
"position": {
"x": 90,
"y": 200
}
},
{
"type": "outputNode",
"id": "27e18970-f565-43eb-859e-568c9f53b7a8",
"name": "response",
"position": {
"x": 510,
"y": 200
}
}
],
"edges": [
{
"id": "4c036317-bc39-4ad2-a825-adbfd4ca2df6",
"sourceId": "4354dede-b4ed-4a57-9b80-45c1e33e2326",
"type": "edge",
"targetId": "27e18970-f565-43eb-859e-568c9f53b7a8"
}
]
}
+34
View File
@@ -0,0 +1,34 @@
{
"contentType": "application/vnd.gorules.decision",
"nodes": [
{
"type": "inputNode",
"id": "4354dede-b4ed-4a57-9b80-45c1e33e2326",
"name": "request",
"position": {
"x": 90,
"y": 200
}
},
{
"type": "outputNode",
"id": "27e18970-f565-43eb-859e-568c9f53b7a8",
"name": "response",
"content": {
"schema": "{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Generated schema for Root\",\"type\":\"object\",\"properties\":{\"color\":{\"enum\":[\"red\",\"blue\",\"green\"]},\"customer\":{\"type\":\"object\",\"properties\":{\"firstName\":{\"description\":\"Customer first name\",\"type\":\"string\",\"minimum\":1},\"lastName\":{\"description\":\"Customer last name\",\"type\":\"string\",\"minimum\":1},\"email\":{\"description\":\"Customer email\",\"type\":\"string\",\"format\":\"email\"},\"age\":{\"description\":\"Customer age\",\"type\":\"number\",\"minimum\":18}},\"required\":[\"firstName\",\"lastName\",\"email\",\"age\"]}},\"required\":[\"color\",\"customer\"]}"
},
"position": {
"x": 510,
"y": 200
}
}
],
"edges": [
{
"id": "4c036317-bc39-4ad2-a825-adbfd4ca2df6",
"sourceId": "4354dede-b4ed-4a57-9b80-45c1e33e2326",
"type": "edge",
"targetId": "27e18970-f565-43eb-859e-568c9f53b7a8"
}
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"contentType": "application/vnd.gorules.decision",
"nodes": [
{
"type": "inputNode",
"id": "4354dede-b4ed-4a57-9b80-45c1e33e2326",
"name": "request",
"position": {
"x": 90,
"y": 200
}
},
{
"type": "outputNode",
"id": "27e18970-f565-43eb-859e-568c9f53b7a8",
"name": "response",
"position": {
"x": 510,
"y": 200
}
}
],
"edges": [
{
"id": "4c036317-bc39-4ad2-a825-adbfd4ca2df6",
"sourceId": "4354dede-b4ed-4a57-9b80-45c1e33e2326",
"type": "edge",
"targetId": "27e18970-f565-43eb-859e-568c9f53b7a8"
}
]
}