Skip to main content

polyspec_crudui_validator/expr/
ast.rs

1//! CRUDUI expression AST nodes (expressions.md §4, JS `ConditionParser.ts` parity).
2//!
3//! `to_value()` emits the canonical fixture shape, byte-matching the JS reference
4//! field names: `Ternary{condition,trueValue,falseValue}`, `Binary{operator,left,
5//! right}`, `Unary{operator,operand}`, `In{negated,value,list}`, `Path{relative,
6//! levelsUp,segments}`, `Literal{valueType,value}`, `Group{expression}`. The
7//! JS/Go `position` field is DELIBERATELY ABSENT — Rust holds no position, so the
8//! 4-language AST contract excludes it; including it would break cross-language
9//! fixture equality.
10
11use serde_json::{json, Value};
12
13/// A literal's value kind plus its concrete value (number kept as int or float so
14/// the fixture's integer literals stay integers when serialized).
15#[derive(Debug, Clone, PartialEq)]
16pub enum LiteralValue {
17    /// `valueType:"string"`.
18    Str(String),
19    /// `valueType:"number"`, integral.
20    Int(i64),
21    /// `valueType:"number"`, fractional.
22    Float(f64),
23    /// `valueType:"boolean"`.
24    Bool(bool),
25    /// `valueType:"null"`.
26    Null,
27}
28
29impl LiteralValue {
30    fn value_type(&self) -> &'static str {
31        match self {
32            LiteralValue::Str(_) => "string",
33            LiteralValue::Int(_) | LiteralValue::Float(_) => "number",
34            LiteralValue::Bool(_) => "boolean",
35            LiteralValue::Null => "null",
36        }
37    }
38
39    pub(crate) fn to_value(&self) -> Value {
40        match self {
41            LiteralValue::Str(s) => Value::String(s.clone()),
42            LiteralValue::Int(n) => json!(n),
43            LiteralValue::Float(f) => json!(f),
44            LiteralValue::Bool(b) => Value::Bool(*b),
45            LiteralValue::Null => Value::Null,
46        }
47    }
48}
49
50/// A single path segment: identifier (field name), wildcard (`*`), or index
51/// (numeric array index). The index value serializes as a number to match the JS
52/// reference segment shape (`{type:'index', value:number}`).
53#[derive(Debug, Clone, PartialEq)]
54pub enum PathSegment {
55    /// A named-field segment.
56    Identifier(String),
57    /// A wildcard (`*`) segment matching any array index.
58    Wildcard,
59    /// A fixed numeric array-index segment.
60    Index(i64),
61}
62
63impl PathSegment {
64    fn to_value(&self) -> Value {
65        match self {
66            PathSegment::Identifier(v) => json!({ "type": "identifier", "value": v }),
67            PathSegment::Wildcard => json!({ "type": "wildcard" }),
68            PathSegment::Index(i) => json!({ "type": "index", "value": i }),
69        }
70    }
71}
72
73/// An expression AST node (expressions.md §4).
74#[derive(Debug, Clone, PartialEq)]
75pub enum Node {
76    /// `condition ? trueValue : falseValue` (right-associative).
77    Ternary {
78        /// The boolean condition.
79        condition: Box<Node>,
80        /// Branch evaluated when the condition is truthy.
81        true_value: Box<Node>,
82        /// Branch evaluated when the condition is falsy.
83        false_value: Box<Node>,
84    },
85    /// Logical (`&& ||`) or comparison (`== != > >= < <=`) operation.
86    Binary {
87        /// The operator spelling.
88        operator: String,
89        /// Left operand.
90        left: Box<Node>,
91        /// Right operand.
92        right: Box<Node>,
93    },
94    /// `! operand`.
95    Unary {
96        /// The operator spelling (`!`).
97        operator: String,
98        /// The negated operand.
99        operand: Box<Node>,
100    },
101    /// `value [not] in list` membership test.
102    In {
103        /// Whether the test is negated (`not in`).
104        negated: bool,
105        /// The probed value.
106        value: Box<Node>,
107        /// Candidate Literal nodes.
108        list: Vec<Node>,
109    },
110    /// A path reference.
111    Path {
112        /// `false` → resolved from the data root; `true` → relative to the field.
113        relative: bool,
114        /// Additional parent ascents beyond the current field's parent.
115        levels_up: i64,
116        /// Ordered path segments.
117        segments: Vec<PathSegment>,
118    },
119    /// A literal value.
120    Literal(LiteralValue),
121    /// A parenthesized group wrapping an inner expression.
122    Group(Box<Node>),
123}
124
125impl Node {
126    /// Serialize to the canonical fixture AST shape (no `position`).
127    pub fn to_value(&self) -> Value {
128        match self {
129            Node::Ternary {
130                condition,
131                true_value,
132                false_value,
133            } => json!({
134                "type": "Ternary",
135                "condition": condition.to_value(),
136                "trueValue": true_value.to_value(),
137                "falseValue": false_value.to_value(),
138            }),
139            Node::Binary {
140                operator,
141                left,
142                right,
143            } => json!({
144                "type": "Binary",
145                "operator": operator,
146                "left": left.to_value(),
147                "right": right.to_value(),
148            }),
149            Node::Unary { operator, operand } => json!({
150                "type": "Unary",
151                "operator": operator,
152                "operand": operand.to_value(),
153            }),
154            Node::In {
155                negated,
156                value,
157                list,
158            } => json!({
159                "type": "In",
160                "negated": negated,
161                "value": value.to_value(),
162                "list": list.iter().map(Node::to_value).collect::<Vec<_>>(),
163            }),
164            Node::Path {
165                relative,
166                levels_up,
167                segments,
168            } => json!({
169                "type": "Path",
170                "relative": relative,
171                "levelsUp": levels_up,
172                "segments": segments.iter().map(PathSegment::to_value).collect::<Vec<_>>(),
173            }),
174            Node::Literal(lit) => json!({
175                "type": "Literal",
176                "valueType": lit.value_type(),
177                "value": lit.to_value(),
178            }),
179            Node::Group(expr) => json!({
180                "type": "Group",
181                "expression": expr.to_value(),
182            }),
183        }
184    }
185}