nodedb_query/scan_filter/
op.rs1#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12#[error("unknown filter operator `{op}`")]
13pub struct UnknownFilterOp {
14 pub op: String,
16}
17
18impl From<UnknownFilterOp> for zerompk::Error {
19 fn from(e: UnknownFilterOp) -> Self {
20 zerompk::Error::IoError(std::io::Error::new(std::io::ErrorKind::InvalidData, e))
21 }
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum FilterOp {
27 Eq,
28 Ne,
29 Gt,
30 Gte,
31 Lt,
32 Lte,
33 Contains,
34 Like,
35 NotLike,
36 Ilike,
37 NotIlike,
38 In,
39 NotIn,
40 IsNull,
41 IsNotNull,
42 ArrayContains,
43 ArrayContainsAll,
44 ArrayOverlap,
45 MatchAll,
48 Exists,
49 NotExists,
50 Or,
51 Expr,
57 GtColumn,
61 GteColumn,
62 LtColumn,
63 LteColumn,
64 EqColumn,
65 NeColumn,
66}
67
68impl FilterOp {
69 pub fn parse(name: &str) -> Result<Self, UnknownFilterOp> {
72 Ok(match name {
73 "eq" => Self::Eq,
74 "ne" | "neq" => Self::Ne,
75 "gt" => Self::Gt,
76 "gte" | "ge" => Self::Gte,
77 "lt" => Self::Lt,
78 "lte" | "le" => Self::Lte,
79 "contains" => Self::Contains,
80 "like" => Self::Like,
81 "not_like" => Self::NotLike,
82 "ilike" => Self::Ilike,
83 "not_ilike" => Self::NotIlike,
84 "in" => Self::In,
85 "not_in" => Self::NotIn,
86 "is_null" => Self::IsNull,
87 "is_not_null" => Self::IsNotNull,
88 "array_contains" => Self::ArrayContains,
89 "array_contains_all" => Self::ArrayContainsAll,
90 "array_overlap" => Self::ArrayOverlap,
91 "match_all" => Self::MatchAll,
92 "exists" => Self::Exists,
93 "not_exists" => Self::NotExists,
94 "or" => Self::Or,
95 "expr" => Self::Expr,
96 "gt_col" => Self::GtColumn,
97 "gte_col" => Self::GteColumn,
98 "lt_col" => Self::LtColumn,
99 "lte_col" => Self::LteColumn,
100 "eq_col" => Self::EqColumn,
101 "ne_col" => Self::NeColumn,
102 other => {
103 return Err(UnknownFilterOp {
104 op: other.to_string(),
105 });
106 }
107 })
108 }
109
110 pub fn as_str(&self) -> &'static str {
111 match self {
112 Self::Eq => "eq",
113 Self::Ne => "ne",
114 Self::Gt => "gt",
115 Self::Gte => "gte",
116 Self::Lt => "lt",
117 Self::Lte => "lte",
118 Self::Contains => "contains",
119 Self::Like => "like",
120 Self::NotLike => "not_like",
121 Self::Ilike => "ilike",
122 Self::NotIlike => "not_ilike",
123 Self::In => "in",
124 Self::NotIn => "not_in",
125 Self::IsNull => "is_null",
126 Self::IsNotNull => "is_not_null",
127 Self::ArrayContains => "array_contains",
128 Self::ArrayContainsAll => "array_contains_all",
129 Self::ArrayOverlap => "array_overlap",
130 Self::MatchAll => "match_all",
131 Self::Exists => "exists",
132 Self::NotExists => "not_exists",
133 Self::Or => "or",
134 Self::Expr => "expr",
135 Self::GtColumn => "gt_col",
136 Self::GteColumn => "gte_col",
137 Self::LtColumn => "lt_col",
138 Self::LteColumn => "lte_col",
139 Self::EqColumn => "eq_col",
140 Self::NeColumn => "ne_col",
141 }
142 }
143}
144
145impl serde::Serialize for FilterOp {
146 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
147 serializer.serialize_str(self.as_str())
148 }
149}
150
151impl<'de> serde::Deserialize<'de> for FilterOp {
152 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
153 let s = String::deserialize(deserializer)?;
154 FilterOp::parse(&s).map_err(serde::de::Error::custom)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 const ALL: [FilterOp; 29] = [
163 FilterOp::Eq,
164 FilterOp::Ne,
165 FilterOp::Gt,
166 FilterOp::Gte,
167 FilterOp::Lt,
168 FilterOp::Lte,
169 FilterOp::Contains,
170 FilterOp::Like,
171 FilterOp::NotLike,
172 FilterOp::Ilike,
173 FilterOp::NotIlike,
174 FilterOp::In,
175 FilterOp::NotIn,
176 FilterOp::IsNull,
177 FilterOp::IsNotNull,
178 FilterOp::ArrayContains,
179 FilterOp::ArrayContainsAll,
180 FilterOp::ArrayOverlap,
181 FilterOp::MatchAll,
182 FilterOp::Exists,
183 FilterOp::NotExists,
184 FilterOp::Or,
185 FilterOp::Expr,
186 FilterOp::GtColumn,
187 FilterOp::GteColumn,
188 FilterOp::LtColumn,
189 FilterOp::LteColumn,
190 FilterOp::EqColumn,
191 FilterOp::NeColumn,
192 ];
193
194 #[test]
195 fn every_name_round_trips() {
196 for op in ALL {
197 assert_eq!(FilterOp::parse(op.as_str()), Ok(op), "{op:?}");
198 }
199 }
200
201 #[test]
202 fn aliases_parse_to_their_canonical_variant() {
203 assert_eq!(FilterOp::parse("neq"), Ok(FilterOp::Ne));
204 assert_eq!(FilterOp::parse("ge"), Ok(FilterOp::Gte));
205 assert_eq!(FilterOp::parse("le"), Ok(FilterOp::Lte));
206 }
207
208 #[test]
209 fn unknown_name_is_an_error_naming_the_text() {
210 let err = FilterOp::parse("any_in").expect_err("unknown");
211 assert_eq!(err.op, "any_in");
212 assert_eq!(err.to_string(), "unknown filter operator `any_in`");
213 assert!(FilterOp::parse("").is_err());
214 assert!(FilterOp::parse("EQ").is_err());
215 }
216
217 #[test]
218 fn serde_rejects_an_unknown_name() {
219 let ok: FilterOp = serde_json::from_str("\"array_overlap\"").expect("known");
220 assert_eq!(ok, FilterOp::ArrayOverlap);
221 let err = serde_json::from_str::<FilterOp>("\"any_in\"").expect_err("unknown");
222 assert!(err.to_string().contains("any_in"), "{err}");
223 }
224}