aboutsummaryrefslogtreecommitdiffstats
path: root/crates/typologies/src/processor/typology/aggregate_rules.rs
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/aggregate_rules.rs
parent725739985d853b07d73fa7fcd6db1f2f1b0000b6 (diff)
downloadwarden-73d7bab8844bb21c7a9143c30800c2d11d411e42.tar.bz2
warden-73d7bab8844bb21c7a9143c30800c2d11d411e42.zip
feat: typology processor (#8)
Diffstat (limited to 'crates/typologies/src/processor/typology/aggregate_rules.rs')
-rw-r--r--crates/typologies/src/processor/typology/aggregate_rules.rs202
1 files changed, 202 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");
+ }
+}