1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3pub enum ComparisonOp {
4 Eq,
6 Ne,
8 Gt,
10 Lt,
12 Gte,
14 Lte,
16}
17
18#[derive(Clone, Debug, PartialEq)]
20pub enum FieldValue {
21 Text(String),
23 Integer(i64),
25 Float(f64),
27 Boolean(bool),
29 Null,
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct FieldPath(String);
36
37impl FieldPath {
38 #[must_use]
40 pub fn new(path: impl Into<String>) -> Self {
41 Self(path.into())
42 }
43
44 pub fn segments(&self) -> impl Iterator<Item = &str> + '_ {
46 self.0.split('.')
47 }
48}
49
50#[derive(Clone, Debug, PartialEq)]
56pub enum Predicate {
57 Comparison {
59 field: FieldPath,
61 op: ComparisonOp,
63 value: FieldValue,
65 },
66 And(Vec<Self>),
68 Or(Vec<Self>),
70 Not(Box<Self>),
72 Range {
74 field: FieldPath,
76 lower: FieldValue,
78 upper: FieldValue,
80 },
81 Exists {
83 field: FieldPath,
85 },
86}
87
88#[cfg(test)]
89mod tests {
90 use super::{ComparisonOp, FieldPath, FieldValue, Predicate};
91
92 #[test]
93 fn comparison_predicate_constructs() {
94 let predicate = Predicate::Comparison {
95 field: FieldPath::new("amount"),
96 op: ComparisonOp::Gt,
97 value: FieldValue::Integer(1_000),
98 };
99
100 assert_eq!(
101 predicate,
102 Predicate::Comparison {
103 field: FieldPath::new("amount"),
104 op: ComparisonOp::Gt,
105 value: FieldValue::Integer(1_000),
106 }
107 );
108 }
109
110 #[test]
111 fn comparison_debug_round_trips_all_variants() {
112 assert_eq!(format!("{:?}", ComparisonOp::Eq), "Eq");
113 assert_eq!(format!("{:?}", ComparisonOp::Ne), "Ne");
114 assert_eq!(format!("{:?}", ComparisonOp::Gt), "Gt");
115 assert_eq!(format!("{:?}", ComparisonOp::Lt), "Lt");
116 assert_eq!(format!("{:?}", ComparisonOp::Gte), "Gte");
117 assert_eq!(format!("{:?}", ComparisonOp::Lte), "Lte");
118 }
119
120 #[test]
121 fn boolean_combinators_construct() {
122 let amount = Predicate::Comparison {
123 field: FieldPath::new("amount"),
124 op: ComparisonOp::Gt,
125 value: FieldValue::Integer(1_000),
126 };
127 let region = Predicate::Comparison {
128 field: FieldPath::new("region"),
129 op: ComparisonOp::Eq,
130 value: FieldValue::Text(String::from("eu")),
131 };
132
133 let and = Predicate::And(vec![amount.clone(), region.clone()]);
134 let or = Predicate::Or(vec![amount.clone(), region.clone()]);
135 let not = Predicate::Not(Box::new(region.clone()));
136 let nested = Predicate::And(vec![amount.clone(), Predicate::Or(vec![region.clone()])]);
137
138 assert_eq!(and, Predicate::And(vec![amount.clone(), region.clone()]));
139 assert_eq!(or, Predicate::Or(vec![amount.clone(), region.clone()]));
140 assert_eq!(not, Predicate::Not(Box::new(region.clone())));
141 assert_eq!(
142 nested,
143 Predicate::And(vec![amount, Predicate::Or(vec![region])])
144 );
145 }
146
147 #[test]
148 fn empty_boolean_combinators_construct() {
149 assert_eq!(Predicate::And(Vec::new()), Predicate::And(Vec::new()));
150 assert_eq!(Predicate::Or(Vec::new()), Predicate::Or(Vec::new()));
151 }
152
153 #[test]
154 fn range_and_exists_predicates_construct() {
155 let integer_range = Predicate::Range {
156 field: FieldPath::new("amount"),
157 lower: FieldValue::Integer(100),
158 upper: FieldValue::Integer(200),
159 };
160 let text_range = Predicate::Range {
161 field: FieldPath::new("name"),
162 lower: FieldValue::Text(String::from("a")),
163 upper: FieldValue::Text(String::from("z")),
164 };
165 let exists = Predicate::Exists {
166 field: FieldPath::new("region"),
167 };
168
169 assert_eq!(
170 integer_range,
171 Predicate::Range {
172 field: FieldPath::new("amount"),
173 lower: FieldValue::Integer(100),
174 upper: FieldValue::Integer(200),
175 }
176 );
177 assert_eq!(
178 text_range,
179 Predicate::Range {
180 field: FieldPath::new("name"),
181 lower: FieldValue::Text(String::from("a")),
182 upper: FieldValue::Text(String::from("z")),
183 }
184 );
185 assert_eq!(
186 exists,
187 Predicate::Exists {
188 field: FieldPath::new("region"),
189 }
190 );
191 }
192
193 #[test]
194 fn field_path_segments_are_borrowed_dot_parts() {
195 let nested = FieldPath::new("user.address.city");
196 let nested_segments: Vec<_> = nested.segments().collect();
197 let single = FieldPath::new("name");
198 let single_segments: Vec<_> = single.segments().collect();
199
200 assert_eq!(nested_segments, ["user", "address", "city"]);
201 assert_eq!(single_segments, ["name"]);
202 }
203
204 #[test]
205 fn routing_root_re_exports_predicate_types() {
206 use crate::routing::{
207 ComparisonOp as RootComparisonOp, FieldPath as RootFieldPath,
208 FieldValue as RootFieldValue, Predicate as RootPredicate,
209 };
210
211 let predicate = RootPredicate::Comparison {
212 field: RootFieldPath::new("amount"),
213 op: RootComparisonOp::Gte,
214 value: RootFieldValue::Integer(100),
215 };
216
217 assert_eq!(
218 predicate,
219 RootPredicate::Comparison {
220 field: RootFieldPath::new("amount"),
221 op: RootComparisonOp::Gte,
222 value: RootFieldValue::Integer(100),
223 }
224 );
225 }
226}