aboutsummaryrefslogtreecommitdiffstats
path: root/crates/typologies/src/processor/typology
diff options
context:
space:
mode:
authorrtkay123 <dev@kanjala.com>2025-08-17 20:02:49 +0200
committerGitHub <noreply@github.com>2025-08-17 20:02:49 +0200
commit73d7bab8844bb21c7a9143c30800c2d11d411e42 (patch)
tree955290bd2bded56b534738d6320216fbeeb708cb /crates/typologies/src/processor/typology
parent725739985d853b07d73fa7fcd6db1f2f1b0000b6 (diff)
downloadwarden-73d7bab8844bb21c7a9143c30800c2d11d411e42.tar.bz2
warden-73d7bab8844bb21c7a9143c30800c2d11d411e42.zip
feat: typology processor (#8)
Diffstat (limited to 'crates/typologies/src/processor/typology')
-rw-r--r--crates/typologies/src/processor/typology/aggregate_rules.rs202
-rw-r--r--crates/typologies/src/processor/typology/evaluate_expression.rs171
2 files changed, 373 insertions, 0 deletions
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<TypologyResult>, usize)> {
+ let mut typology_result: Vec<TypologyResult> = 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<f64> {
+ 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);
+ }
+}