pub mod dns;
pub mod encoding;
pub mod geoip;
pub mod list;
pub mod network;
pub mod string_mutators;
use crate::error::{Result, TqlError};
use serde_json::Value as JsonValue;
use std::collections::HashMap;
pub trait Mutator: Send + Sync {
fn apply(&self, field_name: &str, record: &JsonValue, value: &JsonValue) -> Result<JsonValue>;
fn name(&self) -> &str;
fn is_enrichment(&self) -> bool {
false
}
fn returns_boolean(&self) -> bool {
false
}
}
pub type MutatorParams = HashMap<String, JsonValue>;
pub fn apply_mutators(
value: &JsonValue,
mutators: &[Box<dyn Mutator>],
field_name: &str,
record: &JsonValue,
) -> Result<JsonValue> {
let mut result = value.clone();
for mutator in mutators {
result = mutator.apply(field_name, record, &result)?;
}
Ok(result)
}
pub fn get_param<'a>(
params: &'a HashMap<String, JsonValue>,
named_key: &str,
positional_index: usize,
) -> Option<&'a JsonValue> {
params
.get(named_key)
.or_else(|| params.get(&positional_index.to_string()))
}
pub const MUTATOR_NAMES: &[&str] = &[
"lowercase",
"uppercase",
"trim",
"split",
"length",
"replace",
"b64encode",
"b64decode",
"urldecode",
"hexencode",
"hexdecode",
"md5",
"sha256",
"refang",
"defang",
"is_private",
"is_global",
"is_multicast",
"is_loopback",
"is_link_local",
"nslookup",
"geoip",
"geoip_lookup",
"geo",
"any",
"all",
"avg",
"average",
"sum",
"max",
"min",
];
pub fn returns_boolean(name: &str) -> bool {
create_mutator(name, None).is_ok_and(|m| m.returns_boolean())
}
pub fn boolean_predicate_names() -> Vec<&'static str> {
let mut names: Vec<&'static str> = MUTATOR_NAMES
.iter()
.copied()
.filter(|n| returns_boolean(n))
.collect();
names.sort_unstable();
names
}
pub fn mutator_names() -> Vec<&'static str> {
let mut names = MUTATOR_NAMES.to_vec();
names.sort_unstable();
names
}
pub fn create_mutator(name: &str, params: Option<MutatorParams>) -> Result<Box<dyn Mutator>> {
let params = params.unwrap_or_default();
let name_lower = name.to_lowercase();
match name_lower.as_str() {
"lowercase" => Ok(Box::new(string_mutators::LowercaseMutator::new(params))),
"uppercase" => Ok(Box::new(string_mutators::UppercaseMutator::new(params))),
"trim" => Ok(Box::new(string_mutators::TrimMutator::new(params))),
"split" => Ok(Box::new(string_mutators::SplitMutator::new(params))),
"length" => Ok(Box::new(string_mutators::LengthMutator::new(params))),
"replace" => Ok(Box::new(string_mutators::ReplaceMutator::new(params))),
"b64encode" => Ok(Box::new(encoding::Base64EncodeMutator::new(params))),
"b64decode" => Ok(Box::new(encoding::Base64DecodeMutator::new(params))),
"urldecode" => Ok(Box::new(encoding::URLDecodeMutator::new(params))),
"hexencode" => Ok(Box::new(encoding::HexEncodeMutator::new(params))),
"hexdecode" => Ok(Box::new(encoding::HexDecodeMutator::new(params))),
"md5" => Ok(Box::new(encoding::MD5Mutator::new(params))),
"sha256" => Ok(Box::new(encoding::SHA256Mutator::new(params))),
"refang" => Ok(Box::new(network::RefangMutator::new(params))),
"defang" => Ok(Box::new(network::DefangMutator::new(params))),
"is_private" => Ok(Box::new(network::IsPrivateMutator::new(params))),
"is_global" => Ok(Box::new(network::IsGlobalMutator::new(params))),
"is_multicast" => Ok(Box::new(network::IsMulticastMutator::new(params))),
"is_loopback" => Ok(Box::new(network::IsLoopbackMutator::new(params))),
"is_link_local" => Ok(Box::new(network::IsLinkLocalMutator::new(params))),
"nslookup" => Ok(Box::new(dns::NSLookupMutator::new(params))),
"geoip" | "geoip_lookup" | "geo" => Ok(Box::new(geoip::GeoIPMutator::new(params))),
"any" => Ok(Box::new(list::AnyMutator::new(params))),
"all" => Ok(Box::new(list::AllMutator::new(params))),
"avg" => Ok(Box::new(list::AvgMutator::new(params))),
"average" => Ok(Box::new(list::AverageMutator::new(params))),
"sum" => Ok(Box::new(list::SumMutator::new(params))),
"max" => Ok(Box::new(list::MaxMutator::new(params))),
"min" => Ok(Box::new(list::MinMutator::new(params))),
_ => Err(TqlError::MutatorError(format!("Unknown mutator: {}", name))),
}
}
#[cfg(test)]
mod registry {
use super::*;
fn dispatch_names_from_source() -> Vec<String> {
let src = include_str!("mod.rs");
let fn_start = src
.find("pub fn create_mutator(")
.expect("create_mutator not found in mod.rs");
let match_start = src[fn_start..]
.find("match name_lower.as_str() {")
.expect("create_mutator match block not found")
+ fn_start;
let mut names = Vec::new();
for line in src[match_start..].lines() {
let line = line.trim();
if line.starts_with("//") {
continue;
}
if !line.contains("=>") {
continue;
}
let arm = line.split("=>").next().unwrap_or("");
let mut rest = arm;
while let Some(open) = rest.find('"') {
let after = &rest[open + 1..];
let Some(close) = after.find('"') else { break };
names.push(after[..close].to_string());
rest = &after[close + 1..];
}
}
assert!(
!names.is_empty(),
"failed to parse any mutator names out of create_mutator"
);
names.sort();
names
}
#[test]
fn mutator_names_matches_dispatch() {
let from_source = dispatch_names_from_source();
let listed = mutator_names()
.into_iter()
.map(str::to_string)
.collect::<Vec<_>>();
assert_eq!(
listed, from_source,
"MUTATOR_NAMES has drifted from the create_mutator match arms. \
Add or remove the name in BOTH places (and in the Python + JS registries \
— see js/tests/registryParity.spec.ts)."
);
}
#[test]
fn every_listed_name_constructs() {
for name in MUTATOR_NAMES {
let built = create_mutator(name, None);
assert!(
built.is_ok(),
"MUTATOR_NAMES lists `{name}` but create_mutator rejects it"
);
}
}
#[test]
fn listed_names_are_unique_and_lowercase() {
let mut seen = std::collections::HashSet::new();
for name in MUTATOR_NAMES {
assert_eq!(
*name,
name.to_lowercase(),
"MUTATOR_NAMES entry `{name}` is not lowercase; create_mutator lowercases \
its input, so a mixed-case entry could never be matched"
);
assert!(
seen.insert(*name),
"duplicate entry `{name}` in MUTATOR_NAMES"
);
}
}
#[test]
fn unknown_mutator_is_rejected() {
assert!(
create_mutator("definitely_not_a_mutator", None).is_err(),
"create_mutator accepted a name that is not in the registry"
);
}
}