qualia/
query_builder.rs

1use crate::object::PropValue;
2use crate::query::QueryNode;
3use crate::query::QueryNode::*;
4
5/// A convenience class for creating [`QueryNode`] objects. This enum should be used by calling
6/// methods on [`Q`], rather than by creating a new [`QueryBuilder`] yourself.
7///
8/// A [`QueryBuilder`] starts out empty. Each call to [`.id()`](QueryBuilder::id), [`.equal()`](QueryBuilder::equal), or [`.like()`](QueryBuilder::like) will add a new criteria to the query. All these criteria are then ANDed together.
9pub enum QueryBuilder {
10    #[doc(hidden)]
11    Empty,
12    #[doc(hidden)]
13    Single(QueryNode),
14    #[doc(hidden)]
15    And(Vec<QueryNode>),
16}
17
18/// A blank [`QueryBuilder`] object, to be used instead of constructing new [`QueryBuilder`]
19/// objects.
20pub const Q: QueryBuilder = QueryBuilder::Empty;
21
22impl QueryBuilder {
23    fn add(self, node: QueryNode) -> Self {
24        match self {
25            QueryBuilder::Empty => QueryBuilder::Single(node),
26            QueryBuilder::Single(prev_node) => QueryBuilder::And(vec![prev_node, node]),
27            QueryBuilder::And(mut nodes) => {
28                nodes.push(node);
29                QueryBuilder::And(nodes)
30            }
31        }
32    }
33
34    /// Add the criteria that the object have the given ID.
35    pub fn id(self, value: impl Into<i64>) -> Self {
36        self.add(PropEqual {
37            name: "object_id".into(),
38            value: value.into().into(),
39        })
40    }
41
42    /// Add the criteria that the given field has exactly the given value.
43    pub fn equal(self, name: impl Into<String>, value: impl Into<PropValue>) -> Self {
44        self.add(PropEqual {
45            name: name.into(),
46            value: value.into(),
47        })
48    }
49
50    /// Add the criteria that the given field have contents matching the given value.
51    ///
52    /// See [`PropLike`] for the supported syntax.
53    pub fn like(self, name: impl Into<String>, pattern: impl Into<String>) -> Self {
54        self.add(PropLike {
55            name: name.into(),
56            pattern: pattern.into(),
57        })
58    }
59
60    /// Consume this [`QueryBuilder`] and build a [`QueryNode`].
61    pub fn build(self) -> QueryNode {
62        match self {
63            QueryBuilder::Single(node) => node,
64            QueryBuilder::And(nodes) => And(nodes),
65            QueryBuilder::Empty => Empty,
66        }
67    }
68}
69
70impl Into<QueryNode> for QueryBuilder {
71    fn into(self) -> QueryNode {
72        self.build()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    macro_rules! builder_test {
81        ($description:expr, $actual:expr, $expected:expr $(,)?) => {
82            ($description, $actual, $expected)
83        };
84    }
85
86    #[test]
87    fn queries_build_correctly() {
88        let tests = [
89            builder_test!("empty query", Q.build(), Empty {}),
90            builder_test!(
91                "string equal",
92                Q.equal("name", "value").build(),
93                PropEqual {
94                    name: "name".to_string(),
95                    value: "value".into(),
96                },
97            ),
98            builder_test!(
99                "number equal",
100                Q.equal("name", 42).build(),
101                PropEqual {
102                    name: "name".to_string(),
103                    value: 42.into(),
104                },
105            ),
106            builder_test!(
107                "object_id equal",
108                Q.id(42).build(),
109                PropEqual {
110                    name: "object_id".to_string(),
111                    value: 42.into(),
112                },
113            ),
114            builder_test!(
115                "simple word like",
116                Q.like("name", "phrase").build(),
117                PropLike {
118                    name: "name".to_string(),
119                    pattern: "phrase".to_string(),
120                },
121            ),
122            builder_test!(
123                "anded queries",
124                Q.equal("name1", "value1")
125                    .equal("name2", "value2")
126                    .equal("name3", "value3")
127                    .build(),
128                And(vec![
129                    PropEqual {
130                        name: "name1".to_string(),
131                        value: "value1".into(),
132                    },
133                    PropEqual {
134                        name: "name2".to_string(),
135                        value: "value2".into(),
136                    },
137                    PropEqual {
138                        name: "name3".to_string(),
139                        value: "value3".into(),
140                    },
141                ]),
142            ),
143        ];
144
145        for (description, actual_query, expected_query) in &tests {
146            assert_eq!(expected_query, actual_query, "{}", description);
147        }
148    }
149}