Skip to main content

wowlab_types/types/sim/rotation/
condition.rs

1//! Composable condition syntax and field-read operators.
2
3use serde::{Deserialize, Serialize};
4
5use super::super::AuraOn;
6
7/// Leaf node that reads a single field from the runtime buffer.
8#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
9#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
10#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12pub struct FieldRead {
13    pub domain: String,
14    pub name: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub key: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub on: Option<AuraOn>,
19}
20
21/// Binary comparison operator between two numeric condition subtrees.
22#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
24#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
25#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
26#[serde(rename_all = "snake_case")]
27#[non_exhaustive]
28pub enum CompareOp {
29    Gt,
30    Gte,
31    Lt,
32    Lte,
33    Eq,
34    Ne,
35}
36
37/// Binary arithmetic operator for combining numeric condition subtrees.
38#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
39#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
40#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[serde(rename_all = "snake_case")]
43#[non_exhaustive]
44pub enum ArithOp {
45    Add,
46    Sub,
47    Mul,
48    Div,
49    Mod,
50}
51
52/// Unary math function applied to a numeric condition subtree.
53#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
54#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
55#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[serde(rename_all = "snake_case")]
58#[non_exhaustive]
59pub enum UnaryMathOp {
60    Floor,
61    Ceil,
62    Abs,
63}
64
65/// Binary selection operator picking the smaller or larger of two subtrees.
66#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
67#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
68#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
69#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
70#[serde(rename_all = "snake_case")]
71#[non_exhaustive]
72pub enum MinMaxOp {
73    Min,
74    Max,
75}
76
77impl CompareOp {
78    /// Human-readable operator symbol (e.g. `">="`).
79    #[must_use]
80    pub fn symbol(self) -> &'static str {
81        match self {
82            CompareOp::Gt => ">",
83            CompareOp::Gte => ">=",
84            CompareOp::Lt => "<",
85            CompareOp::Lte => "<=",
86            CompareOp::Eq => "==",
87            CompareOp::Ne => "!=",
88        }
89    }
90}
91
92impl std::fmt::Display for CompareOp {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        f.write_str(self.symbol())
95    }
96}
97
98impl ArithOp {
99    /// Human-readable operator symbol (e.g. `"+"`).
100    #[must_use]
101    pub fn symbol(self) -> &'static str {
102        match self {
103            ArithOp::Add => "+",
104            ArithOp::Sub => "-",
105            ArithOp::Mul => "*",
106            ArithOp::Div => "/",
107            ArithOp::Mod => "%",
108        }
109    }
110}
111
112impl std::fmt::Display for ArithOp {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.write_str(self.symbol())
115    }
116}
117
118impl UnaryMathOp {
119    /// Function name (e.g. `"floor"`).
120    #[must_use]
121    pub fn symbol(self) -> &'static str {
122        match self {
123            UnaryMathOp::Floor => "floor",
124            UnaryMathOp::Ceil => "ceil",
125            UnaryMathOp::Abs => "abs",
126        }
127    }
128}
129
130impl std::fmt::Display for UnaryMathOp {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.write_str(self.symbol())
133    }
134}
135
136impl MinMaxOp {
137    /// Function name (e.g. `"min"`).
138    #[must_use]
139    pub fn symbol(self) -> &'static str {
140        match self {
141            MinMaxOp::Min => "min",
142            MinMaxOp::Max => "max",
143        }
144    }
145}
146
147impl std::fmt::Display for MinMaxOp {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.write_str(self.symbol())
150    }
151}
152
153/// Composable condition tree used by authored rotations.
154#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
155#[serde(tag = "type", rename_all = "snake_case")]
156#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
157#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
159#[non_exhaustive]
160pub enum Condition {
161    Read {
162        #[serde(flatten)]
163        field: FieldRead,
164    },
165    Bool {
166        value: bool,
167    },
168    Int {
169        value: i64,
170    },
171    Float {
172        value: f64,
173    },
174    Var {
175        name: String,
176    },
177    Compare {
178        op: CompareOp,
179        left: Box<Condition>,
180        right: Box<Condition>,
181    },
182    And {
183        operands: Vec<Condition>,
184    },
185    Or {
186        operands: Vec<Condition>,
187    },
188    Not {
189        operand: Box<Condition>,
190    },
191    Arith {
192        op: ArithOp,
193        left: Box<Condition>,
194        right: Box<Condition>,
195    },
196    UnaryMath {
197        op: UnaryMathOp,
198        operand: Box<Condition>,
199    },
200    MinMax {
201        op: MinMaxOp,
202        left: Box<Condition>,
203        right: Box<Condition>,
204    },
205    IfThenElse {
206        condition: Box<Condition>,
207        then: Box<Condition>,
208        otherwise: Box<Condition>,
209    },
210}
211
212#[derive(Debug)]
213#[non_exhaustive]
214enum ChildIter<'a> {
215    Empty,
216    One(std::iter::Once<&'a Condition>),
217    Pair(std::array::IntoIter<&'a Condition, 2>),
218    Triple(std::array::IntoIter<&'a Condition, 3>),
219    Many(std::slice::Iter<'a, Condition>),
220}
221
222impl<'a> Iterator for ChildIter<'a> {
223    type Item = &'a Condition;
224
225    fn next(&mut self) -> Option<Self::Item> {
226        match self {
227            ChildIter::Empty => None,
228            ChildIter::One(iter) => iter.next(),
229            ChildIter::Pair(iter) => iter.next(),
230            ChildIter::Triple(iter) => iter.next(),
231            ChildIter::Many(iter) => iter.next(),
232        }
233    }
234}
235
236impl Condition {
237    /// Iterator over this node's direct child conditions.
238    pub fn children(&self) -> impl Iterator<Item = &Condition> {
239        match self {
240            Condition::Compare { left, right, .. }
241            | Condition::Arith { left, right, .. }
242            | Condition::MinMax { left, right, .. } => {
243                ChildIter::Pair([left.as_ref(), right.as_ref()].into_iter())
244            }
245            Condition::And { operands } | Condition::Or { operands } => {
246                ChildIter::Many(operands.iter())
247            }
248            Condition::Not { operand } | Condition::UnaryMath { operand, .. } => {
249                ChildIter::One(std::iter::once(operand.as_ref()))
250            }
251            Condition::IfThenElse {
252                condition,
253                then,
254                otherwise,
255            } => ChildIter::Triple(
256                [condition.as_ref(), then.as_ref(), otherwise.as_ref()].into_iter(),
257            ),
258            Condition::Read { .. }
259            | Condition::Bool { .. }
260            | Condition::Int { .. }
261            | Condition::Float { .. }
262            | Condition::Var { .. } => ChildIter::Empty,
263        }
264    }
265
266    /// Visits every node in the tree (pre-order), iteratively to avoid stack overflow on WASM.
267    pub fn walk<'a>(&'a self, visitor: &mut impl FnMut(&'a Condition)) {
268        self.walk_with_depth(&mut |condition, _depth| visitor(condition));
269    }
270
271    /// Visits every node and its one-based depth in pre-order.
272    pub fn walk_with_depth<'a>(&'a self, visitor: &mut impl FnMut(&'a Condition, usize)) {
273        let mut stack: Vec<(&Condition, usize)> = vec![(self, 1)];
274
275        while let Some((node, depth)) = stack.pop() {
276            visitor(node, depth);
277            stack.extend(node.children().map(|child| (child, depth + 1)));
278        }
279    }
280
281    /// Collects every [`FieldRead`] leaf in the tree.
282    #[must_use]
283    pub fn field_reads(&self) -> Vec<&FieldRead> {
284        let mut reads = Vec::new();
285
286        self.walk(&mut |node| {
287            if let Condition::Read { field } = node {
288                reads.push(field);
289            }
290        });
291
292        reads
293    }
294}