mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 08:02:28 +00:00
232 lines
6.7 KiB
Rust
232 lines
6.7 KiB
Rust
use serde_json::json;
|
|
use zen_engine::policy::{
|
|
Cursor, CursorTarget, ExpressionKind, NlExpression, PolicyDocument, PolicyWorkspace,
|
|
};
|
|
use zen_expression::nl::{EditHint, NlTokenKind, OpSym, TypeTag};
|
|
|
|
fn workspace() -> PolicyWorkspace {
|
|
let doc: PolicyDocument = serde_json::from_value(json!({
|
|
"blocks": [
|
|
{
|
|
"id": "dm",
|
|
"type": "dataModel",
|
|
"props": { "data": {
|
|
"name": "customer",
|
|
"properties": [
|
|
{ "id": "p1", "name": "age", "type": "number", "array": false, "optional": false },
|
|
{ "id": "p2", "name": "tier", "type": "string", "enum": ["gold", "silver", "bronze"], "array": false, "optional": false }
|
|
]
|
|
} },
|
|
"children": []
|
|
},
|
|
{
|
|
"id": "assert1",
|
|
"type": "assertion",
|
|
"props": { "data": {
|
|
"output": "customer.isAdult",
|
|
"conditions": [
|
|
{ "id": "c1", "expression": "customer.age >= 18", "operator": "and", "depth": 0 }
|
|
]
|
|
} },
|
|
"children": []
|
|
},
|
|
{
|
|
"id": "dt1",
|
|
"type": "decisionTable",
|
|
"props": { "data": {
|
|
"hitPolicy": "first",
|
|
"inputs": [
|
|
{ "id": "in1", "name": "Age", "field": "customer.age" },
|
|
{ "id": "in2", "name": "Tier", "field": "customer.tier" }
|
|
],
|
|
"outputs": [ { "id": "out1", "name": "Tag", "field": "customer.tag" } ],
|
|
"rules": [ { "_id": "row1", "in1": "> 18", "in2": "'gold'", "out1": "'vip'" } ]
|
|
} },
|
|
"children": []
|
|
}
|
|
]
|
|
}))
|
|
.expect("valid policy fixture");
|
|
|
|
let mut ws = PolicyWorkspace::new();
|
|
ws.set_policy("policy", doc);
|
|
ws
|
|
}
|
|
|
|
fn find<'a>(results: &'a [NlExpression], pred: impl Fn(&NlExpression) -> bool) -> &'a NlExpression {
|
|
results
|
|
.iter()
|
|
.find(|e| pred(e))
|
|
.expect("expression present")
|
|
}
|
|
|
|
fn kinds(e: &NlExpression) -> Vec<NlTokenKind> {
|
|
e.result.tokens.iter().map(|t| t.token.clone()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn assertion_condition_resolves_field_type() {
|
|
let ws = workspace();
|
|
let results = ws.nl("policy");
|
|
|
|
let condition = find(
|
|
&results,
|
|
|e| matches!(&e.target, CursorTarget::Expression { id } if id.as_ref() == "c1"),
|
|
);
|
|
|
|
assert_eq!(condition.kind, ExpressionKind::Standard);
|
|
assert_eq!(condition.block_id.as_ref(), "assert1");
|
|
assert_eq!(
|
|
kinds(condition),
|
|
vec![
|
|
NlTokenKind::Field {
|
|
path: vec!["customer".into(), "age".into()],
|
|
ty: TypeTag::Number,
|
|
},
|
|
NlTokenKind::Op {
|
|
sym: OpSym::Gte,
|
|
implied: false,
|
|
between: false,
|
|
},
|
|
NlTokenKind::Number { value: "18".into() },
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decision_table_input_cell_is_unary_with_resolved_subject() {
|
|
let ws = workspace();
|
|
let results = ws.nl("policy");
|
|
|
|
let cell = find(&results, |e| {
|
|
matches!(
|
|
&e.target,
|
|
CursorTarget::DecisionTableCell { row, col }
|
|
if row.as_ref() == "row1" && col.as_ref() == "in1"
|
|
)
|
|
});
|
|
|
|
assert_eq!(cell.kind, ExpressionKind::Unary);
|
|
let k = kinds(cell);
|
|
assert_eq!(
|
|
k[0],
|
|
NlTokenKind::Op {
|
|
sym: OpSym::Gt,
|
|
implied: true,
|
|
between: false,
|
|
}
|
|
);
|
|
assert_eq!(k[1], NlTokenKind::Number { value: "18".into() });
|
|
}
|
|
|
|
#[test]
|
|
fn decision_table_output_cell_is_standard() {
|
|
let ws = workspace();
|
|
let results = ws.nl("policy");
|
|
|
|
let cell = find(&results, |e| {
|
|
matches!(
|
|
&e.target,
|
|
CursorTarget::DecisionTableCell { row, col }
|
|
if row.as_ref() == "row1" && col.as_ref() == "out1"
|
|
)
|
|
});
|
|
|
|
assert_eq!(cell.kind, ExpressionKind::Standard);
|
|
assert!(matches!(kinds(cell).as_slice(), [NlTokenKind::Str { .. }]));
|
|
}
|
|
|
|
fn cell_cursor(col: &str) -> Cursor {
|
|
Cursor {
|
|
policy_path: "policy".into(),
|
|
block_id: "dt1".into(),
|
|
pos: 0,
|
|
target: CursorTarget::DecisionTableCell {
|
|
row: "row1".into(),
|
|
col: col.into(),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nl_tokenize_resolves_unary_enum_subject() {
|
|
let ws = workspace();
|
|
let result = ws
|
|
.nl_tokenize(&cell_cursor("in2"), "'gold'")
|
|
.expect("cursor resolves");
|
|
|
|
let str_tok = result
|
|
.tokens
|
|
.iter()
|
|
.find(|t| matches!(t.token, NlTokenKind::Str { .. }))
|
|
.expect("string token present");
|
|
assert_eq!(str_tok.hint, Some(EditHint::Select { options: 0 }));
|
|
let labels: Vec<&str> = result.enums[0].iter().map(|o| o.label.as_str()).collect();
|
|
assert_eq!(labels, vec!["gold", "silver", "bronze"]);
|
|
assert!(result.enums[0]
|
|
.iter()
|
|
.all(|o| o.source.as_deref() == Some(format!("\"{}\"", o.label).as_str())));
|
|
}
|
|
|
|
#[test]
|
|
fn nl_tokenize_projects_live_text_over_stored_cell() {
|
|
let ws = workspace();
|
|
let result = ws
|
|
.nl_tokenize(&cell_cursor("in1"), "< 21")
|
|
.expect("cursor resolves");
|
|
|
|
let tokens: Vec<NlTokenKind> = result.tokens.iter().map(|t| t.token.clone()).collect();
|
|
assert_eq!(
|
|
tokens[0],
|
|
NlTokenKind::Op {
|
|
sym: OpSym::Lt,
|
|
implied: true,
|
|
between: false,
|
|
}
|
|
);
|
|
assert_eq!(tokens[1], NlTokenKind::Number { value: "21".into() });
|
|
}
|
|
|
|
#[test]
|
|
fn nl_tokenize_standard_expression_uses_policy_scope() {
|
|
let ws = workspace();
|
|
let cursor = Cursor {
|
|
policy_path: "policy".into(),
|
|
block_id: "assert1".into(),
|
|
pos: 0,
|
|
target: CursorTarget::Expression { id: "c1".into() },
|
|
};
|
|
let result = ws
|
|
.nl_tokenize(&cursor, "customer.age >= 21")
|
|
.expect("cursor resolves");
|
|
|
|
let tokens: Vec<NlTokenKind> = result.tokens.iter().map(|t| t.token.clone()).collect();
|
|
assert_eq!(
|
|
tokens[0],
|
|
NlTokenKind::Field {
|
|
path: vec!["customer".into(), "age".into()],
|
|
ty: TypeTag::Number,
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn decision_table_input_head_is_projected() {
|
|
let ws = workspace();
|
|
let results = ws.nl("policy");
|
|
|
|
let head = find(
|
|
&results,
|
|
|e| matches!(&e.target, CursorTarget::DecisionTableHead { col } if col.as_ref() == "in1"),
|
|
);
|
|
|
|
assert_eq!(head.kind, ExpressionKind::Standard);
|
|
assert_eq!(
|
|
kinds(head),
|
|
vec![NlTokenKind::Field {
|
|
path: vec!["customer".into(), "age".into()],
|
|
ty: TypeTag::Number,
|
|
}]
|
|
);
|
|
}
|