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 accepted-not-completed counter
81    /// (drainclaim, claimfamily). Pipeline drain sites split a sibling
82    /// of the envelope's claim onto the exchange so residency inside
83    /// pipeline-embedded stash sites (resequencer buffers, aggregator
84    /// buckets — raw-`Exchange` territory with no envelope) stays
85    /// counted. `None` on every constructor, on clones, and on
86    /// exchanges accepted through uncounted paths.
87    pub in_flight_claim: Option<crate::in_flight::InFlightClaim>,
88}
89
90impl Exchange {
91    /// Create a new exchange with the given input message.
92    pub fn new(input: Message) -> Self {
93        Self {
94            input,
95            output: None,
96            properties: HashMap::new(),
97            extensions: HashMap::new(),
98            error: None,
99            pattern: ExchangePattern::default(),
100            correlation_id: Uuid::new_v4().to_string(),
101            otel_context: Context::new(),
102            in_flight_claim: None,
103        }
104    }
105
106    /// Create a new exchange with the InOut pattern.
107    pub fn new_in_out(input: Message) -> Self {
108        Self {
109            input,
110            output: None,
111            properties: HashMap::new(),
112            extensions: HashMap::new(),
113            error: None,
114            pattern: ExchangePattern::InOut,
115            correlation_id: Uuid::new_v4().to_string(),
116            otel_context: Context::new(),
117            in_flight_claim: None,
118        }
119    }
120
121    /// Get the correlation ID for this exchange.
122    pub fn correlation_id(&self) -> &str {
123        &self.correlation_id
124    }
125
126    /// Get a property value.
127    pub fn property(&self, key: &str) -> Option<&Value> {
128        self.properties.get(key)
129    }
130
131    /// Set a property value.
132    pub fn set_property(&mut self, key: impl Into<String>, value: impl Into<Value>) {
133        self.properties.insert(key.into(), value.into());
134    }
135
136    /// Check if the exchange has an error.
137    pub fn has_error(&self) -> bool {
138        self.error.is_some()
139    }
140
141    /// Set an error on this exchange.
142    ///
143    /// Automatically populates exchange properties with error context so that
144    /// all languages can access error information via their property mechanisms:
145    /// - `CamelExceptionMessage` — the error's Display string
146    /// - `CamelExceptionKind` — the error variant name (via `CamelError::classify()`)
147    /// - `CamelExceptionCaught` — alias for Java Camel parity
148    pub fn set_error(&mut self, error: CamelError) {
149        let msg = error.to_string();
150        let kind = error.classify().to_string();
151        self.properties.insert(
152            PROPERTY_EXCEPTION_MESSAGE.to_string(),
153            Value::String(msg.clone()),
154        );
155        self.properties
156            .insert(PROPERTY_EXCEPTION_KIND.to_string(), Value::String(kind));
157        self.properties
158            .insert(PROPERTY_EXCEPTION_CAUGHT.to_string(), Value::String(msg));
159        self.error = Some(error);
160    }
161
162    /// Clear the error and remove all exception properties.
163    ///
164    /// Called after `handled:true` in on_exception steps to prevent stale
165    /// error state from leaking into subsequent processing.
166    pub fn clear_error(&mut self) {
167        self.error = None;
168        self.properties.remove(PROPERTY_EXCEPTION_MESSAGE);
169        self.properties.remove(PROPERTY_EXCEPTION_KIND);
170        self.properties.remove(PROPERTY_EXCEPTION_CAUGHT);
171    }
172
173    /// Mark the exception as handled and clear the error.
174    ///
175    /// Sets `CamelExceptionHandled = true` then calls `clear_error()`.
176    /// This matches Java Camel's `Exchange.EXCEPTION_HANDLED` semantics.
177    pub fn handle_error(&mut self) {
178        self.properties
179            .insert(PROPERTY_EXCEPTION_HANDLED.to_string(), Value::Bool(true));
180        self.clear_error();
181    }
182
183    /// Store a non-serializable extension value (e.g. a channel sender).
184    pub fn set_extension(&mut self, key: impl Into<String>, value: Arc<dyn Any + Send + Sync>) {
185        self.extensions.insert(key.into(), value);
186    }
187
188    /// Retrieve a typed extension value. Returns `None` if the key is absent
189    /// or the stored value is not of type `T`.
190    pub fn get_extension<T: Any>(&self, key: &str) -> Option<&T> {
191        self.extensions.get(key)?.downcast_ref::<T>()
192    }
193
194    /// Deserialize the body into type `T`.
195    ///
196    /// Uses built-in conversions for `String`, `Vec<u8>`, [`bytes::Bytes`], and
197    /// `serde_json::Value`. For custom types, implement [`FromBody`] or use
198    /// the `impl_from_body_via_serde!` macro.
199    ///
200    /// # Example
201    /// ```rust,ignore
202    /// let text: String = exchange.body_as::<String>()?;
203    /// let raw: Vec<u8> = exchange.body_as::<Vec<u8>>()?;
204    /// ```
205    pub fn body_as<T: FromBody>(&self) -> Result<T, CamelError> {
206        T::from_body(&self.input.body)
207    }
208}
209
210impl Clone for Exchange {
211    fn clone(&self) -> Self {
212        Self {
213            input: self.input.clone(),
214            output: self.output.clone(),
215            properties: self.properties.clone(),
216            extensions: self.extensions.clone(), // Arc ref-count bump, cheap
217            error: self.error.clone(),
218            pattern: self.pattern,
219            correlation_id: self.correlation_id.clone(),
220            otel_context: self.otel_context.clone(),
221            // claimfamily: clones carry no claim — duplicating one would
222            // double-release on drop. Fanout sites that need each copy
223            // counted split a sibling explicitly.
224            in_flight_claim: None,
225        }
226    }
227}
228
229impl Default for Exchange {
230    fn default() -> Self {
231        Self::new(Message::default())
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::Body;
239    use serde_json::json;
240
241    #[test]
242    fn test_exchange_new() {
243        let msg = Message::new("test");
244        let ex = Exchange::new(msg);
245        assert_eq!(ex.input.body.as_text(), Some("test"));
246        assert!(ex.output.is_none());
247        assert!(!ex.has_error());
248        assert_eq!(ex.pattern, ExchangePattern::InOnly);
249    }
250
251    #[test]
252    fn test_exchange_in_out() {
253        let ex = Exchange::new_in_out(Message::default());
254        assert_eq!(ex.pattern, ExchangePattern::InOut);
255    }
256
257    #[test]
258    fn test_exchange_properties() {
259        let mut ex = Exchange::default();
260        ex.set_property("key", Value::Bool(true));
261        assert_eq!(ex.property("key"), Some(&Value::Bool(true)));
262        assert_eq!(ex.property("missing"), None);
263    }
264
265    #[test]
266    fn test_exchange_error() {
267        let mut ex = Exchange::default();
268        assert!(!ex.has_error());
269        ex.set_error(CamelError::ProcessorError("test".into()));
270        assert!(ex.has_error());
271    }
272
273    #[test]
274    fn test_set_error_populates_properties() {
275        let mut ex = Exchange::default();
276        ex.set_error(CamelError::ProcessorError("boom".into()));
277
278        assert!(ex.has_error());
279        assert_eq!(
280            ex.properties.get(PROPERTY_EXCEPTION_MESSAGE),
281            Some(&Value::String("Processor error: boom".to_string()))
282        );
283        assert_eq!(
284            ex.properties.get(PROPERTY_EXCEPTION_KIND),
285            Some(&Value::String("processor".to_string()))
286        );
287        assert_eq!(
288            ex.properties.get(PROPERTY_EXCEPTION_CAUGHT),
289            Some(&Value::String("Processor error: boom".to_string()))
290        );
291    }
292
293    #[test]
294    fn test_clear_error_removes_properties() {
295        let mut ex = Exchange::default();
296        ex.set_error(CamelError::RouteError("fail".into()));
297        assert!(ex.has_error());
298        assert!(ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
299
300        ex.clear_error();
301
302        assert!(!ex.has_error());
303        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
304        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_KIND));
305        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_CAUGHT));
306    }
307
308    #[test]
309    fn test_exchange_lifecycle() {
310        let mut ex = Exchange::new(Message::new("input data"));
311        assert_eq!(ex.input.body.as_text(), Some("input data"));
312
313        // Set some properties
314        ex.set_property("processed", Value::Bool(true));
315
316        // Set output
317        ex.output = Some(Message::new("output data"));
318        assert!(ex.output.is_some());
319
320        // Verify no error
321        assert!(!ex.has_error());
322    }
323
324    #[test]
325    fn test_exchange_otel_context_default() {
326        let ex = Exchange::default();
327        // Field must exist and be accessible — compilation is the test
328        // Also verify it's a fresh context (noop span)
329        use opentelemetry::trace::TraceContextExt;
330        assert!(!ex.otel_context.span().span_context().is_valid());
331    }
332
333    #[test]
334    fn test_exchange_otel_context_propagates_in_clone() {
335        let ex = Exchange::default();
336        let cloned = ex.clone();
337        // Both should have the same (empty) context
338        use opentelemetry::trace::TraceContextExt;
339        assert!(!cloned.otel_context.span().span_context().is_valid());
340    }
341
342    #[test]
343    fn test_set_and_get_extension() {
344        use std::sync::Arc;
345        let mut ex = Exchange::default();
346        ex.set_extension("my.key", Arc::new(42u32));
347        let val: Option<&u32> = ex.get_extension("my.key");
348        assert_eq!(val, Some(&42u32));
349    }
350
351    #[test]
352    fn test_get_extension_wrong_type_returns_none() {
353        use std::sync::Arc;
354        let mut ex = Exchange::default();
355        ex.set_extension("my.key", Arc::new(42u32));
356        let val: Option<&String> = ex.get_extension("my.key");
357        assert!(val.is_none());
358    }
359
360    #[test]
361    fn test_get_extension_missing_key_returns_none() {
362        let ex = Exchange::default();
363        let val: Option<&u32> = ex.get_extension("nope");
364        assert!(val.is_none());
365    }
366
367    #[test]
368    fn test_clone_shares_extension_arc() {
369        use std::sync::Arc;
370        let mut ex = Exchange::default();
371        ex.set_extension("shared", Arc::new(99u64));
372        let cloned = ex.clone();
373        // Both see the same value
374        assert_eq!(ex.get_extension::<u64>("shared"), Some(&99u64));
375        assert_eq!(cloned.get_extension::<u64>("shared"), Some(&99u64));
376    }
377
378    #[test]
379    fn test_body_as_string_from_text() {
380        let ex = Exchange::new(Message::new(Body::Text("hello".to_string())));
381
382        let result = ex.body_as::<String>();
383
384        assert_eq!(result.unwrap(), "hello");
385    }
386
387    #[test]
388    fn test_body_as_string_from_json_string() {
389        let ex = Exchange::new(Message::new(Body::Json(json!("hello"))));
390
391        let result = ex.body_as::<String>();
392
393        assert_eq!(result.unwrap(), "hello");
394    }
395
396    #[test]
397    fn test_body_as_json_value_from_json_number() {
398        let ex = Exchange::new(Message::new(Body::Json(json!(42))));
399
400        let result = ex.body_as::<serde_json::Value>();
401
402        assert_eq!(result.unwrap(), json!(42));
403    }
404
405    #[test]
406    fn test_body_as_vec_u8_from_bytes() {
407        let ex = Exchange::new(Message::new(Body::from(vec![1u8, 2, 3, 4])));
408
409        let result = ex.body_as::<Vec<u8>>();
410
411        assert_eq!(result.unwrap(), vec![1u8, 2, 3, 4]);
412    }
413
414    #[test]
415    fn test_body_as_string_from_empty_returns_err() {
416        let ex = Exchange::new(Message::new(Body::Empty));
417
418        let result = ex.body_as::<String>();
419
420        assert!(result.is_err());
421    }
422}