imessage_database/util/
query_context.rs

1/*!
2 Contains logic for handling query filter configurations.
3*/
4use std::collections::BTreeSet;
5
6use chrono::prelude::*;
7
8use crate::{
9    error::query_context::QueryContextError,
10    util::dates::{TIMESTAMP_FACTOR, get_offset},
11};
12
13#[derive(Debug, Default, PartialEq, Eq)]
14/// Represents filter configurations for a SQL query.
15pub struct QueryContext {
16    /// The start date filter. Only messages sent on or after this date will be included.
17    pub start: Option<i64>,
18    /// The end date filter. Only messages sent before this date will be included.
19    pub end: Option<i64>,
20    /// Selected handle IDs
21    pub selected_handle_ids: Option<BTreeSet<i32>>,
22    /// Selected chat IDs
23    pub selected_chat_ids: Option<BTreeSet<i32>>,
24}
25
26impl QueryContext {
27    /// Populate a [`QueryContext`] with a start date
28    /// # Example:
29    ///
30    /// ```
31    /// use imessage_database::util::query_context::QueryContext;
32    ///
33    /// let mut context = QueryContext::default();
34    /// context.set_start("2023-01-01");
35    /// ```
36    pub fn set_start(&mut self, start: &str) -> Result<(), QueryContextError> {
37        let timestamp = QueryContext::sanitize_date(start)
38            .ok_or(QueryContextError::InvalidDate(start.to_string()))?;
39        self.start = Some(timestamp);
40        Ok(())
41    }
42
43    /// Populate a [`QueryContext`] with an end date
44    /// # Example:
45    ///
46    /// ```
47    /// use imessage_database::util::query_context::QueryContext;
48    ///
49    /// let mut context = QueryContext::default();
50    /// context.set_end("2023-01-01");
51    /// ```
52    pub fn set_end(&mut self, end: &str) -> Result<(), QueryContextError> {
53        let timestamp = QueryContext::sanitize_date(end)
54            .ok_or(QueryContextError::InvalidDate(end.to_string()))?;
55        self.end = Some(timestamp);
56        Ok(())
57    }
58
59    /// Populate a [`QueryContext`] with a list of handle IDs to select
60    ///
61    /// # Example:
62    ///
63    /// ```
64    /// use std::collections::BTreeSet;
65    /// use imessage_database::util::query_context::QueryContext;
66    ///
67    /// let mut context = QueryContext::default();
68    /// context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
69    /// ```
70    pub fn set_selected_handle_ids(&mut self, selected_handle_ids: BTreeSet<i32>) {
71        self.selected_handle_ids = (!selected_handle_ids.is_empty()).then_some(selected_handle_ids);
72    }
73
74    /// Populate a [`QueryContext`] with a list of chat IDs to select
75    ///
76    /// # Example:
77    ///
78    /// ```
79    /// use std::collections::BTreeSet;
80    /// use imessage_database::util::query_context::QueryContext;
81    ///
82    /// let mut context = QueryContext::default();
83    /// context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
84    /// ```
85    pub fn set_selected_chat_ids(&mut self, selected_chat_ids: BTreeSet<i32>) {
86        self.selected_chat_ids = (!selected_chat_ids.is_empty()).then_some(selected_chat_ids);
87    }
88
89    /// Ensure a date string is valid
90    fn sanitize_date(date: &str) -> Option<i64> {
91        if date.len() < 9 {
92            return None;
93        }
94
95        let year = date.get(0..4)?.parse::<i32>().ok()?;
96
97        if !date.get(4..5)?.eq("-") {
98            return None;
99        }
100
101        let month = date.get(5..7)?.parse::<u32>().ok()?;
102        if month > 12 {
103            return None;
104        }
105
106        if !date.get(7..8)?.eq("-") {
107            return None;
108        }
109
110        let day = date.get(8..)?.parse::<u32>().ok()?;
111        if day > 31 {
112            return None;
113        }
114
115        let local = Local.with_ymd_and_hms(year, month, day, 0, 0, 0).single()?;
116        let stamp = local.timestamp_nanos_opt().unwrap_or(0);
117
118        Some(stamp - (get_offset() * TIMESTAMP_FACTOR))
119    }
120
121    /// Determine if the current `QueryContext` has any filters present
122    ///
123    /// # Example:
124    ///
125    /// ```
126    /// use imessage_database::util::query_context::QueryContext;
127    ///
128    /// let mut context = QueryContext::default();
129    /// assert!(!context.has_filters());
130    /// context.set_start("2023-01-01");
131    /// assert!(context.has_filters());
132    /// ```
133    #[must_use]
134    pub fn has_filters(&self) -> bool {
135        self.start.is_some()
136            || self.end.is_some()
137            || self.selected_chat_ids.is_some()
138            || self.selected_handle_ids.is_some()
139    }
140}
141
142#[cfg(test)]
143mod use_tests {
144    use chrono::prelude::*;
145
146    use crate::util::{
147        dates::{TIMESTAMP_FACTOR, format, get_offset},
148        query_context::QueryContext,
149    };
150
151    #[test]
152    fn can_create() {
153        let context = QueryContext::default();
154        assert!(context.start.is_none());
155        assert!(context.end.is_none());
156        assert!(!context.has_filters());
157    }
158
159    #[test]
160    fn can_create_start() {
161        let mut context = QueryContext::default();
162        context.set_start("2020-01-01").unwrap();
163
164        let from_timestamp = DateTime::from_timestamp(
165            (context.start.unwrap() / TIMESTAMP_FACTOR) + get_offset(),
166            0,
167        )
168        .unwrap()
169        .naive_utc();
170        let local = Local.from_utc_datetime(&from_timestamp);
171
172        assert_eq!(format(&Ok(local)), "Jan 01, 2020 12:00:00 AM");
173        assert!(context.start.is_some());
174        assert!(context.end.is_none());
175        assert!(context.has_filters());
176    }
177
178    #[test]
179    fn can_create_end() {
180        let mut context = QueryContext::default();
181        context.set_end("2020-01-01").unwrap();
182
183        let from_timestamp =
184            DateTime::from_timestamp((context.end.unwrap() / TIMESTAMP_FACTOR) + get_offset(), 0)
185                .unwrap()
186                .naive_utc();
187        let local = Local.from_utc_datetime(&from_timestamp);
188
189        assert_eq!(format(&Ok(local)), "Jan 01, 2020 12:00:00 AM");
190        assert!(context.start.is_none());
191        assert!(context.end.is_some());
192        assert!(context.has_filters());
193    }
194
195    #[test]
196    fn can_create_both() {
197        let mut context = QueryContext::default();
198        context.set_start("2020-01-01").unwrap();
199        context.set_end("2020-02-02").unwrap();
200
201        let from_timestamp = DateTime::from_timestamp(
202            (context.start.unwrap() / TIMESTAMP_FACTOR) + get_offset(),
203            0,
204        )
205        .unwrap()
206        .naive_utc();
207        let local_start = Local.from_utc_datetime(&from_timestamp);
208
209        let from_timestamp =
210            DateTime::from_timestamp((context.end.unwrap() / TIMESTAMP_FACTOR) + get_offset(), 0)
211                .unwrap()
212                .naive_utc();
213        let local_end = Local.from_utc_datetime(&from_timestamp);
214
215        assert_eq!(format(&Ok(local_start)), "Jan 01, 2020 12:00:00 AM");
216        assert_eq!(format(&Ok(local_end)), "Feb 02, 2020 12:00:00 AM");
217        assert!(context.start.is_some());
218        assert!(context.end.is_some());
219        assert!(context.has_filters());
220    }
221}
222
223#[cfg(test)]
224mod id_tests {
225    use std::collections::BTreeSet;
226
227    use crate::util::query_context::QueryContext;
228
229    #[test]
230    fn test_can_set_selected_chat_ids() {
231        let mut qc = QueryContext::default();
232        qc.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
233
234        assert_eq!(qc.selected_chat_ids, Some(BTreeSet::from([1, 2, 3])));
235        assert!(qc.has_filters());
236    }
237
238    #[test]
239    fn test_can_set_selected_chat_ids_empty() {
240        let mut qc = QueryContext::default();
241        qc.set_selected_chat_ids(BTreeSet::new());
242
243        assert_eq!(qc.selected_chat_ids, None);
244        assert!(!qc.has_filters());
245    }
246
247    #[test]
248    fn test_can_overwrite_selected_chat_ids_empty() {
249        let mut qc = QueryContext::default();
250        qc.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
251        qc.set_selected_chat_ids(BTreeSet::new());
252
253        assert_eq!(qc.selected_chat_ids, None);
254        assert!(!qc.has_filters());
255    }
256
257    #[test]
258    fn test_can_set_selected_handle_ids() {
259        let mut qc = QueryContext::default();
260        qc.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
261
262        assert_eq!(qc.selected_handle_ids, Some(BTreeSet::from([1, 2, 3])));
263        assert!(qc.has_filters());
264    }
265
266    #[test]
267    fn test_can_set_selected_handle_ids_empty() {
268        let mut qc = QueryContext::default();
269        qc.set_selected_handle_ids(BTreeSet::new());
270
271        assert_eq!(qc.selected_handle_ids, None);
272        assert!(!qc.has_filters());
273    }
274
275    #[test]
276    fn test_can_overwrite_selected_handle_ids_empty() {
277        let mut qc = QueryContext::default();
278        qc.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
279        qc.set_selected_handle_ids(BTreeSet::new());
280
281        assert_eq!(qc.selected_handle_ids, None);
282        assert!(!qc.has_filters());
283    }
284}
285
286#[cfg(test)]
287mod sanitize_tests {
288    use crate::util::query_context::QueryContext;
289
290    #[test]
291    fn can_sanitize_good() {
292        let res = QueryContext::sanitize_date("2020-01-01");
293        assert!(res.is_some());
294    }
295
296    #[test]
297    fn can_reject_bad_short() {
298        let res = QueryContext::sanitize_date("1-1-20");
299        assert!(res.is_none());
300    }
301
302    #[test]
303    fn can_reject_bad_order() {
304        let res = QueryContext::sanitize_date("01-01-2020");
305        assert!(res.is_none());
306    }
307
308    #[test]
309    fn can_reject_bad_month() {
310        let res = QueryContext::sanitize_date("2020-31-01");
311        assert!(res.is_none());
312    }
313
314    #[test]
315    fn can_reject_bad_day() {
316        let res = QueryContext::sanitize_date("2020-01-32");
317        assert!(res.is_none());
318    }
319
320    #[test]
321    fn can_reject_bad_data() {
322        let res = QueryContext::sanitize_date("2020-AB-CD");
323        assert!(res.is_none());
324    }
325
326    #[test]
327    fn can_reject_wrong_hyphen() {
328        let res = QueryContext::sanitize_date("2020–01–01");
329        assert!(res.is_none());
330    }
331}