1use std::fmt;
2
3use crate::proto;
4
5const PRECEDENCE_OR: u8 = 1;
6const PRECEDENCE_AND: u8 = 2;
7const PRECEDENCE_NOT: u8 = 3;
8const PRECEDENCE_COMPARISON: u8 = 4;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct FilterValue(FilterValueKind);
13
14#[derive(Clone, Debug, Eq, PartialEq)]
15enum FilterValueKind {
16 Text(String),
17 Boolean(bool),
18 Number(String),
19}
20
21impl FilterValue {
22 #[must_use]
24 pub fn text(value: impl Into<String>) -> Self {
25 Self(FilterValueKind::Text(value.into()))
26 }
27
28 #[must_use]
30 pub const fn boolean(value: bool) -> Self {
31 Self(FilterValueKind::Boolean(value))
32 }
33
34 pub fn number(value: f64) -> Result<Self, FilterError> {
38 let number = serde_json::Number::from_f64(value)
39 .ok_or_else(|| FilterError::new(FilterErrorKind::NonFiniteNumber))?;
40
41 Ok(Self(FilterValueKind::Number(number.to_string())))
42 }
43
44 #[must_use]
46 pub fn integer(value: i64) -> Self {
47 Self(FilterValueKind::Number(value.to_string()))
48 }
49
50 #[must_use]
52 pub fn unsigned(value: u64) -> Self {
53 Self(FilterValueKind::Number(value.to_string()))
54 }
55
56 fn kind_name(&self) -> &'static str {
57 match self.0 {
58 FilterValueKind::Text(_) => "text",
59 FilterValueKind::Boolean(_) => "boolean",
60 FilterValueKind::Number(_) => "number",
61 }
62 }
63
64 fn wire_value(&self) -> String {
65 match &self.0 {
66 FilterValueKind::Text(value) | FilterValueKind::Number(value) => value.clone(),
67 FilterValueKind::Boolean(value) => value.to_string(),
68 }
69 }
70
71 fn is_empty_text(&self) -> bool {
72 matches!(&self.0, FilterValueKind::Text(value) if value.is_empty())
73 }
74}
75
76impl fmt::Display for FilterValue {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match &self.0 {
79 FilterValueKind::Text(value) => f.write_str(&json_string(value)),
80 FilterValueKind::Boolean(value) => value.fmt(f),
81 FilterValueKind::Number(value) => f.write_str(value),
82 }
83 }
84}
85
86#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
88#[error("{kind}")]
89pub struct FilterError {
90 kind: FilterErrorKind,
91}
92
93impl FilterError {
94 const fn new(kind: FilterErrorKind) -> Self {
95 Self { kind }
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
100enum FilterErrorKind {
101 #[error("filter tag keys must not be empty")]
102 InvalidIdentifier,
103 #[error("filter comparison `{comparison}` requires a non-empty value")]
104 EmptyValue { comparison: &'static str },
105 #[error("filter comparison `{comparison}` requires at least one value")]
106 EmptyList { comparison: &'static str },
107 #[error("filter operator `{operator}` requires at least one child expression")]
108 EmptyGroup { operator: &'static str },
109 #[error("filter comparison `{comparison}` requires {expected}, received {actual}")]
110 InvalidValueKind {
111 comparison: &'static str,
112 expected: &'static str,
113 actual: &'static str,
114 },
115 #[error("filter numbers must be finite")]
116 NonFiniteNumber,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct SubscriptionFilter(FilterExpression);
122
123#[derive(Clone, Debug, Eq, PartialEq)]
124enum FilterExpression {
125 Comparison { key: String, comparison: Comparison },
126 And(Vec<SubscriptionFilter>),
127 Or(Vec<SubscriptionFilter>),
128 Not(Box<SubscriptionFilter>),
129}
130
131#[derive(Clone, Debug, Eq, PartialEq)]
132enum Comparison {
133 Equal(FilterValue),
134 NotEqual(FilterValue),
135 In(Vec<FilterValue>),
136 NotIn(Vec<FilterValue>),
137 Exists,
138 NotExists,
139 StartsWith(FilterValue),
140 EndsWith(FilterValue),
141 Contains(FilterValue),
142 GreaterThan(FilterValue),
143 GreaterThanOrEqual(FilterValue),
144 LessThan(FilterValue),
145 LessThanOrEqual(FilterValue),
146}
147
148impl SubscriptionFilter {
149 pub fn equal(key: impl Into<String>, value: FilterValue) -> Result<Self, FilterError> {
153 Self::value_comparison(key, value, "eq", None, Comparison::Equal)
154 }
155
156 pub fn not_equal(key: impl Into<String>, value: FilterValue) -> Result<Self, FilterError> {
160 Self::value_comparison(key, value, "neq", None, Comparison::NotEqual)
161 }
162
163 pub fn is_in<I>(key: impl Into<String>, values: I) -> Result<Self, FilterError>
167 where
168 I: IntoIterator<Item = FilterValue>,
169 {
170 Self::list_comparison(key, values, "in", Comparison::In)
171 }
172
173 pub fn is_not_in<I>(key: impl Into<String>, values: I) -> Result<Self, FilterError>
177 where
178 I: IntoIterator<Item = FilterValue>,
179 {
180 Self::list_comparison(key, values, "nin", Comparison::NotIn)
181 }
182
183 pub fn exists(key: impl Into<String>) -> Result<Self, FilterError> {
187 Self::comparison(key, Comparison::Exists)
188 }
189
190 pub fn not_exists(key: impl Into<String>) -> Result<Self, FilterError> {
194 Self::comparison(key, Comparison::NotExists)
195 }
196
197 pub fn starts_with(key: impl Into<String>, value: FilterValue) -> Result<Self, FilterError> {
201 Self::value_comparison(key, value, "sw", Some("text"), Comparison::StartsWith)
202 }
203
204 pub fn ends_with(key: impl Into<String>, value: FilterValue) -> Result<Self, FilterError> {
208 Self::value_comparison(key, value, "ew", Some("text"), Comparison::EndsWith)
209 }
210
211 pub fn contains(key: impl Into<String>, value: FilterValue) -> Result<Self, FilterError> {
215 Self::value_comparison(key, value, "ct", Some("text"), Comparison::Contains)
216 }
217
218 pub fn greater_than(key: impl Into<String>, value: FilterValue) -> Result<Self, FilterError> {
222 Self::value_comparison(key, value, "gt", Some("number"), Comparison::GreaterThan)
223 }
224
225 pub fn greater_than_or_equal(
229 key: impl Into<String>,
230 value: FilterValue,
231 ) -> Result<Self, FilterError> {
232 Self::value_comparison(
233 key,
234 value,
235 "gte",
236 Some("number"),
237 Comparison::GreaterThanOrEqual,
238 )
239 }
240
241 pub fn less_than(key: impl Into<String>, value: FilterValue) -> Result<Self, FilterError> {
245 Self::value_comparison(key, value, "lt", Some("number"), Comparison::LessThan)
246 }
247
248 pub fn less_than_or_equal(
252 key: impl Into<String>,
253 value: FilterValue,
254 ) -> Result<Self, FilterError> {
255 Self::value_comparison(
256 key,
257 value,
258 "lte",
259 Some("number"),
260 Comparison::LessThanOrEqual,
261 )
262 }
263
264 pub fn and<I>(filters: I) -> Result<Self, FilterError>
268 where
269 I: IntoIterator<Item = Self>,
270 {
271 Self::group(filters, "and", FilterExpression::And)
272 }
273
274 pub fn or<I>(filters: I) -> Result<Self, FilterError>
278 where
279 I: IntoIterator<Item = Self>,
280 {
281 Self::group(filters, "or", FilterExpression::Or)
282 }
283
284 #[must_use]
286 pub fn negate(filter: Self) -> Self {
287 Self(FilterExpression::Not(Box::new(filter)))
288 }
289
290 fn comparison(key: impl Into<String>, comparison: Comparison) -> Result<Self, FilterError> {
291 let key = key.into();
292
293 validate_identifier(&key)?;
294
295 Ok(Self(FilterExpression::Comparison { key, comparison }))
296 }
297
298 fn value_comparison(
299 key: impl Into<String>,
300 value: FilterValue,
301 comparison: &'static str,
302 required_kind: Option<&'static str>,
303 constructor: impl FnOnce(FilterValue) -> Comparison,
304 ) -> Result<Self, FilterError> {
305 if value.is_empty_text() {
306 return Err(FilterError::new(FilterErrorKind::EmptyValue { comparison }));
307 }
308
309 if let Some(expected) = required_kind {
310 let actual = value.kind_name();
311
312 if actual != expected {
313 return Err(FilterError::new(FilterErrorKind::InvalidValueKind {
314 comparison,
315 expected,
316 actual,
317 }));
318 }
319 }
320
321 Self::comparison(key, constructor(value))
322 }
323
324 fn list_comparison<I>(
325 key: impl Into<String>,
326 values: I,
327 comparison: &'static str,
328 constructor: impl FnOnce(Vec<FilterValue>) -> Comparison,
329 ) -> Result<Self, FilterError>
330 where
331 I: IntoIterator<Item = FilterValue>,
332 {
333 let values = values.into_iter().collect::<Vec<_>>();
334
335 if values.is_empty() {
336 return Err(FilterError::new(FilterErrorKind::EmptyList { comparison }));
337 }
338
339 Self::comparison(key, constructor(values))
340 }
341
342 fn group<I>(
343 filters: I,
344 operator: &'static str,
345 constructor: impl FnOnce(Vec<Self>) -> FilterExpression,
346 ) -> Result<Self, FilterError>
347 where
348 I: IntoIterator<Item = Self>,
349 {
350 let filters = filters.into_iter().collect::<Vec<_>>();
351
352 if filters.is_empty() {
353 return Err(FilterError::new(FilterErrorKind::EmptyGroup { operator }));
354 }
355
356 Ok(Self(constructor(filters)))
357 }
358
359 fn precedence(&self) -> u8 {
360 match self.0 {
361 FilterExpression::Or(_) => PRECEDENCE_OR,
362 FilterExpression::And(_) => PRECEDENCE_AND,
363 FilterExpression::Not(_) => PRECEDENCE_NOT,
364 FilterExpression::Comparison { .. } => PRECEDENCE_COMPARISON,
365 }
366 }
367
368 fn fmt_with_precedence(
369 &self,
370 formatter: &mut fmt::Formatter<'_>,
371 parent_precedence: u8,
372 ) -> fmt::Result {
373 let precedence = self.precedence();
374 let parenthesized = precedence < parent_precedence;
375
376 if parenthesized {
377 formatter.write_str("(")?;
378 }
379
380 match &self.0 {
381 FilterExpression::Comparison { key, comparison } => {
382 comparison.fmt_for_key(key, formatter)?;
383 }
384 FilterExpression::And(filters) => {
385 fmt_joined(filters, " && ", precedence, formatter)?;
386 }
387 FilterExpression::Or(filters) => {
388 fmt_joined(filters, " || ", precedence, formatter)?;
389 }
390 FilterExpression::Not(filter) => {
391 formatter.write_str("!")?;
392 filter.fmt_with_precedence(formatter, precedence)?;
393 }
394 }
395
396 if parenthesized {
397 formatter.write_str(")")?;
398 }
399
400 Ok(())
401 }
402}
403
404impl fmt::Display for SubscriptionFilter {
405 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406 self.fmt_with_precedence(f, 0)
407 }
408}
409
410impl Comparison {
411 fn fmt_for_key(&self, key: &str, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
412 let escaped_key = json_string(key);
413 let key = format!("tag[{escaped_key}]");
414
415 match self {
416 Self::Equal(value) => write!(formatter, "{key} == {value}"),
417 Self::NotEqual(value) => write!(formatter, "{key} != {value}"),
418 Self::In(values) => fmt_membership(&key, "in", values, formatter),
419 Self::NotIn(values) => fmt_membership(&key, "not in", values, formatter),
420 Self::Exists => write!(formatter, "exists({key})"),
421 Self::NotExists => write!(formatter, "not_exists({key})"),
422 Self::StartsWith(value) => write!(formatter, "starts_with({key}, {value})"),
423 Self::EndsWith(value) => write!(formatter, "ends_with({key}, {value})"),
424 Self::Contains(value) => write!(formatter, "contains({key}, {value})"),
425 Self::GreaterThan(value) => write!(formatter, "{key} > {value}"),
426 Self::GreaterThanOrEqual(value) => write!(formatter, "{key} >= {value}"),
427 Self::LessThan(value) => write!(formatter, "{key} < {value}"),
428 Self::LessThanOrEqual(value) => write!(formatter, "{key} <= {value}"),
429 }
430 }
431
432 fn to_proto(&self, key: &str) -> proto::FilterNode {
433 let mut node = proto::FilterNode {
434 key: key.to_owned(),
435 ..Default::default()
436 };
437
438 match self {
439 Self::Equal(value) => set_value(&mut node, "eq", value),
440 Self::NotEqual(value) => set_value(&mut node, "neq", value),
441 Self::In(values) => set_values(&mut node, "in", values),
442 Self::NotIn(values) => set_values(&mut node, "nin", values),
443 Self::Exists => "ex".clone_into(&mut node.cmp),
444 Self::NotExists => "nex".clone_into(&mut node.cmp),
445 Self::StartsWith(value) => set_value(&mut node, "sw", value),
446 Self::EndsWith(value) => set_value(&mut node, "ew", value),
447 Self::Contains(value) => set_value(&mut node, "ct", value),
448 Self::GreaterThan(value) => set_value(&mut node, "gt", value),
449 Self::GreaterThanOrEqual(value) => set_value(&mut node, "gte", value),
450 Self::LessThan(value) => set_value(&mut node, "lt", value),
451 Self::LessThanOrEqual(value) => set_value(&mut node, "lte", value),
452 }
453
454 node
455 }
456}
457
458impl From<&SubscriptionFilter> for proto::FilterNode {
459 fn from(filter: &SubscriptionFilter) -> Self {
460 match &filter.0 {
461 FilterExpression::Comparison { key, comparison } => comparison.to_proto(key),
462 FilterExpression::And(filters) => logical_node("and", filters),
463 FilterExpression::Or(filters) => logical_node("or", filters),
464 FilterExpression::Not(filter) => logical_node("not", std::slice::from_ref(filter)),
465 }
466 }
467}
468
469fn validate_identifier(identifier: &str) -> Result<(), FilterError> {
470 if identifier.is_empty() {
471 Err(FilterError::new(FilterErrorKind::InvalidIdentifier))
472 } else {
473 Ok(())
474 }
475}
476
477fn json_string(value: &str) -> String {
478 serde_json::to_string(value).unwrap_or_else(|error| {
479 debug_assert!(
480 error.is_io(),
481 "serializing a string unexpectedly failed: {error}"
482 );
483
484 "\"<unrenderable>\"".to_owned()
485 })
486}
487
488fn fmt_joined(
489 filters: &[SubscriptionFilter],
490 separator: &str,
491 precedence: u8,
492 formatter: &mut fmt::Formatter<'_>,
493) -> fmt::Result {
494 for (index, filter) in filters.iter().enumerate() {
495 if index > 0 {
496 formatter.write_str(separator)?;
497 }
498
499 filter.fmt_with_precedence(formatter, precedence)?;
500 }
501
502 Ok(())
503}
504
505fn fmt_membership(
506 key: &str,
507 operator: &str,
508 values: &[FilterValue],
509 formatter: &mut fmt::Formatter<'_>,
510) -> fmt::Result {
511 write!(formatter, "{key} {operator} [")?;
512
513 for (index, value) in values.iter().enumerate() {
514 if index > 0 {
515 formatter.write_str(", ")?;
516 }
517
518 write!(formatter, "{value}")?;
519 }
520
521 formatter.write_str("]")
522}
523
524fn set_value(node: &mut proto::FilterNode, comparison: &str, value: &FilterValue) {
525 comparison.clone_into(&mut node.cmp);
526 node.val = value.wire_value();
527}
528
529fn set_values(node: &mut proto::FilterNode, comparison: &str, values: &[FilterValue]) {
530 comparison.clone_into(&mut node.cmp);
531 node.vals = values.iter().map(FilterValue::wire_value).collect();
532}
533
534fn logical_node(operator: &str, filters: &[SubscriptionFilter]) -> proto::FilterNode {
535 proto::FilterNode {
536 op: operator.to_owned(),
537 nodes: filters.iter().map(proto::FilterNode::from).collect(),
538 ..Default::default()
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use googletest::prelude::*;
545 use prost::Message as _;
546
547 use super::{FilterValue, SubscriptionFilter};
548
549 fn text_filter(key: &str, value: &str) -> Result<SubscriptionFilter> {
550 SubscriptionFilter::equal(key, FilterValue::text(value)).or_fail()
551 }
552
553 #[gtest]
554 fn lowers_known_centrifugo_filters_to_exact_protocol_trees() -> Result<()> {
555 let filter = SubscriptionFilter::or([
556 SubscriptionFilter::and([
557 text_filter("env", "prod")?,
558 SubscriptionFilter::is_in(
559 "region",
560 [FilterValue::text("us"), FilterValue::text("eu")],
561 )
562 .or_fail()?,
563 SubscriptionFilter::greater_than("version", FilterValue::unsigned(42)).or_fail()?,
564 ])
565 .or_fail()?,
566 SubscriptionFilter::and([
567 text_filter("env", "staging")?,
568 SubscriptionFilter::negate(SubscriptionFilter::exists("debug").or_fail()?),
569 ])
570 .or_fail()?,
571 ])
572 .or_fail()?;
573
574 let node = crate::proto::FilterNode::from(&filter);
575
576 verify_that!(node.op.as_str(), eq("or"))?;
577 verify_that!(node.nodes.len(), eq(2))?;
578 verify_that!(node.nodes[0].op.as_str(), eq("and"))?;
579 verify_that!(node.nodes[0].nodes[0].key.as_str(), eq("env"))?;
580 verify_that!(node.nodes[0].nodes[0].cmp.as_str(), eq("eq"))?;
581 verify_that!(node.nodes[0].nodes[0].val.as_str(), eq("prod"))?;
582 verify_that!(node.nodes[0].nodes[1].cmp.as_str(), eq("in"))?;
583 verify_that!(
584 &node.nodes[0].nodes[1].vals,
585 elements_are![eq("us"), eq("eu")]
586 )?;
587 verify_that!(node.nodes[0].nodes[2].cmp.as_str(), eq("gt"))?;
588 verify_that!(node.nodes[0].nodes[2].val.as_str(), eq("42"))?;
589 verify_that!(node.nodes[1].nodes[1].op.as_str(), eq("not"))?;
590 verify_that!(node.nodes[1].nodes[1].nodes[0].cmp.as_str(), eq("ex"))?;
591
592 verify_that!(
593 node.encode_to_vec(),
594 eq(&vec![
595 10, 2, 111, 114, 50, 63, 10, 3, 97, 110, 100, 50, 15, 18, 3, 101, 110, 118, 26, 2,
596 101, 113, 34, 4, 112, 114, 111, 100, 50, 20, 18, 6, 114, 101, 103, 105, 111, 110,
597 26, 2, 105, 110, 42, 2, 117, 115, 42, 2, 101, 117, 50, 17, 18, 7, 118, 101, 114,
598 115, 105, 111, 110, 26, 2, 103, 116, 34, 2, 52, 50, 50, 45, 10, 3, 97, 110, 100,
599 50, 18, 18, 3, 101, 110, 118, 26, 2, 101, 113, 34, 7, 115, 116, 97, 103, 105, 110,
600 103, 50, 18, 10, 3, 110, 111, 116, 50, 11, 18, 5, 100, 101, 98, 117, 103, 26, 2,
601 101, 120,
602 ])
603 )
604 }
605}