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}