use crate::filters::IndentDisplay;
use crate::filters::TxnFilter;
use crate::tackler;
use base64::{Engine as _, engine::general_purpose};
use jiff::tz::TimeZone;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::str::from_utf8;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FilterDefinition {
#[doc(hidden)]
#[serde(rename = "txnFilter")]
pub txn_filter: TxnFilter,
}
pub struct FilterDefZoned<'a> {
pub filt_def: &'a FilterDefinition,
pub tz: TimeZone,
}
impl Display for FilterDefZoned<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Filter")?;
self.filt_def.txn_filter.i_fmt(" ", self.tz.clone(), f)
}
}
impl FilterDefinition {
const FILTER_ARMOR: &'static str = "base64:";
pub fn from_json_str(filt_str: &str) -> Result<FilterDefinition, tackler::Error> {
match serde_json::from_str::<FilterDefinition>(filt_str) {
Ok(flt) => Ok(flt),
Err(err) => {
let msg = format!("Txn Filter definition is not valid JSON: {err}");
Err(msg.into())
}
}
}
#[must_use]
pub fn is_armored(filt: &str) -> bool {
filt.starts_with(FilterDefinition::FILTER_ARMOR)
}
pub fn from_armor(filt_armor_str: &str) -> Result<FilterDefinition, tackler::Error> {
let filt_armor = if FilterDefinition::is_armored(filt_armor_str) {
filt_armor_str.trim_start_matches(FilterDefinition::FILTER_ARMOR)
} else {
let filt_begin = match filt_armor_str.char_indices().nth(10) {
None => filt_armor_str,
Some((idx, _)) => &filt_armor_str[..idx],
};
let msg = format!(
"Unknown filter encoding, supported armor is: {}, (first 10 chars are): [{}]",
FilterDefinition::FILTER_ARMOR,
filt_begin
);
return Err(msg.into());
};
let filt_json = match general_purpose::STANDARD.decode(filt_armor) {
Ok(data) => data,
Err(err) => {
let msg = format!("Transaction Filter Ascii Armor decoding failure: {err}");
return Err(msg.into());
}
};
FilterDefinition::from_json_str(from_utf8(&filt_json)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filters::NullaryTRUE;
use indoc::indoc;
use jiff::tz;
use tackler_rs::IndocUtils;
#[test]
fn filter_definition_json() {
let filter_json_str = r#"{"txnFilter":{"NullaryTRUE":{}}}"#;
let filter_text_str = indoc! {
"|Filter
| All pass
|"}
.strip_margin();
let tf_res = serde_json::from_str::<FilterDefinition>(filter_json_str);
assert!(tf_res.is_ok());
let tf = tf_res.unwrap();
if let TxnFilter::NullaryTRUE(_) = tf.txn_filter {
} else {
panic!()
}
assert_eq!(
format!(
"{}",
FilterDefZoned {
filt_def: &tf,
tz: tz::TimeZone::UTC
}
),
filter_text_str
);
assert_eq!(
serde_json::to_string(&tf).unwrap(),
filter_json_str
);
}
#[test]
fn filter_definition_text() {
let filter_text_str = indoc! {
"|Filter
| All pass
|"}
.strip_margin();
let tf = FilterDefinition {
txn_filter: TxnFilter::NullaryTRUE(NullaryTRUE {}),
};
assert_eq!(
format!(
"{}",
FilterDefZoned {
filt_def: &tf,
tz: tz::TimeZone::UTC
}
),
filter_text_str
);
}
#[test]
fn filter_definition_is_encoded() {
assert!(FilterDefinition::is_armored(FilterDefinition::FILTER_ARMOR));
assert!(!FilterDefinition::is_armored("hello there"));
}
#[test]
fn filter_definition_from_decoded() {
let filters = vec![
"base64:eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K",
"base64:IHsgInR4bkZpbHRlciI6eyJOdWxsYXJ5VFJVRSI6e30gfSB9Cg==",
];
for s in filters {
let tf_res = FilterDefinition::from_armor(s);
assert!(tf_res.is_ok());
let tf = tf_res.unwrap();
if let TxnFilter::NullaryTRUE(_) = tf.txn_filter {
} else {
panic!()
}
}
}
#[test]
fn filter_definition_check_err_msg() {
let s_err = "eyJ0eG5GaWx0ZXIiOnsiTnVsbGFyeVRSVUUiOnt9fX0K";
let tf_res = FilterDefinition::from_armor(s_err);
assert!(tf_res.is_err());
let msg = tf_res.err().unwrap().to_string();
assert!(msg.contains(FilterDefinition::FILTER_ARMOR));
assert!(msg.contains("eyJ0eG5GaW"));
assert!(!msg.contains("eyJ0eG5GaWx"));
}
}