milli_core/
attribute_patterns.rs

1use deserr::Deserr;
2use serde::{Deserialize, Serialize};
3use utoipa::ToSchema;
4
5use crate::is_faceted_by;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
8#[repr(transparent)]
9#[serde(transparent)]
10pub struct AttributePatterns {
11    #[schema(example = json!(["title", "overview_*", "release_date"]))]
12    pub patterns: Vec<String>,
13}
14
15impl<E: deserr::DeserializeError> Deserr<E> for AttributePatterns {
16    fn deserialize_from_value<V: deserr::IntoValue>(
17        value: deserr::Value<V>,
18        location: deserr::ValuePointerRef,
19    ) -> Result<Self, E> {
20        Vec::<String>::deserialize_from_value(value, location).map(|patterns| Self { patterns })
21    }
22}
23
24impl From<Vec<String>> for AttributePatterns {
25    fn from(patterns: Vec<String>) -> Self {
26        Self { patterns }
27    }
28}
29
30impl AttributePatterns {
31    /// Match a string against the attribute patterns using the match_pattern function.
32    pub fn match_str(&self, str: &str) -> PatternMatch {
33        let mut pattern_match = PatternMatch::NoMatch;
34        for pattern in &self.patterns {
35            match match_pattern(pattern, str) {
36                PatternMatch::Match => return PatternMatch::Match,
37                PatternMatch::Parent => pattern_match = PatternMatch::Parent,
38                PatternMatch::NoMatch => {}
39            }
40        }
41        pattern_match
42    }
43}
44
45/// Match a string against a pattern.
46///
47/// The pattern can be a wildcard, a prefix, a suffix or an exact match.
48///
49/// # Arguments
50///
51/// * `pattern` - The pattern to match against.
52/// * `str` - The string to match against the pattern.
53pub fn match_pattern(pattern: &str, str: &str) -> PatternMatch {
54    // If the pattern is a wildcard, return Match
55    if pattern == "*" {
56        return PatternMatch::Match;
57    } else if pattern.starts_with('*') && pattern.ends_with('*') {
58        // If the pattern starts and ends with a wildcard, return Match if the string contains the pattern without the wildcards
59        if str.contains(&pattern[1..pattern.len() - 1]) {
60            return PatternMatch::Match;
61        }
62    } else if let Some(pattern) = pattern.strip_prefix('*') {
63        // If the pattern starts with a wildcard, return Match if the string ends with the pattern without the wildcard
64        if str.ends_with(pattern) {
65            return PatternMatch::Match;
66        }
67    } else if let Some(pattern) = pattern.strip_suffix('*') {
68        // If the pattern ends with a wildcard, return Match if the string starts with the pattern without the wildcard
69        if str.starts_with(pattern) {
70            return PatternMatch::Match;
71        }
72    } else if pattern == str {
73        // If the pattern is exactly the string, return Match
74        return PatternMatch::Match;
75    }
76
77    // If the field is a parent field of the pattern, return Parent
78    if is_faceted_by(pattern, str) {
79        PatternMatch::Parent
80    } else {
81        PatternMatch::NoMatch
82    }
83}
84
85/// Match a field against a pattern using the legacy behavior.
86///
87/// A field matches a pattern if it is a parent of the pattern or if it is the pattern itself.
88/// This behavior is used to match the sortable attributes, the searchable attributes and the filterable attributes rules `Field`.
89///
90/// # Arguments
91///
92/// * `pattern` - The pattern to match against.
93/// * `field` - The field to match against the pattern.
94pub fn match_field_legacy(pattern: &str, field: &str) -> PatternMatch {
95    if is_faceted_by(field, pattern) {
96        // If the field matches the pattern or is a nested field of the pattern, return Match (legacy behavior)
97        PatternMatch::Match
98    } else if is_faceted_by(pattern, field) {
99        // If the field is a parent field of the pattern, return Parent
100        PatternMatch::Parent
101    } else {
102        // If the field does not match the pattern and is not a parent of a nested field that matches the pattern, return NoMatch
103        PatternMatch::NoMatch
104    }
105}
106
107/// Match a field against a distinct field.
108pub fn match_distinct_field(distinct_field: Option<&str>, field: &str) -> PatternMatch {
109    if let Some(distinct_field) = distinct_field {
110        if field == distinct_field {
111            // If the field matches exactly the distinct field, return Match
112            return PatternMatch::Match;
113        } else if is_faceted_by(distinct_field, field) {
114            // If the field is a parent field of the distinct field, return Parent
115            return PatternMatch::Parent;
116        }
117    }
118    // If the field does not match the distinct field and is not a parent of a nested field that matches the distinct field, return NoMatch
119    PatternMatch::NoMatch
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
123pub enum PatternMatch {
124    /// The field is a parent of a nested field that matches the pattern
125    /// For example, the field is `toto`, and the pattern is `toto.titi`
126    Parent,
127    /// The field matches the pattern
128    Match,
129    /// The field does not match the pattern
130    NoMatch,
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_match_pattern() {
139        assert_eq!(match_pattern("*", "test"), PatternMatch::Match);
140        assert_eq!(match_pattern("test*", "test"), PatternMatch::Match);
141        assert_eq!(match_pattern("test*", "testa"), PatternMatch::Match);
142        assert_eq!(match_pattern("*test", "test"), PatternMatch::Match);
143        assert_eq!(match_pattern("*test", "atest"), PatternMatch::Match);
144        assert_eq!(match_pattern("*test*", "test"), PatternMatch::Match);
145        assert_eq!(match_pattern("*test*", "atesta"), PatternMatch::Match);
146        assert_eq!(match_pattern("*test*", "atest"), PatternMatch::Match);
147        assert_eq!(match_pattern("*test*", "testa"), PatternMatch::Match);
148        assert_eq!(match_pattern("test*test", "test"), PatternMatch::NoMatch);
149        assert_eq!(match_pattern("*test", "testa"), PatternMatch::NoMatch);
150        assert_eq!(match_pattern("test*", "atest"), PatternMatch::NoMatch);
151    }
152}