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> {
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 #[must_use]
98 pub fn is_armored(filt: &str) -> bool {
99 filt.starts_with(FilterDefinition::FILTER_ARMOR)
100 }
101
102 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 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();
177
178 if let TxnFilter::NullaryTRUE(_) = tf.txn_filter {
179 } else {
180 panic!()
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(),
195 filter_json_str
196 );
197 }
198
199 #[test]
200 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 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();
245 if let TxnFilter::NullaryTRUE(_) = tf.txn_filter {
246 } else {
247 panic!()
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().to_string();
260
261 assert!(msg.contains(FilterDefinition::FILTER_ARMOR));
262 assert!(msg.contains("eyJ0eG5GaW"));
264 assert!(!msg.contains("eyJ0eG5GaWx"));
265 }
266}