From 73d7bab8844bb21c7a9143c30800c2d11d411e42 Mon Sep 17 00:00:00 2001 From: rtkay123 Date: Sun, 17 Aug 2025 20:02:49 +0200 Subject: feat: typology processor (#8) --- .../src/processor/typology/aggregate_rules.rs | 202 +++++++++++++++++++++ .../src/processor/typology/evaluate_expression.rs | 171 +++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 crates/typologies/src/processor/typology/aggregate_rules.rs create mode 100644 crates/typologies/src/processor/typology/evaluate_expression.rs (limited to 'crates/typologies/src/processor/typology') diff --git a/crates/typologies/src/processor/typology/aggregate_rules.rs b/crates/typologies/src/processor/typology/aggregate_rules.rs new file mode 100644 index 0000000..8f92dae --- /dev/null +++ b/crates/typologies/src/processor/typology/aggregate_rules.rs @@ -0,0 +1,202 @@ +use anyhow::Result; +use std::collections::HashSet; + +use warden_core::{ + configuration::routing::RoutingConfiguration, + message::{RuleResult, TypologyResult}, +}; + +pub(super) fn aggregate_rules( + rule_results: &[RuleResult], + routing: &RoutingConfiguration, + rule_result: &RuleResult, +) -> Result<(Vec, usize)> { + let mut typology_result: Vec = vec![]; + let mut all_rules_set = HashSet::new(); + + routing.messages.iter().for_each(|message| { + message.typologies.iter().for_each(|typology| { + let mut set = HashSet::new(); + + for rule in typology.rules.iter() { + set.insert((&rule.id, rule.version())); + all_rules_set.insert((&rule.id, rule.version())); + } + + if !set.contains(&(&rule_result.id, rule_result.version.as_str())) { + return; + } + + let rule_results: Vec<_> = rule_results + .iter() + .filter_map(|value| { + if set.contains(&(&value.id, &value.version)) { + Some(value.to_owned()) + } else { + None + } + }) + .collect(); + + if !rule_results.is_empty() { + typology_result.push(TypologyResult { + id: typology.id.to_owned(), + version: typology.version.to_owned(), + rule_results, + ..Default::default() + }); + } + }); + }); + + Ok((typology_result, all_rules_set.len())) +} + +#[cfg(test)] +mod tests { + use super::*; + use warden_core::{ + configuration::routing::{Message, RoutingConfiguration, Rule, Typology}, + message::RuleResult, + }; + + fn create_rule(id: &str, version: &str) -> Rule { + Rule { + id: id.to_string(), + version: Some(version.to_string()), + } + } + + fn create_rule_result(id: &str, version: &str) -> RuleResult { + RuleResult { + id: id.to_string(), + version: version.to_string(), + ..Default::default() + } + } + + #[test] + fn returns_empty_when_no_matching_typology() { + let routing = RoutingConfiguration { + messages: vec![Message { + typologies: vec![Typology { + id: "T1".to_string(), + version: "v1".to_string(), + rules: vec![create_rule("R1", "v1")], + }], + ..Default::default() + }], + ..Default::default() + }; + + let rule_results = vec![create_rule_result("R2", "v1")]; + let input_rule = create_rule_result("R2", "v1"); + + let (result, count) = aggregate_rules(&rule_results, &routing, &input_rule).unwrap(); + assert!(result.is_empty()); + assert_eq!(count, 1); // one rule in routing + } + + #[test] + fn returns_typology_with_matching_rule() { + let routing = RoutingConfiguration { + messages: vec![Message { + typologies: vec![Typology { + id: "T1".to_string(), + version: "v1".to_string(), + rules: vec![create_rule("R1", "v1"), create_rule("R2", "v1")], + }], + ..Default::default() + }], + ..Default::default() + }; + + let rule_results = vec![ + create_rule_result("R1", "v1"), + create_rule_result("R2", "v1"), + ]; + + let input_rule = create_rule_result("R1", "v1"); + + let (result, count) = aggregate_rules(&rule_results, &routing, &input_rule).unwrap(); + + assert_eq!(count, 2); // R1, R2 + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, "T1"); + assert_eq!(result[0].rule_results.len(), 2); + } + + #[test] + fn ignores_unrelated_rules_in_rule_results() { + let routing = RoutingConfiguration { + messages: vec![Message { + typologies: vec![Typology { + id: "T1".to_string(), + version: "v1".to_string(), + rules: vec![create_rule("R1", "v1")], + }], + ..Default::default() + }], + ..Default::default() + }; + + let rule_results = vec![ + create_rule_result("R1", "v1"), + create_rule_result("R99", "v1"), // unrelated + ]; + + let input_rule = create_rule_result("R1", "v1"); + + let (result, count) = aggregate_rules(&rule_results, &routing, &input_rule).unwrap(); + + assert_eq!(count, 1); + assert_eq!(result.len(), 1); + assert_eq!(result[0].rule_results.len(), 1); + assert_eq!(result[0].rule_results[0].id, "R1"); + } + + #[test] + fn handles_multiple_messages_and_typologies() { + let routing = RoutingConfiguration { + messages: vec![ + Message { + typologies: vec![ + Typology { + id: "T1".to_string(), + version: "v1".to_string(), + rules: vec![create_rule("R1", "v1")], + }, + Typology { + id: "T2".to_string(), + version: "v1".to_string(), + rules: vec![create_rule("R2", "v1")], + }, + ], + ..Default::default() + }, + Message { + typologies: vec![Typology { + id: "T3".to_string(), + version: "v1".to_string(), + rules: vec![create_rule("R1", "v1"), create_rule("R2", "v1")], + }], + ..Default::default() + }, + ], + ..Default::default() + }; + + let rule_results = vec![ + create_rule_result("R1", "v1"), + create_rule_result("R2", "v1"), + ]; + let input_rule = create_rule_result("R1", "v1"); + + let (result, count) = aggregate_rules(&rule_results, &routing, &input_rule).unwrap(); + + assert_eq!(count, 2); // R1, R2 appear in multiple typologies, but unique rules are 2 + assert_eq!(result.len(), 2); // T1 (R1) and T3 (R1 & R2) + assert_eq!(result[0].id, "T1"); + assert_eq!(result[1].id, "T3"); + } +} diff --git a/crates/typologies/src/processor/typology/evaluate_expression.rs b/crates/typologies/src/processor/typology/evaluate_expression.rs new file mode 100644 index 0000000..844011e --- /dev/null +++ b/crates/typologies/src/processor/typology/evaluate_expression.rs @@ -0,0 +1,171 @@ +use anyhow::Result; +use tracing::warn; +use warden_core::{configuration::typology::TypologyConfiguration, message::TypologyResult}; + +pub(super) fn evaluate_expression( + typology_result: &mut TypologyResult, + typology_config: &TypologyConfiguration, +) -> Result { + let mut to_return = 0.0; + let expression = typology_config + .expression + .as_ref() + .expect("expression is missing"); + + let rule_values = &typology_config.rules; + + for rule in expression.terms.iter() { + let rule_result = typology_result + .rule_results + .iter() + .find(|value| value.id.eq(&rule.id) && value.version.eq(&rule.version)); + + if rule_result.is_none() { + warn!(term = ?rule, "could not find rule result for typology term"); + return Ok(Default::default()); + } + + let rule_result = rule_result.expect("checked and is some"); + + let weight = rule_values + .iter() + .filter_map(|rv| { + if !(rv.id.eq(&rule_result.id) && rv.version.eq(&rule_result.version)) { + None + } else { + rv.wghts.iter().find_map(|value| { + match value.r#ref.eq(&rule_result.sub_rule_ref) { + true => Some(value.wght), + false => None, + } + }) + } + }) + .next(); + + if weight.is_none() { + warn!(rule = ?rule, "could not find a weight for the matching rule"); + } + let weight = weight.unwrap_or_default(); + + to_return = match expression.operator() { + warden_core::configuration::typology::Operator::Add => to_return + weight, + warden_core::configuration::typology::Operator::Multiply => to_return * weight, + warden_core::configuration::typology::Operator::Subtract => to_return - weight, + warden_core::configuration::typology::Operator::Divide => { + if weight.ne(&0.0) { + to_return / weight + } else { + to_return + } + } + }; + } + Ok(to_return) +} + +#[cfg(test)] +mod tests { + use warden_core::{ + configuration::typology::{Expression, Operator, Term, TypologyRule, TypologyRuleWeight}, + message::RuleResult, + }; + + use super::*; + + fn make_rule_result(id: &str, version: &str, sub_ref: &str) -> RuleResult { + RuleResult { + id: id.to_string(), + version: version.to_string(), + sub_rule_ref: sub_ref.to_string(), + ..Default::default() + } + } + + fn make_rule_value(id: &str, version: &str, ref_name: &str, weight: f64) -> TypologyRule { + TypologyRule { + id: id.to_string(), + version: version.to_string(), + wghts: vec![TypologyRuleWeight { + r#ref: ref_name.to_string(), + wght: weight, + }], + } + } + + fn make_expression(terms: Vec<(&str, &str)>, op: Operator) -> Expression { + Expression { + terms: terms + .into_iter() + .map(|(id, version)| Term { + id: id.to_string(), + version: version.to_string(), + }) + .collect(), + operator: op.into(), + } + } + + #[test] + fn test_add_operator_multiple_terms() { + let mut typology_result = TypologyResult { + rule_results: vec![ + make_rule_result("R1", "v1", "sub1"), + make_rule_result("R2", "v1", "sub2"), + ], + ..Default::default() + }; + + let config = TypologyConfiguration { + expression: Some(make_expression( + vec![("R1", "v1"), ("R2", "v1")], + Operator::Add, + )), + rules: vec![ + make_rule_value("R1", "v1", "sub1", 10.0), + make_rule_value("R2", "v1", "sub2", 5.0), + ], + ..Default::default() + }; + + let result = evaluate_expression(&mut typology_result, &config).unwrap(); + assert_eq!(result, 15.0); + } + + #[test] + fn test_missing_rule_result_returns_zero() { + let mut typology_result = TypologyResult { + rule_results: vec![make_rule_result("R1", "v1", "sub1")], + ..Default::default() + }; + + let config = TypologyConfiguration { + expression: Some(make_expression( + vec![("R1", "v1"), ("R2", "v1")], + Operator::Add, + )), + rules: vec![make_rule_value("R1", "v1", "sub1", 10.0)], + ..Default::default() + }; + + let result = evaluate_expression(&mut typology_result, &config).unwrap(); + assert_eq!(result, 0.0); + } + + #[test] + fn test_missing_weight_defaults_to_zero() { + let mut typology_result = TypologyResult { + rule_results: vec![make_rule_result("R1", "v1", "subX")], // sub_ref doesn't match + ..Default::default() + }; + + let config = TypologyConfiguration { + expression: Some(make_expression(vec![("R1", "v1")], Operator::Add)), + rules: vec![make_rule_value("R1", "v1", "sub1", 10.0)], // different ref + ..Default::default() + }; + + let result = evaluate_expression(&mut typology_result, &config).unwrap(); + assert_eq!(result, 0.0); + } +} -- cgit v1.2.3