tackler_api/filters/
filter_definition.rs1use 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#[derive(Serialize, Deserialize, Debug, Clone)]
39pub struct FilterDefinition {
40 #[doc(hidden)]
41 #[serde(rename = "txnFilter")]
42 pub txn_filter: TxnFilter,
43}
44
45pub struct FilterDefZoned<'a> {
47 pub filt_def: &'a FilterDefinition,
49 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 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 #[must_use]
94 pub fn is_armored(filt: &str) -> bool {
95 filt.starts_with(FilterDefinition::FILTER_ARMOR)
96 }
97
98 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 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();
167
168 match tf.txn_filter {
169 TxnFilter::NullaryTRUE(_) => (),
170 _ => panic!(),
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(),
185 filter_json_str
186 );
187 }
188
189 #[test]
190 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 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();
235 match tf.txn_filter {
236 TxnFilter::NullaryTRUE(_) => (),
237 _ => panic!(),
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().to_string();
250
251 assert!(msg.contains(FilterDefinition::FILTER_ARMOR));
252 assert!(msg.contains("eyJ0eG5GaW"));
254 assert!(!msg.contains("eyJ0eG5GaWx"));
255 }
256}