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    ///
150    /// For `HttpOperationFailed` it also sets the `CamelHttpResponseCode` and
151    /// `CamelHttpResponseText` headers, because a failing producer loses its
152    /// exchange with the `Err` and error-handling expressions would otherwise
153    /// have no way to read the status.
154    pub fn set_error(&mut self, error: CamelError) {
155        if let CamelError::HttpOperationFailed {
156            status_code,
157            status_text,
158            ..
159        } = &error
160        {
161            self.input.set_header(
162                "CamelHttpResponseCode",
163                Value::Number((*status_code).into()),
164            );
165            self.input
166                .set_header("CamelHttpResponseText", Value::String(status_text.clone()));
167        }
168        let msg = error.to_string();
169        let kind = error.classify().to_string();
170        self.properties.insert(
171            PROPERTY_EXCEPTION_MESSAGE.to_string(),
172            Value::String(msg.clone()),
173        );
174        self.properties
175            .insert(PROPERTY_EXCEPTION_KIND.to_string(), Value::String(kind));
176        self.properties
177            .insert(PROPERTY_EXCEPTION_CAUGHT.to_string(), Value::String(msg));
178        self.error = Some(error);
179    }
180
181    /// Clear the error and remove all exception properties.
182    ///
183    /// Called after `handled:true` in on_exception steps to prevent stale
184    /// error state from leaking into subsequent processing.
185    pub fn clear_error(&mut self) {
186        self.error = None;
187        self.properties.remove(PROPERTY_EXCEPTION_MESSAGE);
188        self.properties.remove(PROPERTY_EXCEPTION_KIND);
189        self.properties.remove(PROPERTY_EXCEPTION_CAUGHT);
190    }
191
192    /// Mark the exception as handled and clear the error.
193    ///
194    /// Sets `CamelExceptionHandled = true` then calls `clear_error()`.
195    /// This matches Java Camel's `Exchange.EXCEPTION_HANDLED` semantics.
196    pub fn handle_error(&mut self) {
197        self.properties
198            .insert(PROPERTY_EXCEPTION_HANDLED.to_string(), Value::Bool(true));
199        self.clear_error();
200    }
201
202    /// Store a non-serializable extension value (e.g. a channel sender).
203    pub fn set_extension(&mut self, key: impl Into<String>, value: Arc<dyn Any + Send + Sync>) {
204        self.extensions.insert(key.into(), value);
205    }
206
207    /// Retrieve a typed extension value. Returns `None` if the key is absent
208    /// or the stored value is not of type `T`.
209    pub fn get_extension<T: Any>(&self, key: &str) -> Option<&T> {
210        self.extensions.get(key)?.downcast_ref::<T>()
211    }
212
213    /// Deserialize the body into type `T`.
214    ///
215    /// Uses built-in conversions for `String`, `Vec<u8>`, [`bytes::Bytes`], and
216    /// `serde_json::Value`. For custom types, implement [`FromBody`] or use
217    /// the `impl_from_body_via_serde!` macro.
218    ///
219    /// # Example
220    /// ```rust,ignore
221    /// let text: String = exchange.body_as::<String>()?;
222    /// let raw: Vec<u8> = exchange.body_as::<Vec<u8>>()?;
223    /// ```
224    pub fn body_as<T: FromBody>(&self) -> Result<T, CamelError> {
225        T::from_body(&self.input.body)
226    }
227
228    /// Clone for a fanout branch copy. Unlike [`Exchange::clone`], a
229    /// carried in-flight claim is split onto the copy (claimfamily):
230    /// the sibling counts the copy independently and releases when the
231    /// copy completes; the original claim stays live.
232    pub fn branch_clone(&self) -> Self {
233        let mut copy = self.clone();
234        copy.in_flight_claim = self.in_flight_claim.as_ref().map(|claim| claim.split());
235        copy
236    }
237}
238
239impl Clone for Exchange {
240    fn clone(&self) -> Self {
241        Self {
242            input: self.input.clone(),
243            output: self.output.clone(),
244            properties: self.properties.clone(),
245            extensions: self.extensions.clone(), // Arc ref-count bump, cheap
246            error: self.error.clone(),
247            pattern: self.pattern,
248            correlation_id: self.correlation_id.clone(),
249            otel_context: self.otel_context.clone(),
250            // claimfamily: clones carry no claim — duplicating one would
251            // double-release on drop. Fanout sites that need each copy
252            // counted split a sibling explicitly.
253            in_flight_claim: None,
254        }
255    }
256}
257
258impl Default for Exchange {
259    fn default() -> Self {
260        Self::new(Message::default())
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::Body;
268    use crate::in_flight::{InFlightClaim, InFlightGauge};
269    use serde_json::json;
270    use std::sync::Arc;
271
272    #[test]
273    fn test_exchange_new() {
274        let msg = Message::new("test");
275        let ex = Exchange::new(msg);
276        assert_eq!(ex.input.body.as_text(), Some("test"));
277        assert!(ex.output.is_none());
278        assert!(!ex.has_error());
279        assert_eq!(ex.pattern, ExchangePattern::InOnly);
280    }
281
282    #[test]
283    fn test_exchange_in_out() {
284        let ex = Exchange::new_in_out(Message::default());
285        assert_eq!(ex.pattern, ExchangePattern::InOut);
286    }
287
288    #[test]
289    fn test_exchange_properties() {
290        let mut ex = Exchange::default();
291        ex.set_property("key", Value::Bool(true));
292        assert_eq!(ex.property("key"), Some(&Value::Bool(true)));
293        assert_eq!(ex.property("missing"), None);
294    }
295
296    #[test]
297    fn test_exchange_error() {
298        let mut ex = Exchange::default();
299        assert!(!ex.has_error());
300        ex.set_error(CamelError::ProcessorError("test".into()));
301        assert!(ex.has_error());
302    }
303
304    #[test]
305    fn test_set_error_populates_properties() {
306        let mut ex = Exchange::default();
307        ex.set_error(CamelError::ProcessorError("boom".into()));
308
309        assert!(ex.has_error());
310        assert_eq!(
311            ex.properties.get(PROPERTY_EXCEPTION_MESSAGE),
312            Some(&Value::String("Processor error: boom".to_string()))
313        );
314        assert_eq!(
315            ex.properties.get(PROPERTY_EXCEPTION_KIND),
316            Some(&Value::String("processor".to_string()))
317        );
318        assert_eq!(
319            ex.properties.get(PROPERTY_EXCEPTION_CAUGHT),
320            Some(&Value::String("Processor error: boom".to_string()))
321        );
322    }
323
324    #[test]
325    fn test_set_error_http_failure_exposes_status_headers() {
326        let mut ex = Exchange::default();
327        ex.set_error(CamelError::HttpOperationFailed {
328            method: "GET".into(),
329            url: "http://example.test/x".into(),
330            status_code: 404,
331            status_text: "Not Found".into(),
332            response_body: None,
333        });
334
335        assert_eq!(
336            ex.input.header("CamelHttpResponseCode"),
337            Some(&Value::Number(404.into()))
338        );
339        assert_eq!(
340            ex.input.header("CamelHttpResponseText"),
341            Some(&Value::String("Not Found".to_string()))
342        );
343    }
344
345    #[test]
346    fn test_set_error_non_http_leaves_http_headers_untouched() {
347        let mut ex = Exchange::default();
348        ex.set_error(CamelError::ProcessorError("boom".into()));
349
350        assert!(ex.input.header("CamelHttpResponseCode").is_none());
351        assert!(ex.input.header("CamelHttpResponseText").is_none());
352    }
353
354    #[test]
355    fn test_clear_error_removes_properties() {
356        let mut ex = Exchange::default();
357        ex.set_error(CamelError::RouteError("fail".into()));
358        assert!(ex.has_error());
359        assert!(ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
360
361        ex.clear_error();
362
363        assert!(!ex.has_error());
364        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
365        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_KIND));
366        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_CAUGHT));
367    }
368
369    #[test]
370    fn test_exchange_lifecycle() {
371        let mut ex = Exchange::new(Message::new("input data"));
372        assert_eq!(ex.input.body.as_text(), Some("input data"));
373
374        // Set some properties
375        ex.set_property("processed", Value::Bool(true));
376
377        // Set output
378        ex.output = Some(Message::new("output data"));
379        assert!(ex.output.is_some());
380
381        // Verify no error
382        assert!(!ex.has_error());
383    }
384
385    #[test]
386    fn test_exchange_otel_context_default() {
387        let ex = Exchange::default();
388        // Field must exist and be accessible — compilation is the test
389        // Also verify it's a fresh context (noop span)
390        use opentelemetry::trace::TraceContextExt;
391        assert!(!ex.otel_context.span().span_context().is_valid());
392    }
393
394    #[test]
395    fn test_exchange_otel_context_propagates_in_clone() {
396        let ex = Exchange::default();
397        let cloned = ex.clone();
398        // Both should have the same (empty) context
399        use opentelemetry::trace::TraceContextExt;
400        assert!(!cloned.otel_context.span().span_context().is_valid());
401    }
402
403    #[test]
404    fn test_set_and_get_extension() {
405        use std::sync::Arc;
406        let mut ex = Exchange::default();
407        ex.set_extension("my.key", Arc::new(42u32));
408        let val: Option<&u32> = ex.get_extension("my.key");
409        assert_eq!(val, Some(&42u32));
410    }
411
412    #[test]
413    fn test_get_extension_wrong_type_returns_none() {
414        use std::sync::Arc;
415        let mut ex = Exchange::default();
416        ex.set_extension("my.key", Arc::new(42u32));
417        let val: Option<&String> = ex.get_extension("my.key");
418        assert!(val.is_none());
419    }
420
421    #[test]
422    fn test_get_extension_missing_key_returns_none() {
423        let ex = Exchange::default();
424        let val: Option<&u32> = ex.get_extension("nope");
425        assert!(val.is_none());
426    }
427
428    #[test]
429    fn test_clone_shares_extension_arc() {
430        use std::sync::Arc;
431        let mut ex = Exchange::default();
432        ex.set_extension("shared", Arc::new(99u64));
433        let cloned = ex.clone();
434        // Both see the same value
435        assert_eq!(ex.get_extension::<u64>("shared"), Some(&99u64));
436        assert_eq!(cloned.get_extension::<u64>("shared"), Some(&99u64));
437    }
438
439    #[test]
440    fn test_body_as_string_from_text() {
441        let ex = Exchange::new(Message::new(Body::Text("hello".to_string())));
442
443        let result = ex.body_as::<String>();
444
445        assert_eq!(result.unwrap(), "hello");
446    }
447
448    #[test]
449    fn test_body_as_string_from_json_string() {
450        let ex = Exchange::new(Message::new(Body::Json(json!("hello"))));
451
452        let result = ex.body_as::<String>();
453
454        assert_eq!(result.unwrap(), "hello");
455    }
456
457    #[test]
458    fn test_body_as_json_value_from_json_number() {
459        let ex = Exchange::new(Message::new(Body::Json(json!(42))));
460
461        let result = ex.body_as::<serde_json::Value>();
462
463        assert_eq!(result.unwrap(), json!(42));
464    }
465
466    #[test]
467    fn test_body_as_vec_u8_from_bytes() {
468        let ex = Exchange::new(Message::new(Body::from(vec![1u8, 2, 3, 4])));
469
470        let result = ex.body_as::<Vec<u8>>();
471
472        assert_eq!(result.unwrap(), vec![1u8, 2, 3, 4]);
473    }
474
475    #[test]
476    fn test_body_as_string_from_empty_returns_err() {
477        let ex = Exchange::new(Message::new(Body::Empty));
478
479        let result = ex.body_as::<String>();
480
481        assert!(result.is_err());
482    }
483
484    #[test]
485    fn branch_clone_splits_claim_original_unaffected() {
486        let gauge = Arc::new(InFlightGauge::new());
487        let mut src = Exchange::new(Message::default());
488        src.in_flight_claim = Some(InFlightClaim::attach(&gauge));
489
490        let copy = src.branch_clone();
491
492        assert_eq!(gauge.total(), 2);
493        assert!(copy.in_flight_claim.is_some());
494        assert!(src.in_flight_claim.is_some());
495    }
496
497    #[test]
498    fn branch_clone_sibling_releases_independently() {
499        let gauge = Arc::new(InFlightGauge::new());
500        let mut src = Exchange::new(Message::default());
501        src.in_flight_claim = Some(InFlightClaim::attach(&gauge));
502
503        let copy = src.branch_clone();
504        drop(copy);
505        assert_eq!(gauge.total(), 1);
506
507        drop(src);
508        assert_eq!(gauge.total(), 0);
509    }
510
511    #[test]
512    fn plain_clone_carries_no_claim() {
513        let gauge = Arc::new(InFlightGauge::new());
514        let mut src = Exchange::new(Message::default());
515        src.in_flight_claim = Some(InFlightClaim::attach(&gauge));
516        assert_eq!(gauge.total(), 1);
517
518        let copy = src.clone();
519
520        assert_eq!(gauge.total(), 1);
521        assert!(copy.in_flight_claim.is_none());
522        assert!(src.in_flight_claim.is_some());
523    }
524}