use crate::v2_model::{
V2Comparison, V2ComparisonOp, V2Condition, V2Expr, V2IfStep, V2LetStep, V2MapStep, V2OpStep,
V2Pipe, V2Ref, V2Start, V2Step,
};
use crate::v2_validator::is_valid_op;
use serde_json::Value as JsonValue;
pub fn parse_v2_ref(s: &str) -> Option<V2Ref> {
if !s.starts_with('@') {
return None;
}
let rest = &s[1..];
if let Some(path) = rest.strip_prefix("input.") {
if path.is_empty() {
return None;
}
return Some(V2Ref::Input(path.to_string()));
}
if let Some(path) = rest.strip_prefix("context.") {
if path.is_empty() {
return None;
}
return Some(V2Ref::Context(path.to_string()));
}
if let Some(path) = rest.strip_prefix("out.") {
if path.is_empty() {
return None;
}
return Some(V2Ref::Out(path.to_string()));
}
if rest == "input" {
return Some(V2Ref::Input(String::new()));
}
if rest == "context" {
return Some(V2Ref::Context(String::new()));
}
if rest == "out" {
return Some(V2Ref::Out(String::new()));
}
if let Some(path) = rest.strip_prefix("item.") {
if path.is_empty() {
return None;
}
return Some(V2Ref::Item(path.to_string()));
}
if let Some(path) = rest.strip_prefix("item") {
if path.is_empty() {
return Some(V2Ref::Item(String::new()));
}
}
if let Some(path) = rest.strip_prefix("acc.") {
if path.is_empty() {
return None;
}
return Some(V2Ref::Acc(path.to_string()));
}
if let Some(path) = rest.strip_prefix("acc") {
if path.is_empty() {
return Some(V2Ref::Acc(String::new()));
}
}
if rest == "input" || rest == "context" || rest == "out" {
return None; }
if is_valid_identifier(rest) {
return Some(V2Ref::Local(rest.to_string()));
}
None
}
fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn is_pipe_value(s: &str) -> bool {
s == "$"
}
pub fn is_literal_escape(s: &str) -> bool {
s.starts_with("lit:")
}
pub fn extract_literal(s: &str) -> Option<&str> {
s.strip_prefix("lit:")
}
pub fn is_v2_ref(s: &str) -> bool {
s.starts_with('@')
}
pub fn parse_v2_start(value: &JsonValue) -> Result<V2Start, V2ParseError> {
match value {
JsonValue::String(s) => {
if is_pipe_value(s) {
return Ok(V2Start::PipeValue);
}
if let Some(lit) = extract_literal(s) {
return Ok(V2Start::Literal(JsonValue::String(lit.to_string())));
}
if let Some(v2_ref) = parse_v2_ref(s) {
return Ok(V2Start::Ref(v2_ref));
}
if is_v2_ref(s) {
return Err(V2ParseError::InvalidStart(format!(
"invalid v2 reference: {}",
s
)));
}
Ok(V2Start::Literal(value.clone()))
}
JsonValue::Null | JsonValue::Bool(_) | JsonValue::Number(_) => {
Ok(V2Start::Literal(value.clone()))
}
JsonValue::Array(_) | JsonValue::Object(_) => {
Ok(V2Start::Literal(value.clone()))
}
}
}
pub fn parse_v2_step(value: &JsonValue) -> Result<V2Step, V2ParseError> {
match value {
JsonValue::Object(obj) => {
if let Some(op_name) = obj.get("op").and_then(|v| v.as_str()) {
let args = if let Some(args_val) = obj.get("args") {
parse_v2_expr_args(args_val)?
} else {
vec![]
};
return Ok(V2Step::Op(V2OpStep {
op: op_name.to_string(),
args,
}));
}
if let Some(let_bindings) = obj.get("let") {
return parse_let_step(let_bindings);
}
if obj.contains_key("if") {
return parse_if_step(obj);
}
if let Some(map_steps) = obj.get("map") {
return parse_map_step(map_steps);
}
if obj.len() == 1 {
let (op_name, args_val) = obj.iter().next().unwrap();
if !["op", "let", "if", "map", "then", "else", "cond"].contains(&op_name.as_str()) {
let args = match args_val {
JsonValue::Array(arr) => arr
.iter()
.map(parse_v2_expr)
.collect::<Result<Vec<_>, _>>()?,
other => vec![parse_v2_expr(other)?],
};
return Ok(V2Step::Op(V2OpStep {
op: op_name.clone(),
args,
}));
}
}
Err(V2ParseError::InvalidStep("unknown step type".to_string()))
}
JsonValue::String(s) => {
if let Some(v2_ref) = parse_v2_ref(s) {
return Ok(V2Step::Ref(v2_ref));
}
if is_pipe_value(s) {
return Err(V2ParseError::InvalidStep(
"$ as a step is not valid, use it as start or in expressions".to_string(),
));
}
Ok(V2Step::Op(V2OpStep {
op: s.clone(),
args: vec![],
}))
}
_ => Err(V2ParseError::InvalidStep(
"step must be object or string".to_string(),
)),
}
}
fn parse_v2_expr_args(value: &JsonValue) -> Result<Vec<V2Expr>, V2ParseError> {
match value {
JsonValue::Array(arr) => arr.iter().map(parse_v2_expr).collect(),
_ => Err(V2ParseError::InvalidArgs(
"args must be an array".to_string(),
)),
}
}
fn parse_let_step(bindings: &JsonValue) -> Result<V2Step, V2ParseError> {
match bindings {
JsonValue::Object(obj) => {
let mut result = Vec::new();
for (key, value) in obj {
let expr = parse_v2_expr(value)?;
result.push((key.clone(), expr));
}
Ok(V2Step::Let(V2LetStep { bindings: result }))
}
_ => Err(V2ParseError::InvalidStep(
"let bindings must be an object".to_string(),
)),
}
}
fn parse_if_step(obj: &serde_json::Map<String, JsonValue>) -> Result<V2Step, V2ParseError> {
let if_val = obj
.get("if")
.ok_or_else(|| V2ParseError::InvalidStep("if step missing 'if' key".to_string()))?;
if let JsonValue::Object(inner_obj) = if_val {
if inner_obj.contains_key("cond") || inner_obj.contains_key("then") {
let cond_val = inner_obj
.get("cond")
.ok_or_else(|| V2ParseError::InvalidStep("if step missing 'cond'".to_string()))?;
let then_val = inner_obj.get("then").ok_or_else(|| {
V2ParseError::InvalidStep("if step missing 'then' branch".to_string())
})?;
let condition = parse_v2_condition(cond_val)?;
let then_branch = parse_v2_pipe_from_value(then_val)?;
let else_branch = if let Some(else_val) = inner_obj.get("else") {
Some(parse_v2_pipe_from_value(else_val)?)
} else {
None
};
return Ok(V2Step::If(V2IfStep {
cond: condition,
then_branch,
else_branch,
}));
}
}
let then_val = obj
.get("then")
.ok_or_else(|| V2ParseError::InvalidStep("if step missing then branch".to_string()))?;
let condition = parse_v2_condition(if_val)?;
let then_branch = parse_v2_pipe_from_value(then_val)?;
let else_branch = if let Some(else_val) = obj.get("else") {
Some(parse_v2_pipe_from_value(else_val)?)
} else {
None
};
Ok(V2Step::If(V2IfStep {
cond: condition,
then_branch,
else_branch,
}))
}
fn parse_map_step(steps: &JsonValue) -> Result<V2Step, V2ParseError> {
match steps {
JsonValue::Array(arr) => {
let parsed_steps: Result<Vec<V2Step>, _> = arr.iter().map(parse_v2_step).collect();
Ok(V2Step::Map(V2MapStep {
steps: parsed_steps?,
}))
}
_ => Err(V2ParseError::InvalidStep(
"map steps must be an array".to_string(),
)),
}
}
pub fn parse_v2_pipe_from_value(value: &JsonValue) -> Result<V2Pipe, V2ParseError> {
match value {
JsonValue::Array(arr) => parse_v2_pipe(arr),
JsonValue::String(_) => {
let start = parse_v2_start(value)?;
Ok(V2Pipe {
start,
steps: vec![],
})
}
_ => {
let start = parse_v2_start(value)?;
Ok(V2Pipe {
start,
steps: vec![],
})
}
}
}
pub fn parse_v2_pipe(arr: &[JsonValue]) -> Result<V2Pipe, V2ParseError> {
if arr.is_empty() {
return Err(V2ParseError::EmptyPipe);
}
if arr.len() == 1 && looks_like_step(&arr[0]) {
let steps: Result<Vec<V2Step>, _> = arr.iter().map(parse_v2_step).collect();
return Ok(V2Pipe {
start: V2Start::PipeValue,
steps: steps?,
});
}
let start = parse_v2_start(&arr[0])?;
let steps: Result<Vec<V2Step>, _> = arr[1..].iter().map(parse_v2_step).collect();
Ok(V2Pipe {
start,
steps: steps?,
})
}
fn looks_like_step(value: &JsonValue) -> bool {
match value {
JsonValue::Object(obj) => {
if obj.contains_key("op")
|| obj.contains_key("let")
|| obj.contains_key("if")
|| obj.contains_key("map")
{
return true;
}
if obj.len() == 1 {
let key = obj.keys().next().unwrap();
if !["op", "let", "if", "map", "then", "else", "cond", "ref"]
.contains(&key.as_str())
{
return is_valid_op(key);
}
}
false
}
JsonValue::String(_) => {
false
}
_ => false,
}
}
pub fn parse_v2_expr(value: &JsonValue) -> Result<V2Expr, V2ParseError> {
match value {
JsonValue::Array(arr) => {
let pipe = parse_v2_pipe(arr)?;
Ok(V2Expr::Pipe(pipe))
}
JsonValue::String(s) => {
if is_pipe_value(s) {
Ok(V2Expr::Pipe(V2Pipe {
start: V2Start::PipeValue,
steps: vec![],
}))
} else if let Some(lit) = extract_literal(s) {
Ok(V2Expr::Pipe(V2Pipe {
start: V2Start::Literal(JsonValue::String(lit.to_string())),
steps: vec![],
}))
} else if let Some(v2_ref) = parse_v2_ref(s) {
Ok(V2Expr::Pipe(V2Pipe {
start: V2Start::Ref(v2_ref),
steps: vec![],
}))
} else if is_v2_ref(s) {
Err(V2ParseError::InvalidStart(format!(
"invalid v2 reference: {}",
s
)))
} else {
Ok(V2Expr::Pipe(V2Pipe {
start: V2Start::Literal(value.clone()),
steps: vec![],
}))
}
}
_ => {
Ok(V2Expr::Pipe(V2Pipe {
start: V2Start::Literal(value.clone()),
steps: vec![],
}))
}
}
}
pub fn parse_v2_condition(value: &JsonValue) -> Result<V2Condition, V2ParseError> {
match value {
JsonValue::Object(obj) => {
if let Some(all_arr) = obj.get("all") {
return parse_condition_array(all_arr, |conds| V2Condition::All(conds));
}
if let Some(any_arr) = obj.get("any") {
return parse_condition_array(any_arr, |conds| V2Condition::Any(conds));
}
if let Some(comp) = parse_comparison_from_object(obj)? {
return Ok(V2Condition::Comparison(comp));
}
let expr = parse_v2_expr(value)?;
Ok(V2Condition::Expr(expr))
}
JsonValue::Array(_) => {
let expr = parse_v2_expr(value)?;
Ok(V2Condition::Expr(expr))
}
_ => {
let expr = parse_v2_expr(value)?;
Ok(V2Condition::Expr(expr))
}
}
}
fn parse_condition_array<F>(value: &JsonValue, constructor: F) -> Result<V2Condition, V2ParseError>
where
F: FnOnce(Vec<V2Condition>) -> V2Condition,
{
match value {
JsonValue::Array(arr) => {
let conditions: Result<Vec<V2Condition>, _> =
arr.iter().map(parse_v2_condition).collect();
Ok(constructor(conditions?))
}
_ => Err(V2ParseError::InvalidCondition(
"all/any must contain an array".to_string(),
)),
}
}
fn parse_comparison_from_object(
obj: &serde_json::Map<String, JsonValue>,
) -> Result<Option<V2Comparison>, V2ParseError> {
let ops = [
("eq", V2ComparisonOp::Eq),
("ne", V2ComparisonOp::Ne),
("gt", V2ComparisonOp::Gt),
("gte", V2ComparisonOp::Gte),
("lt", V2ComparisonOp::Lt),
("lte", V2ComparisonOp::Lte),
("match", V2ComparisonOp::Match),
];
for (key, op) in ops.iter() {
if let Some(args_val) = obj.get(*key) {
let args = parse_v2_expr_args(args_val)?;
return Ok(Some(V2Comparison { op: *op, args }));
}
}
Ok(None)
}
#[derive(Debug, Clone, PartialEq)]
pub enum V2ParseError {
EmptyPipe,
InvalidStart(String),
InvalidStep(String),
InvalidArgs(String),
InvalidCondition(String),
}
impl std::fmt::Display for V2ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
V2ParseError::EmptyPipe => write!(f, "pipe array cannot be empty"),
V2ParseError::InvalidStart(msg) => write!(f, "invalid start value: {}", msg),
V2ParseError::InvalidStep(msg) => write!(f, "invalid step: {}", msg),
V2ParseError::InvalidArgs(msg) => write!(f, "invalid args: {}", msg),
V2ParseError::InvalidCondition(msg) => write!(f, "invalid condition: {}", msg),
}
}
}
impl std::error::Error for V2ParseError {}
#[cfg(test)]
mod v2_ref_parser_tests {
use super::*;
#[test]
fn test_parse_input_ref() {
assert_eq!(
parse_v2_ref("@input.name"),
Some(V2Ref::Input("name".to_string()))
);
assert_eq!(
parse_v2_ref("@input.user.profile.name"),
Some(V2Ref::Input("user.profile.name".to_string()))
);
assert_eq!(
parse_v2_ref("@input.items[0].id"),
Some(V2Ref::Input("items[0].id".to_string()))
);
assert_eq!(parse_v2_ref("@input"), Some(V2Ref::Input(String::new())));
}
#[test]
fn test_parse_context_ref() {
assert_eq!(
parse_v2_ref("@context.config"),
Some(V2Ref::Context("config".to_string()))
);
assert_eq!(
parse_v2_ref("@context.users[0].id"),
Some(V2Ref::Context("users[0].id".to_string()))
);
assert_eq!(
parse_v2_ref("@context"),
Some(V2Ref::Context(String::new()))
);
}
#[test]
fn test_parse_out_ref() {
assert_eq!(
parse_v2_ref("@out.user_id"),
Some(V2Ref::Out("user_id".to_string()))
);
assert_eq!(
parse_v2_ref("@out.computed_field"),
Some(V2Ref::Out("computed_field".to_string()))
);
assert_eq!(parse_v2_ref("@out"), Some(V2Ref::Out(String::new())));
}
#[test]
fn test_parse_item_ref() {
assert_eq!(
parse_v2_ref("@item.value"),
Some(V2Ref::Item("value".to_string()))
);
assert_eq!(parse_v2_ref("@item"), Some(V2Ref::Item(String::new())));
}
#[test]
fn test_parse_acc_ref() {
assert_eq!(
parse_v2_ref("@acc.total"),
Some(V2Ref::Acc("total".to_string()))
);
assert_eq!(parse_v2_ref("@acc"), Some(V2Ref::Acc(String::new())));
}
#[test]
fn test_parse_local_ref() {
assert_eq!(
parse_v2_ref("@myVar"),
Some(V2Ref::Local("myVar".to_string()))
);
assert_eq!(
parse_v2_ref("@price"),
Some(V2Ref::Local("price".to_string()))
);
assert_eq!(
parse_v2_ref("@_temp"),
Some(V2Ref::Local("_temp".to_string()))
);
assert_eq!(
parse_v2_ref("@var123"),
Some(V2Ref::Local("var123".to_string()))
);
}
#[test]
fn test_invalid_refs() {
assert_eq!(parse_v2_ref("input.name"), None);
assert_eq!(parse_v2_ref("@"), None);
assert_eq!(parse_v2_ref("@input."), None);
assert_eq!(parse_v2_ref("@context."), None);
assert_eq!(parse_v2_ref("@out."), None);
assert_eq!(parse_v2_ref("@item."), None);
assert_eq!(parse_v2_ref("@acc."), None);
assert_eq!(parse_v2_ref("@123invalid"), None);
}
#[test]
fn test_is_pipe_value() {
assert!(is_pipe_value("$"));
assert!(!is_pipe_value("$$"));
assert!(!is_pipe_value("@input.name"));
assert!(!is_pipe_value(""));
}
#[test]
fn test_is_literal_escape() {
assert!(is_literal_escape("lit:@input.name"));
assert!(is_literal_escape("lit:$"));
assert!(is_literal_escape("lit:"));
assert!(!is_literal_escape("@input.name"));
assert!(!is_literal_escape("literal:"));
}
#[test]
fn test_extract_literal() {
assert_eq!(extract_literal("lit:@input.name"), Some("@input.name"));
assert_eq!(extract_literal("lit:$"), Some("$"));
assert_eq!(extract_literal("lit:"), Some(""));
assert_eq!(extract_literal("@input.name"), None);
}
#[test]
fn test_is_v2_ref() {
assert!(is_v2_ref("@input.name"));
assert!(is_v2_ref("@myVar"));
assert!(!is_v2_ref("input.name"));
assert!(!is_v2_ref("$"));
assert!(!is_v2_ref("lit:@input"));
}
}
#[cfg(test)]
mod v2_pipe_parser_tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_simple_pipe() {
let arr = vec![json!("@input.name"), json!("trim")];
let pipe = parse_v2_pipe(&arr).unwrap();
assert_eq!(pipe.start, V2Start::Ref(V2Ref::Input("name".to_string())));
assert_eq!(pipe.steps.len(), 1);
if let V2Step::Op(op) = &pipe.steps[0] {
assert_eq!(op.op, "trim");
assert!(op.args.is_empty());
} else {
panic!("Expected Op step");
}
}
#[test]
fn test_parse_pipe_with_multiple_steps() {
let arr = vec![json!("@input.name"), json!("trim"), json!("uppercase")];
let pipe = parse_v2_pipe(&arr).unwrap();
assert_eq!(pipe.steps.len(), 2);
}
#[test]
fn test_parse_pipe_with_op_object() {
let arr = vec![json!("@input.value"), json!({ "op": "add", "args": [10] })];
let pipe = parse_v2_pipe(&arr).unwrap();
if let V2Step::Op(op) = &pipe.steps[0] {
assert_eq!(op.op, "add");
assert_eq!(op.args.len(), 1);
} else {
panic!("Expected Op step");
}
}
#[test]
fn test_parse_pipe_with_pipe_value_start() {
let arr = vec![json!("$"), json!("trim")];
let pipe = parse_v2_pipe(&arr).unwrap();
assert_eq!(pipe.start, V2Start::PipeValue);
}
#[test]
fn test_parse_pipe_with_literal_start() {
let arr = vec![json!(42), json!({ "op": "multiply", "args": [2] })];
let pipe = parse_v2_pipe(&arr).unwrap();
assert_eq!(pipe.start, V2Start::Literal(json!(42)));
}
#[test]
fn test_parse_pipe_with_literal_escape() {
let arr = vec![json!("lit:@input.name"), json!("trim")];
let pipe = parse_v2_pipe(&arr).unwrap();
assert_eq!(pipe.start, V2Start::Literal(json!("@input.name")));
}
#[test]
fn test_parse_empty_pipe_error() {
let arr: Vec<JsonValue> = vec![];
let result = parse_v2_pipe(&arr);
assert_eq!(result, Err(V2ParseError::EmptyPipe));
}
#[test]
fn test_parse_v2_start_ref() {
let result = parse_v2_start(&json!("@input.name")).unwrap();
assert_eq!(result, V2Start::Ref(V2Ref::Input("name".to_string())));
}
#[test]
fn test_parse_v2_start_pipe_value() {
let result = parse_v2_start(&json!("$")).unwrap();
assert_eq!(result, V2Start::PipeValue);
}
#[test]
fn test_parse_v2_start_literal() {
let result = parse_v2_start(&json!(123)).unwrap();
assert_eq!(result, V2Start::Literal(json!(123)));
let result = parse_v2_start(&json!(true)).unwrap();
assert_eq!(result, V2Start::Literal(json!(true)));
let result = parse_v2_start(&json!(null)).unwrap();
assert_eq!(result, V2Start::Literal(json!(null)));
}
#[test]
fn test_parse_v2_start_invalid_at_ref_error() {
let invalid_refs = [json!("@"), json!("@foo-bar"), json!("@123invalid")];
for value in invalid_refs {
let err = parse_v2_start(&value).unwrap_err();
assert!(matches!(err, V2ParseError::InvalidStart(_)));
}
}
}
#[cfg(test)]
mod v2_condition_parser_tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_condition_all() {
let value = json!({
"all": [
{ "eq": ["@input.status", "active"] },
{ "gt": ["@input.age", 18] }
]
});
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::All(conditions) = cond {
assert_eq!(conditions.len(), 2);
} else {
panic!("Expected All condition");
}
}
#[test]
fn test_parse_condition_any() {
let value = json!({
"any": [
{ "eq": ["@input.role", "admin"] },
{ "eq": ["@input.role", "moderator"] }
]
});
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Any(conditions) = cond {
assert_eq!(conditions.len(), 2);
} else {
panic!("Expected Any condition");
}
}
#[test]
fn test_parse_condition_eq() {
let value = json!({ "eq": ["@input.name", "John"] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Eq);
assert_eq!(comp.args.len(), 2);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_condition_ne() {
let value = json!({ "ne": ["@input.status", "deleted"] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Ne);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_condition_gt() {
let value = json!({ "gt": ["@input.age", 18] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Gt);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_condition_gte() {
let value = json!({ "gte": ["@input.score", 60] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Gte);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_condition_lt() {
let value = json!({ "lt": ["@input.count", 100] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Lt);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_condition_lte() {
let value = json!({ "lte": ["@input.retries", 3] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Lte);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_condition_match() {
let value = json!({ "match": ["@input.email", "^[a-z]+@"] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Match);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_nested_conditions() {
let value = json!({
"all": [
{ "any": [
{ "eq": ["@input.type", "A"] },
{ "eq": ["@input.type", "B"] }
]},
{ "gt": ["@input.value", 0] }
]
});
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::All(conditions) = cond {
assert_eq!(conditions.len(), 2);
assert!(matches!(conditions[0], V2Condition::Any(_)));
} else {
panic!("Expected All condition");
}
}
}
#[cfg(test)]
mod v2_step_parser_tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_let_step() {
let value = json!({
"let": {
"x": "@input.value",
"y": 10
}
});
let step = parse_v2_step(&value).unwrap();
if let V2Step::Let(let_step) = step {
assert_eq!(let_step.bindings.len(), 2);
} else {
panic!("Expected Let step");
}
}
#[test]
fn test_parse_if_step() {
let value = json!({
"if": { "gt": ["@input.age", 18] },
"then": ["adult"],
"else": ["minor"]
});
let step = parse_v2_step(&value).unwrap();
if let V2Step::If(if_step) = step {
assert!(matches!(if_step.cond, V2Condition::Comparison(_)));
assert!(if_step.else_branch.is_some());
} else {
panic!("Expected If step");
}
}
#[test]
fn test_parse_if_step_without_else() {
let value = json!({
"if": { "eq": ["@input.enabled", true] },
"then": ["process"]
});
let step = parse_v2_step(&value).unwrap();
if let V2Step::If(if_step) = step {
assert!(if_step.else_branch.is_none());
} else {
panic!("Expected If step");
}
}
#[test]
fn test_parse_map_step() {
let value = json!({
"map": [
{ "op": "multiply", "args": [2] }
]
});
let step = parse_v2_step(&value).unwrap();
if let V2Step::Map(map_step) = step {
assert_eq!(map_step.steps.len(), 1);
} else {
panic!("Expected Map step");
}
}
#[test]
fn test_parse_op_step_shorthand() {
let step = parse_v2_step(&json!("trim")).unwrap();
if let V2Step::Op(op) = step {
assert_eq!(op.op, "trim");
assert!(op.args.is_empty());
} else {
panic!("Expected Op step");
}
}
#[test]
fn test_parse_op_step_with_args() {
let value = json!({
"op": "concat",
"args": ["@input.first", " ", "@input.last"]
});
let step = parse_v2_step(&value).unwrap();
if let V2Step::Op(op) = step {
assert_eq!(op.op, "concat");
assert_eq!(op.args.len(), 3);
} else {
panic!("Expected Op step");
}
}
#[test]
fn test_parse_complex_pipe_with_steps() {
let arr = vec![
json!("@input.items"),
json!({ "let": { "threshold": 100 } }),
json!({ "map": [
{ "if": { "gt": ["@item.value", "@threshold"] },
"then": ["@item.value"],
"else": [0]
}
]}),
];
let pipe = parse_v2_pipe(&arr).unwrap();
assert_eq!(pipe.steps.len(), 2);
assert!(matches!(pipe.steps[0], V2Step::Let(_)));
assert!(matches!(pipe.steps[1], V2Step::Map(_)));
}
}
#[cfg(test)]
mod v2_rulefile_parser_tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_v2_expr_from_yaml_array() {
let value = json!(["@input.name", "trim", "uppercase"]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.start, V2Start::Ref(V2Ref::Input("name".to_string())));
assert_eq!(pipe.steps.len(), 2);
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_expr_with_op_args() {
let value = json!(["@input.price", { "op": "multiply", "args": [0.9] }]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.steps.len(), 1);
if let V2Step::Op(op) = &pipe.steps[0] {
assert_eq!(op.op, "multiply");
assert_eq!(op.args.len(), 1);
} else {
panic!("Expected Op step");
}
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_expr_literal_object_start_pipe() {
let value = json!([{"foo": 1}, "keys"]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.start, V2Start::Literal(json!({"foo": 1})));
assert_eq!(pipe.steps.len(), 1);
if let V2Step::Op(op) = &pipe.steps[0] {
assert_eq!(op.op, "keys");
} else {
panic!("Expected Op step");
}
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_expr_literal_object_with_op_key_start_pipe() {
let value = json!([{"op": "x"}, "keys"]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.start, V2Start::Literal(json!({"op": "x"})));
assert_eq!(pipe.steps.len(), 1);
if let V2Step::Op(op) = &pipe.steps[0] {
assert_eq!(op.op, "keys");
} else {
panic!("Expected Op step");
}
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_condition_from_record_when() {
let value = json!({
"all": [
{ "gt": ["@input.score", 0] },
{ "eq": ["@input.active", true] }
]
});
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::All(conditions) = cond {
assert_eq!(conditions.len(), 2);
} else {
panic!("Expected All condition");
}
}
#[test]
fn test_parse_v2_expr_single_ref() {
let value = json!("@input.name");
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.start, V2Start::Ref(V2Ref::Input("name".to_string())));
assert!(pipe.steps.is_empty());
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_expr_invalid_at_ref_error() {
let value = json!("@foo-bar");
let err = parse_v2_expr(&value).unwrap_err();
assert!(matches!(err, V2ParseError::InvalidStart(_)));
}
#[test]
fn test_parse_v2_expr_v1_fallback_op() {
let value = json!(["@input.name", { "op": "uppercase", "args": [] }]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
if let V2Step::Op(op) = &pipe.steps[0] {
assert_eq!(op.op, "uppercase");
} else {
panic!("Expected Op step");
}
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_expr_single_step_comparison_alias() {
let value = json!([{ "gt": 80 }]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.start, V2Start::PipeValue);
assert_eq!(pipe.steps.len(), 1);
if let V2Step::Op(op) = &pipe.steps[0] {
assert_eq!(op.op, "gt");
assert_eq!(op.args.len(), 1);
} else {
panic!("Expected Op step");
}
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_mapping_when_condition() {
let value = json!({ "eq": ["@input.role", "admin"] });
let cond = parse_v2_condition(&value).unwrap();
if let V2Condition::Comparison(comp) = cond {
assert_eq!(comp.op, V2ComparisonOp::Eq);
assert_eq!(comp.args.len(), 2);
} else {
panic!("Expected Comparison condition");
}
}
#[test]
fn test_parse_v2_expr_with_let_step() {
let value = json!([
"@input.price",
{ "let": { "base": "$" } },
{ "op": "add", "args": [100] }
]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.steps.len(), 2);
assert!(matches!(pipe.steps[0], V2Step::Let(_)));
assert!(matches!(pipe.steps[1], V2Step::Op(_)));
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_expr_with_if_step() {
let value = json!([
"@input.amount",
{
"if": { "gt": ["$", 10000] },
"then": [{ "op": "multiply", "args": [0.9] }],
"else": ["$"]
}
]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.steps.len(), 1);
assert!(matches!(pipe.steps[0], V2Step::If(_)));
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_parse_v2_expr_with_map_step() {
let value = json!([
"@input.items",
{
"map": [
{ "op": "get", "args": ["name"] }
]
}
]);
let expr = parse_v2_expr(&value).unwrap();
if let V2Expr::Pipe(pipe) = expr {
assert_eq!(pipe.steps.len(), 1);
assert!(matches!(pipe.steps[0], V2Step::Map(_)));
} else {
panic!("Expected Pipe expression");
}
}
#[test]
fn test_is_v2_expr_pipe_array() {
assert!(is_v2_expr(&json!(["@input.name", "trim"])));
assert!(is_v2_expr(&json!([])));
assert!(is_v2_expr(&json!(["hello", "trim"])));
assert!(is_v2_expr(&json!([{"lookup_first": []}, "trim"])));
assert!(is_v2_expr(&json!("@input.name")));
assert!(is_v2_expr(&json!("lit:@input.name")));
assert!(!is_v2_expr(&json!({ "ref": "input.name" })));
assert!(!is_v2_expr(&json!({ "op": "uppercase", "args": [] })));
}
}
pub fn is_v2_expr(value: &JsonValue) -> bool {
match value {
JsonValue::Array(_) => {
true
}
JsonValue::String(s) => {
is_v2_ref(s) || is_pipe_value(s) || is_literal_escape(s)
}
JsonValue::Object(obj) => {
!(obj.contains_key("ref") || (obj.contains_key("op") && !obj.contains_key("if")))
}
_ => false,
}
}