1use serde_json::{json, Value};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum FilterOperator {
17 Below,
19 BelowOrEqual,
21 Above,
23 AboveOrEqual,
25 Crosses,
27 CrossesUp,
29 CrossesDown,
31 InRange,
33 NotInRange,
35 Equal,
37 NotEqual,
39 Match,
41}
42
43impl FilterOperator {
44 #[must_use]
46 pub const fn as_str(self) -> &'static str {
47 match self {
48 Self::Below => "less",
49 Self::BelowOrEqual => "eless",
50 Self::Above => "greater",
51 Self::AboveOrEqual => "egreater",
52 Self::Crosses => "crosses",
53 Self::CrossesUp => "crosses_above",
54 Self::CrossesDown => "crosses_below",
55 Self::InRange => "in_range",
56 Self::NotInRange => "not_in_range",
57 Self::Equal => "equal",
58 Self::NotEqual => "nequal",
59 Self::Match => "match",
60 }
61 }
62
63 #[must_use]
68 pub fn from_wire(s: &str) -> Option<Self> {
69 match s.trim() {
70 "less" | "<" | "below" => Some(Self::Below),
71 "eless" | "<=" | "below_or_equal" => Some(Self::BelowOrEqual),
72 "greater" | ">" | "above" => Some(Self::Above),
73 "egreater" | ">=" | "above_or_equal" => Some(Self::AboveOrEqual),
74 "crosses" => Some(Self::Crosses),
75 "crosses_above" | "crosses_up" => Some(Self::CrossesUp),
76 "crosses_below" | "crosses_down" => Some(Self::CrossesDown),
77 "in_range" => Some(Self::InRange),
78 "not_in_range" => Some(Self::NotInRange),
79 "equal" | "==" | "=" => Some(Self::Equal),
80 "nequal" | "!=" | "not_equal" => Some(Self::NotEqual),
81 "match" => Some(Self::Match),
82 _ => None,
83 }
84 }
85}
86
87impl std::fmt::Display for FilterOperator {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str(self.as_str())
90 }
91}
92
93impl std::str::FromStr for FilterOperator {
94 type Err = String;
95
96 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
97 Self::from_wire(s).ok_or_else(|| format!("unknown filter operation: {s}"))
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum ExtraFilter {
104 CurrentTradingDay,
106 Search,
108 Primary,
110}
111
112impl ExtraFilter {
113 #[must_use]
115 pub const fn field_name(self) -> &'static str {
116 match self {
117 Self::CurrentTradingDay => "active_symbol",
118 Self::Search => "name,description",
119 Self::Primary => "is_primary",
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq)]
126#[allow(clippy::derive_partial_eq_without_eq)] pub struct Filter {
128 pub left: String,
130 pub operation: FilterOperator,
132 pub values: Vec<Value>,
134}
135
136impl Filter {
137 pub fn new(
139 left: impl Into<String>,
140 operation: FilterOperator,
141 values: impl IntoIterator<Item = Value>,
142 ) -> Self {
143 Self {
144 left: left.into(),
145 operation,
146 values: values.into_iter().collect(),
147 }
148 }
149
150 pub fn on_extra(
152 extra: ExtraFilter,
153 operation: FilterOperator,
154 values: impl IntoIterator<Item = Value>,
155 ) -> Self {
156 Self::new(extra.field_name(), operation, values)
157 }
158
159 #[must_use]
161 pub fn to_json(&self) -> Value {
162 let right = match self.values.as_slice() {
163 [] => Value::Null,
164 [one] => one.clone(),
165 many => Value::Array(many.to_vec()),
166 };
167 json!({
168 "left": self.left,
169 "operation": self.operation.as_str(),
170 "right": right,
171 })
172 }
173}
174
175#[derive(Debug, Clone, PartialEq)]
177#[allow(clippy::derive_partial_eq_without_eq)] pub struct FieldCondition {
179 pub left: String,
181 pub operation: FilterOperator,
183 pub value: Value,
185}
186
187impl FieldCondition {
188 pub fn new(left: impl Into<String>, operation: FilterOperator, value: Value) -> Self {
190 Self {
191 left: left.into(),
192 operation,
193 value,
194 }
195 }
196
197 #[must_use]
199 pub fn into_filter(self) -> Filter {
200 Filter::new(self.left, self.operation, [self.value])
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn filter_operator_wire_values() {
210 assert_eq!(FilterOperator::Below.as_str(), "less");
211 assert_eq!(FilterOperator::Match.as_str(), "match");
212 assert_eq!(FilterOperator::NotEqual.as_str(), "nequal");
213 }
214
215 #[test]
216 fn filter_operator_from_wire_aliases() {
217 assert_eq!(
218 FilterOperator::from_wire(">="),
219 Some(FilterOperator::AboveOrEqual)
220 );
221 assert_eq!(
222 FilterOperator::from_wire("above"),
223 Some(FilterOperator::Above)
224 );
225 assert_eq!(
226 FilterOperator::from_wire("!="),
227 Some(FilterOperator::NotEqual)
228 );
229 assert_eq!(
230 FilterOperator::from_wire("crosses_up"),
231 Some(FilterOperator::CrossesUp)
232 );
233 assert_eq!(FilterOperator::from_wire("nope"), None);
234 }
235
236 #[test]
237 fn filter_operator_display_from_str_roundtrip() {
238 for op in [
239 FilterOperator::Below,
240 FilterOperator::AboveOrEqual,
241 FilterOperator::InRange,
242 FilterOperator::Match,
243 FilterOperator::NotEqual,
244 ] {
245 let wire = op.to_string();
246 let parsed: FilterOperator = wire.parse().expect("parse");
247 assert_eq!(parsed, op);
248 }
249 assert!("nope".parse::<FilterOperator>().is_err());
250 }
251
252 #[test]
253 fn extra_filter_field_names() {
254 assert_eq!(ExtraFilter::Search.field_name(), "name,description");
255 assert_eq!(ExtraFilter::CurrentTradingDay.field_name(), "active_symbol");
256 assert_eq!(ExtraFilter::Primary.field_name(), "is_primary");
257 }
258
259 #[test]
260 fn filter_on_extra_multi_value_json() {
261 let f = Filter::on_extra(
262 ExtraFilter::Primary,
263 FilterOperator::InRange,
264 [json!(true), json!(false)],
265 );
266 let obj = f.to_json();
267 assert_eq!(obj["left"], "is_primary");
268 assert_eq!(obj["operation"], "in_range");
269 assert_eq!(obj["right"], json!([true, false]));
270 }
271
272 #[test]
273 fn filter_to_json_single_right() {
274 let f = Filter::new("close", FilterOperator::Above, [json!(100)]);
275 let obj = f.to_json();
276 assert_eq!(obj["left"], "close");
277 assert_eq!(obj["operation"], "greater");
278 assert_eq!(obj["right"], 100);
279 }
280}