Skip to main content

camel_core/
intercept.rs

1//! Route send-point interception rules.
2
3use camel_api::CamelError;
4
5/// Rule that maps a send URI to an interception action.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct InterceptRule {
8    /// Source URI to intercept (exact match).
9    pub uri: String,
10    /// Action to apply when the source matches.
11    pub action: InterceptAction,
12}
13
14/// Action to apply when a send URI matches a rule.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum InterceptAction {
17    /// Skip the original send and redirect to the target.
18    SkipTo { uri: String },
19    /// Copy the exchange to the target and continue to the real destination.
20    DivertCopyTo { uri: String },
21}
22
23/// Ordered collection of interception rules.
24#[derive(Debug, Clone, Default)]
25pub struct InterceptRules {
26    rules: Vec<InterceptRule>,
27}
28
29impl InterceptRules {
30    /// Create rules from a vector.
31    ///
32    /// Validates that every action target starts with `mock:`.
33    /// Returns `CamelError::Config` that contains the rule index and the
34    /// offending target URI when validation fails.
35    pub fn new(rules: Vec<InterceptRule>) -> Result<Self, CamelError> {
36        for (idx, rule) in rules.iter().enumerate() {
37            let target = match &rule.action {
38                InterceptAction::SkipTo { uri } => uri,
39                InterceptAction::DivertCopyTo { uri } => uri,
40            };
41            if !target.starts_with("mock:") {
42                return Err(CamelError::Config(format!(
43                    "rule {idx}: intercept target '{target}' must start with 'mock:'"
44                )));
45            }
46        }
47        Ok(Self { rules })
48    }
49
50    /// Return the first matching action for `send_uri`.
51    ///
52    /// Uses exact string equality and respects declaration order.
53    pub fn lookup(&self, send_uri: &str) -> Option<&InterceptAction> {
54        for rule in &self.rules {
55            if rule.uri == send_uri {
56                return Some(&rule.action);
57            }
58        }
59        None
60    }
61
62    /// Return true when no rules are present.
63    pub fn is_empty(&self) -> bool {
64        self.rules.is_empty()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn non_mock_action_targets_are_rejected_at_rule_construction() {
74        let skip_bad = InterceptRule {
75            uri: "kafka:x".into(),
76            action: InterceptAction::SkipTo {
77                uri: "direct:y".into(),
78            },
79        };
80        let divert_bad = InterceptRule {
81            uri: "kafka:z".into(),
82            action: InterceptAction::DivertCopyTo {
83                uri: "seda:w".into(),
84            },
85        };
86
87        let err = InterceptRules::new(vec![skip_bad.clone()]).unwrap_err(); // allow-unwrap
88        match err {
89            CamelError::Config(msg) => {
90                assert!(msg.contains("0"));
91                assert!(msg.contains("direct:y"));
92            }
93            other => panic!("expected Config, got {other:?}"),
94        }
95
96        let err = InterceptRules::new(vec![divert_bad.clone()]).unwrap_err(); // allow-unwrap
97        match err {
98            CamelError::Config(msg) => {
99                assert!(msg.contains("0"));
100                assert!(msg.contains("seda:w"));
101            }
102            other => panic!("expected Config, got {other:?}"),
103        }
104
105        let err = InterceptRules::new(vec![skip_bad.clone(), divert_bad.clone()]).unwrap_err(); // allow-unwrap
106        match err {
107            CamelError::Config(msg) => {
108                assert!(msg.contains("0"));
109                assert!(msg.contains("direct:y"));
110            }
111            other => panic!("expected Config, got {other:?}"),
112        }
113
114        // Rule-index propagation must reflect position, not a hardcoded "0".
115        let valid = InterceptRule {
116            uri: "kafka:ok".into(),
117            action: InterceptAction::SkipTo {
118                uri: "mock:ok".into(),
119            },
120        };
121        let err = InterceptRules::new(vec![valid, divert_bad]).unwrap_err(); // allow-unwrap
122        match err {
123            CamelError::Config(msg) => {
124                assert!(msg.contains("rule 1:"));
125                assert!(msg.contains("seda:w"));
126            }
127            other => panic!("expected Config, got {other:?}"),
128        }
129    }
130
131    #[test]
132    fn duplicate_uris_preserve_declaration_order() {
133        assert!(InterceptRules::default().is_empty());
134        assert!(InterceptRules::default().lookup("x").is_none());
135
136        let r1 = InterceptRule {
137            uri: "seda:out".into(),
138            action: InterceptAction::SkipTo {
139                uri: "mock:a".into(),
140            },
141        };
142        let r2 = InterceptRule {
143            uri: "seda:out".into(),
144            action: InterceptAction::SkipTo {
145                uri: "mock:b".into(),
146            },
147        };
148        let rules = InterceptRules::new(vec![r1, r2]).expect("valid mock targets"); // allow-unwrap
149        assert_eq!(
150            rules.lookup("seda:out"),
151            Some(&InterceptAction::SkipTo {
152                uri: "mock:a".into()
153            })
154        );
155        assert_eq!(rules.lookup("seda:out2"), None);
156    }
157
158    #[test]
159    fn mock_targets_accepted() {
160        let rules = InterceptRules::new(vec![
161            InterceptRule {
162                uri: "kafka:x".into(),
163                action: InterceptAction::SkipTo {
164                    uri: "mock:y".into(),
165                },
166            },
167            InterceptRule {
168                uri: "kafka:z".into(),
169                action: InterceptAction::DivertCopyTo {
170                    uri: "mock:w".into(),
171                },
172            },
173        ])
174        .expect("valid mock targets"); // allow-unwrap
175        assert_eq!(
176            rules.lookup("kafka:x"),
177            Some(&InterceptAction::SkipTo {
178                uri: "mock:y".into()
179            })
180        );
181        assert_eq!(
182            rules.lookup("kafka:z"),
183            Some(&InterceptAction::DivertCopyTo {
184                uri: "mock:w".into()
185            })
186        );
187    }
188}