Skip to main content

camel_api/
exchange.rs

1use std::any::Any;
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use opentelemetry::Context;
6use uuid::Uuid;
7
8use crate::error::CamelError;
9use crate::from_body::FromBody;
10use crate::message::Message;
11use crate::value::Value;
12
13/// Extension key used by use_original_message: stashes the pre-route Message
14/// so the error handler can restore body+headers before DLC dispatch.
15pub const ORIGINAL_MESSAGE_EXTENSION: &str = "CamelOriginalMessage";
16
17/// Pipeline-executor-recognized stop signal. Set by processors (ThrottleStrategy::Drop,
18/// SamplingService) that cannot return PipelineOutcome::Stopped directly because they
19/// are Tower `Service<Exchange>` (Process mode), not OutcomePipeline (Segment).
20/// The executor checks this after each step completion. See ADR-0024 amendment.
21pub const CAMEL_STOP: &str = "CamelStop";
22
23/// Check whether the exchange carries the CamelStop signal.
24///
25/// Returns `true` if the `CamelStop` property is set to a boolean `true` value.
26/// Used by the pipeline executor (`run_steps`) and segment adapters to detect
27/// Process-mode processors that cannot return `PipelineOutcome::Stopped` directly.
28pub fn is_camel_stop(exchange: &Exchange) -> bool {
29    exchange
30        .property(CAMEL_STOP)
31        .and_then(|v| v.as_bool())
32        .unwrap_or(false)
33}
34
35/// Property key for the exception message (error Display string).
36pub const PROPERTY_EXCEPTION_MESSAGE: &str = "CamelExceptionMessage";
37/// Property key for the exception kind (error variant name via classify()).
38pub const PROPERTY_EXCEPTION_KIND: &str = "CamelExceptionKind";
39/// Property key for the caught exception (Java Camel parity alias).
40pub const PROPERTY_EXCEPTION_CAUGHT: &str = "CamelExceptionCaught";
41/// Property key set when an exception has been handled (Java Camel parity).
42pub const PROPERTY_EXCEPTION_HANDLED: &str = "CamelExceptionHandled";
43
44/// The exchange pattern (fire-and-forget or request-reply).
45/// exhaustive-by-contract: the InOnly|InOut MEP dichotomy is a fixed, spec-level closed set.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum ExchangePattern {
48    /// Fire-and-forget: message sent, no reply expected.
49    #[default]
50    InOnly,
51    /// Request-reply: message sent, reply expected.
52    InOut,
53}
54
55/// An Exchange represents a message being routed through the framework.
56///
57/// It contains the input message, an optional output message,
58/// properties for passing data between processors, and error state.
59#[derive(Debug)]
60pub struct Exchange {
61    /// The input (incoming) message.
62    pub input: Message,
63    /// The output (response) message, populated for InOut patterns.
64    pub output: Option<Message>,
65    /// Exchange-scoped properties for passing data between processors.
66    pub properties: HashMap<String, Value>,
67    /// Non-serializable extension values (e.g., channel senders).
68    /// Stored as `Arc<dyn Any + Send + Sync>` so cloning is cheap (ref-count bump).
69    pub extensions: HashMap<String, Arc<dyn Any + Send + Sync>>,
70    /// Error state, if processing failed.
71    pub error: Option<CamelError>,
72    /// The exchange pattern.
73    pub pattern: ExchangePattern,
74    /// Unique correlation ID for distributed tracing.
75    pub correlation_id: String,
76    /// OpenTelemetry context for distributed tracing propagation.
77    /// Carries the active span context between processing steps.
78    /// Defaults to an empty context (noop span) if OTel is not active.
79    pub otel_context: Context,
80    /// RAII claim on the context-global in-flight gauge — the
81    /// accepted-not-completed counter (drainclaim, claimfamily). Pipeline
82    /// drain sites split a sibling
83    /// of the envelope's claim onto the exchange so residency inside
84    /// pipeline-embedded stash sites (resequencer buffers, aggregator
85    /// buckets — raw-`Exchange` territory with no envelope) stays
86    /// counted. `None` on every constructor, on clones, and on
87    /// exchanges accepted through uncounted paths.
88    pub in_flight_claim: Option<crate::in_flight::InFlightClaim>,
89}
90
91impl Exchange {
92    /// Create a new exchange with the given input message.
93    pub fn new(input: Message) -> Self {
94        Self {
95            input,
96            output: None,
97            properties: HashMap::new(),
98            extensions: HashMap::new(),
99            error: None,
100            pattern: ExchangePattern::default(),
101            correlation_id: Uuid::new_v4().to_string(),
102            otel_context: Context::new(),
103            in_flight_claim: None,
104        }
105    }
106
107    /// Create a new exchange with the InOut pattern.
108    pub fn new_in_out(input: Message) -> Self {
109        Self {
110            input,
111            output: None,
112            properties: HashMap::new(),
113            extensions: HashMap::new(),
114            error: None,
115            pattern: ExchangePattern::InOut,
116            correlation_id: Uuid::new_v4().to_string(),
117            otel_context: Context::new(),
118            in_flight_claim: None,
119        }
120    }
121
122    /// Get the correlation ID for this exchange.
123    pub fn correlation_id(&self) -> &str {
124        &self.correlation_id
125    }
126
127    /// Get a property value.
128    pub fn property(&self, key: &str) -> Option<&Value> {
129        self.properties.get(key)
130    }
131
132    /// Set a property value.
133    pub fn set_property(&mut self, key: impl Into<String>, value: impl Into<Value>) {
134        self.properties.insert(key.into(), value.into());
135    }
136
137    /// Check if the exchange has an error.
138    pub fn has_error(&self) -> bool {
139        self.error.is_some()
140    }
141
142    /// Set an error on this exchange.
143    ///
144    /// Automatically populates exchange properties with error context so that
145    /// all languages can access error information via their property mechanisms:
146    /// - `CamelExceptionMessage` — the error's Display string
147    /// - `CamelExceptionKind` — the error variant name (via `CamelError::classify()`)
148    /// - `CamelExceptionCaught` — alias for Java Camel parity
149    pub fn set_error(&mut self, error: CamelError) {
150        let msg = error.to_string();
151        let kind = error.classify().to_string();
152        self.properties.insert(
153            PROPERTY_EXCEPTION_MESSAGE.to_string(),
154            Value::String(msg.clone()),
155        );
156        self.properties
157            .insert(PROPERTY_EXCEPTION_KIND.to_string(), Value::String(kind));
158        self.properties
159            .insert(PROPERTY_EXCEPTION_CAUGHT.to_string(), Value::String(msg));
160        self.error = Some(error);
161    }
162
163    /// Clear the error and remove all exception properties.
164    ///
165    /// Called after `handled:true` in on_exception steps to prevent stale
166    /// error state from leaking into subsequent processing.
167    pub fn clear_error(&mut self) {
168        self.error = None;
169        self.properties.remove(PROPERTY_EXCEPTION_MESSAGE);
170        self.properties.remove(PROPERTY_EXCEPTION_KIND);
171        self.properties.remove(PROPERTY_EXCEPTION_CAUGHT);
172    }
173
174    /// Mark the exception as handled and clear the error.
175    ///
176    /// Sets `CamelExceptionHandled = true` then calls `clear_error()`.
177    /// This matches Java Camel's `Exchange.EXCEPTION_HANDLED` semantics.
178    pub fn handle_error(&mut self) {
179        self.properties
180            .insert(PROPERTY_EXCEPTION_HANDLED.to_string(), Value::Bool(true));
181        self.clear_error();
182    }
183
184    /// Store a non-serializable extension value (e.g. a channel sender).
185    pub fn set_extension(&mut self, key: impl Into<String>, value: Arc<dyn Any + Send + Sync>) {
186        self.extensions.insert(key.into(), value);
187    }
188
189    /// Retrieve a typed extension value. Returns `None` if the key is absent
190    /// or the stored value is not of type `T`.
191    pub fn get_extension<T: Any>(&self, key: &str) -> Option<&T> {
192        self.extensions.get(key)?.downcast_ref::<T>()
193    }
194
195    /// Deserialize the body into type `T`.
196    ///
197    /// Uses built-in conversions for `String`, `Vec<u8>`, [`bytes::Bytes`], and
198    /// `serde_json::Value`. For custom types, implement [`FromBody`] or use
199    /// the `impl_from_body_via_serde!` macro.
200    ///
201    /// # Example
202    /// ```rust,ignore
203    /// let text: String = exchange.body_as::<String>()?;
204    /// let raw: Vec<u8> = exchange.body_as::<Vec<u8>>()?;
205    /// ```
206    pub fn body_as<T: FromBody>(&self) -> Result<T, CamelError> {
207        T::from_body(&self.input.body)
208    }
209}
210
211impl Clone for Exchange {
212    fn clone(&self) -> Self {
213        Self {
214            input: self.input.clone(),
215            output: self.output.clone(),
216            properties: self.properties.clone(),
217            extensions: self.extensions.clone(), // Arc ref-count bump, cheap
218            error: self.error.clone(),
219            pattern: self.pattern,
220            correlation_id: self.correlation_id.clone(),
221            otel_context: self.otel_context.clone(),
222            // claimfamily: clones carry no claim — duplicating one would
223            // double-release on drop. Fanout sites that need each copy
224            // counted split a sibling explicitly.
225            in_flight_claim: None,
226        }
227    }
228}
229
230impl Default for Exchange {
231    fn default() -> Self {
232        Self::new(Message::default())
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::Body;
240    use serde_json::json;
241
242    #[test]
243    fn test_exchange_new() {
244        let msg = Message::new("test");
245        let ex = Exchange::new(msg);
246        assert_eq!(ex.input.body.as_text(), Some("test"));
247        assert!(ex.output.is_none());
248        assert!(!ex.has_error());
249        assert_eq!(ex.pattern, ExchangePattern::InOnly);
250    }
251
252    #[test]
253    fn test_exchange_in_out() {
254        let ex = Exchange::new_in_out(Message::default());
255        assert_eq!(ex.pattern, ExchangePattern::InOut);
256    }
257
258    #[test]
259    fn test_exchange_properties() {
260        let mut ex = Exchange::default();
261        ex.set_property("key", Value::Bool(true));
262        assert_eq!(ex.property("key"), Some(&Value::Bool(true)));
263        assert_eq!(ex.property("missing"), None);
264    }
265
266    #[test]
267    fn test_exchange_error() {
268        let mut ex = Exchange::default();
269        assert!(!ex.has_error());
270        ex.set_error(CamelError::ProcessorError("test".into()));
271        assert!(ex.has_error());
272    }
273
274    #[test]
275    fn test_set_error_populates_properties() {
276        let mut ex = Exchange::default();
277        ex.set_error(CamelError::ProcessorError("boom".into()));
278
279        assert!(ex.has_error());
280        assert_eq!(
281            ex.properties.get(PROPERTY_EXCEPTION_MESSAGE),
282            Some(&Value::String("Processor error: boom".to_string()))
283        );
284        assert_eq!(
285            ex.properties.get(PROPERTY_EXCEPTION_KIND),
286            Some(&Value::String("processor".to_string()))
287        );
288        assert_eq!(
289            ex.properties.get(PROPERTY_EXCEPTION_CAUGHT),
290            Some(&Value::String("Processor error: boom".to_string()))
291        );
292    }
293
294    #[test]
295    fn test_clear_error_removes_properties() {
296        let mut ex = Exchange::default();
297        ex.set_error(CamelError::RouteError("fail".into()));
298        assert!(ex.has_error());
299        assert!(ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
300
301        ex.clear_error();
302
303        assert!(!ex.has_error());
304        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
305        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_KIND));
306        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_CAUGHT));
307    }
308
309    #[test]
310    fn test_exchange_lifecycle() {
311        let mut ex = Exchange::new(Message::new("input data"));
312        assert_eq!(ex.input.body.as_text(), Some("input data"));
313
314        // Set some properties
315        ex.set_property("processed", Value::Bool(true));
316
317        // Set output
318        ex.output = Some(Message::new("output data"));
319        assert!(ex.output.is_some());
320
321        // Verify no error
322        assert!(!ex.has_error());
323    }
324
325    #[test]
326    fn test_exchange_otel_context_default() {
327        let ex = Exchange::default();
328        // Field must exist and be accessible — compilation is the test
329        // Also verify it's a fresh context (noop span)
330        use opentelemetry::trace::TraceContextExt;
331        assert!(!ex.otel_context.span().span_context().is_valid());
332    }
333
334    #[test]
335    fn test_exchange_otel_context_propagates_in_clone() {
336        let ex = Exchange::default();
337        let cloned = ex.clone();
338        // Both should have the same (empty) context
339        use opentelemetry::trace::TraceContextExt;
340        assert!(!cloned.otel_context.span().span_context().is_valid());
341    }
342
343    #[test]
344    fn test_set_and_get_extension() {
345        use std::sync::Arc;
346        let mut ex = Exchange::default();
347        ex.set_extension("my.key", Arc::new(42u32));
348        let val: Option<&u32> = ex.get_extension("my.key");
349        assert_eq!(val, Some(&42u32));
350    }
351
352    #[test]
353    fn test_get_extension_wrong_type_returns_none() {
354        use std::sync::Arc;
355        let mut ex = Exchange::default();
356        ex.set_extension("my.key", Arc::new(42u32));
357        let val: Option<&String> = ex.get_extension("my.key");
358        assert!(val.is_none());
359    }
360
361    #[test]
362    fn test_get_extension_missing_key_returns_none() {
363        let ex = Exchange::default();
364        let val: Option<&u32> = ex.get_extension("nope");
365        assert!(val.is_none());
366    }
367
368    #[test]
369    fn test_clone_shares_extension_arc() {
370        use std::sync::Arc;
371        let mut ex = Exchange::default();
372        ex.set_extension("shared", Arc::new(99u64));
373        let cloned = ex.clone();
374        // Both see the same value
375        assert_eq!(ex.get_extension::<u64>("shared"), Some(&99u64));
376        assert_eq!(cloned.get_extension::<u64>("shared"), Some(&99u64));
377    }
378
379    #[test]
380    fn test_body_as_string_from_text() {
381        let ex = Exchange::new(Message::new(Body::Text("hello".to_string())));
382
383        let result = ex.body_as::<String>();
384
385        assert_eq!(result.unwrap(), "hello");
386    }
387
388    #[test]
389    fn test_body_as_string_from_json_string() {
390        let ex = Exchange::new(Message::new(Body::Json(json!("hello"))));
391
392        let result = ex.body_as::<String>();
393
394        assert_eq!(result.unwrap(), "hello");
395    }
396
397    #[test]
398    fn test_body_as_json_value_from_json_number() {
399        let ex = Exchange::new(Message::new(Body::Json(json!(42))));
400
401        let result = ex.body_as::<serde_json::Value>();
402
403        assert_eq!(result.unwrap(), json!(42));
404    }
405
406    #[test]
407    fn test_body_as_vec_u8_from_bytes() {
408        let ex = Exchange::new(Message::new(Body::from(vec![1u8, 2, 3, 4])));
409
410        let result = ex.body_as::<Vec<u8>>();
411
412        assert_eq!(result.unwrap(), vec![1u8, 2, 3, 4]);
413    }
414
415    #[test]
416    fn test_body_as_string_from_empty_returns_err() {
417        let ex = Exchange::new(Message::new(Body::Empty));
418
419        let result = ex.body_as::<String>();
420
421        assert!(result.is_err());
422    }
423}