tackler_api/filters/
filter_definition.rs

1/*
2 * Tackler-NG 2023-2025
3 * SPDX-License-Identifier: Apache-2.0
4 */
5use crate::filters::IndentDisplay;
6use crate::filters::TxnFilter;
7use crate::tackler;
8use base64::{Engine as _, engine::general_purpose};
9use jiff::tz::TimeZone;
10use serde::{Deserialize, Serialize};
11use std::fmt::{Display, Formatter};
12use std::str::from_utf8;
13
14/// The main filter definition
15///
16/// This is the main handle for Txn Filter  definition, and this can be used to serialize
17/// and deserialize filters from JSON.
18///
19/// # Examples
20///
21/// ```
22/// # use std::error::Error;
23/// # use tackler_api::filters::FilterDefinition;
24/// # use tackler_api::filters::TxnFilter;
25///
26/// let filter_json_str = r#"{"txnFilter":{"NullaryTRUE":{}}}"#;
27///
28/// let tf = serde_json::from_str::<FilterDefinition>(filter_json_str)?;
29///
30/// match tf.txn_filter {
31///      TxnFilter::NullaryTRUE(_) => (),
32///      _ => panic!(),
33/// }
34///
35/// assert_eq!(serde_json::to_string(&tf)?, filter_json_str);
36/// # Ok::<(), Box<dyn Error>>(())
37/// ```
38#[derive(Serialize, Deserialize, Debug, Clone)]
39pub struct FilterDefinition {
40    #[doc(hidden)]
41    #[serde(rename = "txnFilter")]
42    pub txn_filter: TxnFilter,
43}
44
45/// Helper used to carry Timezone information to Display Trait
46pub struct FilterDefZoned<'a> {
47    /// Transaction Filter Definition
48    pub filt_def: &'a FilterDefinition,
49    /// Timezone to be by Display
50    pub tz: TimeZone,
51}
52impl Display for FilterDefZoned<'_> {
53    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
54        writeln!(f, "Filter")?;
55        self.filt_def.txn_filter.i_fmt("  ", self.tz.clone(), f)
56    }
57}
58
59impl FilterDefinition {
60    const FILTER_ARMOR: &'static str = "base64:";
61
62    /// Generate filter from JSON String
63    ///
64    /// # Errors
65    ///
66    /// Return `Err` if the filter definition is not valid
67    ///
68    /// # Examples
69    /// ```
70    /// # use tackler_api::tackler;
71    /// # use tackler_api::filters::FilterDefinition;
72    /// # use tackler_api::filters::TxnFilter;
73    ///
74    /// let filter_json_str = r#"{"txnFilter":{"NullaryTRUE":{}}}"#;
75    ///
76    /// let tf = FilterDefinition::from_json_str(filter_json_str)?;
77    ///
78    /// match tf.txn_filter {
79    ///      TxnFilter::NullaryTRUE(_) => (),
80    ///      _ => panic!(),
81    /// }
82    ///
83    /// # Ok::<(), tackler::Error>(())
84    /// ```
85    pub fn from_json_str(filt_str: &str) -> Result<FilterDefinition, tackler::Error> {
86        match serde_json::from_str::<FilterDefinition>(filt_str) {
87            Ok(flt) => Ok(flt),
88            Err(err) => {
89                let msg = format!("Txn Filter definition is not valid JSON: {err}");
90                Err(msg.into())
91            }
92        }
93    }
94
95    /// Test if filter string is ascii armored
96    ///
97    #[must_use]
98    pub fn is_armored(filt: &str) -> bool {
99        filt.starts_with(FilterDefinition::FILTER_ARMOR)
100    }
101
102    /// Generate filter from ascii armor JSON String
103    ///
104    /// The ascii armor must be be prefixed with `base64`
105    ///
106    /// # Errors
107    ///
108    /// Returns `Err` if the filter definition is not valid or encoding is unknown
109    ///
110    /// # Examples
111    /// ```
112    /// # use tackler_api::tackler;
113    /// # use tackler_api::filters::FilterDefinition;
114    /// # use tackler_api::filters::TxnFilter;
115    ///
116    /// let filter_ascii_armor = "base64:eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K";
117    ///
118    /// let tf = FilterDefinition::from_armor(filter_ascii_armor)?;
119    ///
120    /// match tf.txn_filter {
121    ///      TxnFilter::NullaryTRUE(_) => (),
122    ///      _ => panic!(),
123    /// }
124    ///
125    /// # Ok::<(), tackler::Error>(())
126    /// ```
127    pub fn from_armor(filt_armor_str: &str) -> Result<FilterDefinition, tackler::Error> {
128        let filt_armor = if FilterDefinition::is_armored(filt_armor_str) {
129            filt_armor_str.trim_start_matches(FilterDefinition::FILTER_ARMOR)
130        } else {
131            let filt_begin = match filt_armor_str.char_indices().nth(10) {
132                None => filt_armor_str,
133                Some((idx, _)) => &filt_armor_str[..idx],
134            };
135            let msg = format!(
136                "Unknown filter encoding, supported armor is: {}, (first 10 chars are): [{}]",
137                FilterDefinition::FILTER_ARMOR,
138                filt_begin
139            );
140            return Err(msg.into());
141        };
142        let filt_json = match general_purpose::STANDARD.decode(filt_armor) {
143            Ok(data) => data,
144            Err(err) => {
145                let msg = format!("Transaction Filter Ascii Armor decoding failure: {err}");
146                return Err(msg.into());
147            }
148        };
149
150        FilterDefinition::from_json_str(from_utf8(&filt_json)?)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::filters::NullaryTRUE;
158    use indoc::indoc;
159    use jiff::tz;
160    use tackler_rs::IndocUtils;
161
162    #[test]
163    // test: c6fe4f86-1daa-4e29-b327-467aed6dc5bb
164    // desc: filter definition, JSON
165    fn filter_definition_json() {
166        let filter_json_str = r#"{"txnFilter":{"NullaryTRUE":{}}}"#;
167
168        let filter_text_str = indoc! {
169        "|Filter
170         |  All pass
171         |"}
172        .strip_margin();
173
174        let tf_res = serde_json::from_str::<FilterDefinition>(filter_json_str);
175        assert!(tf_res.is_ok());
176        let tf = tf_res.unwrap(/*:test:*/);
177
178        if let TxnFilter::NullaryTRUE(_) = tf.txn_filter {
179        } else {
180            panic!(/*:test:*/)
181        }
182
183        assert_eq!(
184            format!(
185                "{}",
186                FilterDefZoned {
187                    filt_def: &tf,
188                    tz: tz::TimeZone::UTC
189                }
190            ),
191            filter_text_str
192        );
193        assert_eq!(
194            serde_json::to_string(&tf).unwrap(/*:test:*/),
195            filter_json_str
196        );
197    }
198
199    #[test]
200    // test: 5e90f6cb-4414-4d4e-a496-1bb26abb9ba1
201    // desc: filter definition, Text
202    fn filter_definition_text() {
203        let filter_text_str = indoc! {
204        "|Filter
205         |  All pass
206         |"}
207        .strip_margin();
208
209        let tf = FilterDefinition {
210            txn_filter: TxnFilter::NullaryTRUE(NullaryTRUE {}),
211        };
212
213        assert_eq!(
214            format!(
215                "{}",
216                FilterDefZoned {
217                    filt_def: &tf,
218                    tz: tz::TimeZone::UTC
219                }
220            ),
221            filter_text_str
222        );
223    }
224
225    #[test]
226    fn filter_definition_is_encoded() {
227        assert!(FilterDefinition::is_armored(FilterDefinition::FILTER_ARMOR));
228        assert!(!FilterDefinition::is_armored("hello there"));
229    }
230
231    #[test]
232    // test: 939516a3-3c7a-4af8-b8fc-bcec2839965d
233    // desc: decode txn filter from base64 armored JSON
234    fn filter_definition_from_decoded() {
235        let filters = vec![
236            "base64:eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K",
237            "base64:IHsgInR4bkZpbHRlciI6eyJOdWxsYXJ5VFJVRSI6e30gfSB9Cg==",
238        ];
239
240        for s in filters {
241            let tf_res = FilterDefinition::from_armor(s);
242            assert!(tf_res.is_ok());
243
244            let tf = tf_res.unwrap(/*:test:*/);
245            if let TxnFilter::NullaryTRUE(_) = tf.txn_filter {
246            } else {
247                panic!(/*:test:*/)
248            }
249        }
250    }
251
252    #[test]
253    fn filter_definition_check_err_msg() {
254        let s_err = "eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K";
255
256        let tf_res = FilterDefinition::from_armor(s_err);
257        assert!(tf_res.is_err());
258
259        let msg = tf_res.err().unwrap(/*:test:*/).to_string();
260
261        assert!(msg.contains(FilterDefinition::FILTER_ARMOR));
262        // test malformed cut-off
263        assert!(msg.contains("eyJ0eG5GaW"));
264        assert!(!msg.contains("eyJ0eG5GaWx"));
265    }
266}