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    /// # Examples
65    /// ```
66    /// # use tackler_api::tackler;
67    /// # use tackler_api::filters::FilterDefinition;
68    /// # use tackler_api::filters::TxnFilter;
69    ///
70    /// let filter_json_str = r#"{"txnFilter":{"NullaryTRUE":{}}}"#;
71    ///
72    /// let tf = FilterDefinition::from_json_str(filter_json_str)?;
73    ///
74    /// match tf.txn_filter {
75    ///      TxnFilter::NullaryTRUE(_) => (),
76    ///      _ => panic!(),
77    /// }
78    ///
79    /// # Ok::<(), tackler::Error>(())
80    /// ```
81    pub fn from_json_str(filt_str: &str) -> Result<FilterDefinition, tackler::Error> {
82        match serde_json::from_str::<FilterDefinition>(filt_str) {
83            Ok(flt) => Ok(flt),
84            Err(err) => {
85                let msg = format!("Txn Filter definition is not valid JSON: {err}");
86                Err(msg.into())
87            }
88        }
89    }
90
91    /// Test if filter string is ascii armored
92    ///
93    #[must_use]
94    pub fn is_armored(filt: &str) -> bool {
95        filt.starts_with(FilterDefinition::FILTER_ARMOR)
96    }
97
98    /// Generate filter from ascii armor JSON String
99    ///
100    /// # Examples
101    /// ```
102    /// # use tackler_api::tackler;
103    /// # use tackler_api::filters::FilterDefinition;
104    /// # use tackler_api::filters::TxnFilter;
105    ///
106    /// let filter_ascii_armor = "base64:eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K";
107    ///
108    /// let tf = FilterDefinition::from_armor(filter_ascii_armor)?;
109    ///
110    /// match tf.txn_filter {
111    ///      TxnFilter::NullaryTRUE(_) => (),
112    ///      _ => panic!(),
113    /// }
114    ///
115    /// # Ok::<(), tackler::Error>(())
116    /// ```
117    pub fn from_armor(filt_armor_str: &str) -> Result<FilterDefinition, tackler::Error> {
118        let filt_armor = if FilterDefinition::is_armored(filt_armor_str) {
119            filt_armor_str.trim_start_matches(FilterDefinition::FILTER_ARMOR)
120        } else {
121            let filt_begin = match filt_armor_str.char_indices().nth(10) {
122                None => filt_armor_str,
123                Some((idx, _)) => &filt_armor_str[..idx],
124            };
125            let msg = format!(
126                "Unknown filter encoding, supported armor is: {}, (first 10 chars are): [{}]",
127                FilterDefinition::FILTER_ARMOR,
128                filt_begin
129            );
130            return Err(msg.into());
131        };
132        let filt_json = match general_purpose::STANDARD.decode(filt_armor) {
133            Ok(data) => data,
134            Err(err) => {
135                let msg = format!("Transaction Filter Ascii Armor decoding failure: {}", err);
136                return Err(msg.into());
137            }
138        };
139
140        FilterDefinition::from_json_str(from_utf8(&filt_json)?)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::filters::NullaryTRUE;
148    use indoc::indoc;
149    use jiff::tz;
150    use tackler_rs::IndocUtils;
151
152    #[test]
153    // test: c6fe4f86-1daa-4e29-b327-467aed6dc5bb
154    // desc: filter definition, JSON
155    fn filter_definition_json() {
156        let filter_json_str = r#"{"txnFilter":{"NullaryTRUE":{}}}"#;
157
158        let filter_text_str = indoc! {
159        "|Filter
160         |  All pass
161         |"}
162        .strip_margin();
163
164        let tf_res = serde_json::from_str::<FilterDefinition>(filter_json_str);
165        assert!(tf_res.is_ok());
166        let tf = tf_res.unwrap(/*:test:*/);
167
168        match tf.txn_filter {
169            TxnFilter::NullaryTRUE(_) => (),
170            _ => panic!(/*:test:*/),
171        }
172
173        assert_eq!(
174            format!(
175                "{}",
176                FilterDefZoned {
177                    filt_def: &tf,
178                    tz: tz::TimeZone::UTC
179                }
180            ),
181            filter_text_str
182        );
183        assert_eq!(
184            serde_json::to_string(&tf).unwrap(/*:test:*/),
185            filter_json_str
186        );
187    }
188
189    #[test]
190    // test: 5e90f6cb-4414-4d4e-a496-1bb26abb9ba1
191    // desc: filter definition, Text
192    fn filter_definition_text() {
193        let filter_text_str = indoc! {
194        "|Filter
195         |  All pass
196         |"}
197        .strip_margin();
198
199        let tf = FilterDefinition {
200            txn_filter: TxnFilter::NullaryTRUE(NullaryTRUE {}),
201        };
202
203        assert_eq!(
204            format!(
205                "{}",
206                FilterDefZoned {
207                    filt_def: &tf,
208                    tz: tz::TimeZone::UTC
209                }
210            ),
211            filter_text_str
212        );
213    }
214
215    #[test]
216    fn filter_definition_is_encoded() {
217        assert!(FilterDefinition::is_armored(FilterDefinition::FILTER_ARMOR));
218        assert!(!FilterDefinition::is_armored("hello there"));
219    }
220
221    #[test]
222    // test: 939516a3-3c7a-4af8-b8fc-bcec2839965d
223    // desc: decode txn filter from base64 armored JSON
224    fn filter_definition_from_decoded() {
225        let filters = vec![
226            "base64:eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K",
227            "base64:IHsgInR4bkZpbHRlciI6eyJOdWxsYXJ5VFJVRSI6e30gfSB9Cg==",
228        ];
229
230        for s in filters {
231            let tf_res = FilterDefinition::from_armor(s);
232            assert!(tf_res.is_ok());
233
234            let tf = tf_res.unwrap(/*:test:*/);
235            match tf.txn_filter {
236                TxnFilter::NullaryTRUE(_) => (),
237                _ => panic!(/*:test:*/),
238            }
239        }
240    }
241
242    #[test]
243    fn filter_definition_check_err_msg() {
244        let s_err = "eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K";
245
246        let tf_res = FilterDefinition::from_armor(s_err);
247        assert!(tf_res.is_err());
248
249        let msg = tf_res.err().unwrap(/*:test:*/).to_string();
250
251        assert!(msg.contains(FilterDefinition::FILTER_ARMOR));
252        // test malformed cut-off
253        assert!(msg.contains("eyJ0eG5GaW"));
254        assert!(!msg.contains("eyJ0eG5GaWx"));
255    }
256}