Skip to main content

wowlab_centrifuge/
filter.rs

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/// A scalar value in a Centrifugo publication tag filter.
11#[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    /// Creates a text value.
23    #[must_use]
24    pub fn text(value: impl Into<String>) -> Self {
25        Self(FilterValueKind::Text(value.into()))
26    }
27
28    /// Creates a boolean value.
29    #[must_use]
30    pub const fn boolean(value: bool) -> Self {
31        Self(FilterValueKind::Boolean(value))
32    }
33
34    /// Creates a number from a finite floating-point value.
35    /// # Errors
36    /// Returns a [`FilterError`] for NaN or infinity.
37    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    /// Creates a signed integer value.
45    #[must_use]
46    pub fn integer(value: i64) -> Self {
47        Self(FilterValueKind::Number(value.to_string()))
48    }
49
50    /// Creates an unsigned integer value.
51    #[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/// Validation failure while constructing a publication tag filter.
87#[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/// A validated server-side publication tag filter.
120#[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    /// Matches tags whose value equals `value`.
150    /// # Errors
151    /// Returns an error if the key or required value is empty.
152    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    /// Matches tags whose value does not equal `value`.
157    /// # Errors
158    /// Returns an error if the key or required value is empty.
159    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    /// Matches tags whose value is present in `values`.
164    /// # Errors
165    /// Returns an error if the key or value list is empty.
166    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    /// Matches tags whose value is absent from `values`.
174    /// # Errors
175    /// Returns an error if the key or value list is empty.
176    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    /// Matches publications containing `key`.
184    /// # Errors
185    /// Returns an error if the key is empty.
186    pub fn exists(key: impl Into<String>) -> Result<Self, FilterError> {
187        Self::comparison(key, Comparison::Exists)
188    }
189
190    /// Matches publications that do not contain `key`.
191    /// # Errors
192    /// Returns an error if the key is empty.
193    pub fn not_exists(key: impl Into<String>) -> Result<Self, FilterError> {
194        Self::comparison(key, Comparison::NotExists)
195    }
196
197    /// Matches text tags beginning with `value`.
198    /// # Errors
199    /// Returns an error if the key is empty or the value is not non-empty text.
200    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    /// Matches text tags ending with `value`.
205    /// # Errors
206    /// Returns an error if the key is empty or the value is not non-empty text.
207    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    /// Matches text tags containing `value`.
212    /// # Errors
213    /// Returns an error if the key is empty or the value is not non-empty text.
214    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    /// Matches numeric tags greater than `value`.
219    /// # Errors
220    /// Returns an error if the key is empty or the value is not numeric.
221    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    /// Matches numeric tags greater than or equal to `value`.
226    /// # Errors
227    /// Returns an error if the key is empty or the value is not numeric.
228    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    /// Matches numeric tags less than `value`.
242    /// # Errors
243    /// Returns an error if the key is empty or the value is not numeric.
244    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    /// Matches numeric tags less than or equal to `value`.
249    /// # Errors
250    /// Returns an error if the key is empty or the value is not numeric.
251    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    /// Requires every child expression to match.
265    /// # Errors
266    /// Returns an error if no child expressions are provided.
267    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    /// Requires at least one child expression to match.
275    /// # Errors
276    /// Returns an error if no child expressions are provided.
277    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    /// Negates `filter`.
285    #[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}