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/// Extension key for the Content-Type provenance snapshot taken with
18/// `ORIGINAL_MESSAGE_EXTENSION` at route entry when `use_original_message` is
19/// enabled. Capture replaces the snapshot unconditionally; restore reads it to
20/// rebuild the inherited marker.
21pub const ORIGINAL_CONTENT_TYPE_INHERITED_EXTENSION: &str = "CamelOriginalContentTypeInherited";
22
23/// Marker-presence snapshot stored under `ORIGINAL_CONTENT_TYPE_INHERITED_EXTENSION`.
24/// Stored as `Arc<OriginalContentTypeInherited>` via `Exchange::set_extension`.
25pub struct OriginalContentTypeInherited(pub bool);
26
27/// Exchange property marking that the current `Content-Type` was inherited from
28/// the incoming request rather than written by a route step.
29///
30/// Set by the HTTP consumer at ingress and cleared by any route step that
31/// writes or removes `Content-Type`, so downstream components can suppress
32/// value-equality checks in favour of provenance.
33pub const PROPERTY_HTTP_CONTENT_TYPE_INHERITED: &str = "CamelHttpContentTypeInherited";
34
35/// Pipeline-executor-recognized stop signal. Set by processors (ThrottleStrategy::Drop,
36/// SamplingService) that cannot return PipelineOutcome::Stopped directly because they
37/// are Tower `Service<Exchange>` (Process mode), not OutcomePipeline (Segment).
38/// The executor checks this after each step completion. See ADR-0024 amendment.
39pub const CAMEL_STOP: &str = "CamelStop";
40
41/// Check whether the exchange carries the CamelStop signal.
42///
43/// Returns `true` if the `CamelStop` property is set to a boolean `true` value.
44/// Used by the pipeline executor (`run_steps`) and segment adapters to detect
45/// Process-mode processors that cannot return `PipelineOutcome::Stopped` directly.
46pub fn is_camel_stop(exchange: &Exchange) -> bool {
47    exchange
48        .property(CAMEL_STOP)
49        .and_then(|v| v.as_bool())
50        .unwrap_or(false)
51}
52
53/// Property key for the exception message (error Display string).
54pub const PROPERTY_EXCEPTION_MESSAGE: &str = "CamelExceptionMessage";
55/// Property key for the exception kind (error variant name via classify()).
56pub const PROPERTY_EXCEPTION_KIND: &str = "CamelExceptionKind";
57/// Property key for the caught exception (Java Camel parity alias).
58pub const PROPERTY_EXCEPTION_CAUGHT: &str = "CamelExceptionCaught";
59/// Property key set when an exception has been handled (Java Camel parity).
60pub const PROPERTY_EXCEPTION_HANDLED: &str = "CamelExceptionHandled";
61
62/// The exchange pattern (fire-and-forget or request-reply).
63/// exhaustive-by-contract: the InOnly|InOut MEP dichotomy is a fixed, spec-level closed set.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum ExchangePattern {
66    /// Fire-and-forget: message sent, no reply expected.
67    #[default]
68    InOnly,
69    /// Request-reply: message sent, reply expected.
70    InOut,
71}
72
73/// An Exchange represents a message being routed through the framework.
74///
75/// It contains the input message, an optional output message,
76/// properties for passing data between processors, and error state.
77#[derive(Debug)]
78pub struct Exchange {
79    /// The input (incoming) message.
80    pub input: Message,
81    /// The output (response) message, populated for InOut patterns.
82    pub output: Option<Message>,
83    /// Exchange-scoped properties for passing data between processors.
84    pub properties: HashMap<String, Value>,
85    /// Non-serializable extension values (e.g., channel senders).
86    /// Stored as `Arc<dyn Any + Send + Sync>` so cloning is cheap (ref-count bump).
87    pub extensions: HashMap<String, Arc<dyn Any + Send + Sync>>,
88    /// Error state, if processing failed.
89    pub error: Option<CamelError>,
90    /// The exchange pattern.
91    pub pattern: ExchangePattern,
92    /// Unique correlation ID for distributed tracing.
93    pub correlation_id: String,
94    /// OpenTelemetry context for distributed tracing propagation.
95    /// Carries the active span context between processing steps.
96    /// Defaults to an empty context (noop span) if OTel is not active.
97    pub otel_context: Context,
98    /// RAII claim on the context-global in-flight gauge — the
99    /// accepted-not-completed counter (drainclaim, claimfamily). Pipeline
100    /// drain sites split a sibling
101    /// of the envelope's claim onto the exchange so residency inside
102    /// pipeline-embedded stash sites (resequencer buffers, aggregator
103    /// buckets — raw-`Exchange` territory with no envelope) stays
104    /// counted. `None` on every constructor, on clones, and on
105    /// exchanges accepted through uncounted paths.
106    pub in_flight_claim: Option<crate::in_flight::InFlightClaim>,
107}
108
109impl Exchange {
110    /// Create a new exchange with the given input message.
111    pub fn new(input: Message) -> Self {
112        Self {
113            input,
114            output: None,
115            properties: HashMap::new(),
116            extensions: HashMap::new(),
117            error: None,
118            pattern: ExchangePattern::default(),
119            correlation_id: Uuid::new_v4().to_string(),
120            otel_context: Context::new(),
121            in_flight_claim: None,
122        }
123    }
124
125    /// Create a new exchange with the InOut pattern.
126    pub fn new_in_out(input: Message) -> Self {
127        Self {
128            input,
129            output: None,
130            properties: HashMap::new(),
131            extensions: HashMap::new(),
132            error: None,
133            pattern: ExchangePattern::InOut,
134            correlation_id: Uuid::new_v4().to_string(),
135            otel_context: Context::new(),
136            in_flight_claim: None,
137        }
138    }
139
140    /// Get the correlation ID for this exchange.
141    pub fn correlation_id(&self) -> &str {
142        &self.correlation_id
143    }
144
145    /// Get a property value.
146    pub fn property(&self, key: &str) -> Option<&Value> {
147        self.properties.get(key)
148    }
149
150    /// Set a property value.
151    pub fn set_property(&mut self, key: impl Into<String>, value: impl Into<Value>) {
152        self.properties.insert(key.into(), value.into());
153    }
154
155    /// Remove an exchange property. Idempotent: removing an absent key is a no-op.
156    /// Used by provenance writers to clear `PROPERTY_HTTP_CONTENT_TYPE_INHERITED`.
157    pub fn remove_property(&mut self, key: &str) {
158        self.properties.remove(key);
159    }
160
161    /// Check if the exchange has an error.
162    pub fn has_error(&self) -> bool {
163        self.error.is_some()
164    }
165
166    /// Set an error on this exchange.
167    ///
168    /// Automatically populates exchange properties with error context so that
169    /// all languages can access error information via their property mechanisms:
170    /// - `CamelExceptionMessage` — the error's Display string
171    /// - `CamelExceptionKind` — the error variant name (via `CamelError::classify()`)
172    /// - `CamelExceptionCaught` — alias for Java Camel parity
173    ///
174    /// For `HttpOperationFailed` it also sets the `CamelHttpResponseCode` and
175    /// `CamelHttpResponseText` headers, because a failing producer loses its
176    /// exchange with the `Err` and error-handling expressions would otherwise
177    /// have no way to read the status.
178    pub fn set_error(&mut self, error: CamelError) {
179        if let CamelError::HttpOperationFailed {
180            status_code,
181            status_text,
182            ..
183        } = &error
184        {
185            self.input.set_header(
186                "CamelHttpResponseCode",
187                Value::Number((*status_code).into()),
188            );
189            self.input
190                .set_header("CamelHttpResponseText", Value::String(status_text.clone()));
191        }
192        let msg = error.to_string();
193        let kind = error.classify().to_string();
194        self.properties.insert(
195            PROPERTY_EXCEPTION_MESSAGE.to_string(),
196            Value::String(msg.clone()),
197        );
198        self.properties
199            .insert(PROPERTY_EXCEPTION_KIND.to_string(), Value::String(kind));
200        self.properties
201            .insert(PROPERTY_EXCEPTION_CAUGHT.to_string(), Value::String(msg));
202        self.error = Some(error);
203    }
204
205    /// Clear the error and remove all exception properties.
206    ///
207    /// Called after `handled:true` in on_exception steps to prevent stale
208    /// error state from leaking into subsequent processing.
209    pub fn clear_error(&mut self) {
210        self.error = None;
211        self.properties.remove(PROPERTY_EXCEPTION_MESSAGE);
212        self.properties.remove(PROPERTY_EXCEPTION_KIND);
213        self.properties.remove(PROPERTY_EXCEPTION_CAUGHT);
214    }
215
216    /// Mark the exception as handled and clear the error.
217    ///
218    /// Sets `CamelExceptionHandled = true` then calls `clear_error()`.
219    /// This matches Java Camel's `Exchange.EXCEPTION_HANDLED` semantics.
220    pub fn handle_error(&mut self) {
221        self.properties
222            .insert(PROPERTY_EXCEPTION_HANDLED.to_string(), Value::Bool(true));
223        self.clear_error();
224    }
225
226    /// Store a non-serializable extension value (e.g. a channel sender).
227    pub fn set_extension(&mut self, key: impl Into<String>, value: Arc<dyn Any + Send + Sync>) {
228        self.extensions.insert(key.into(), value);
229    }
230
231    /// Retrieve a typed extension value. Returns `None` if the key is absent
232    /// or the stored value is not of type `T`.
233    pub fn get_extension<T: Any>(&self, key: &str) -> Option<&T> {
234        self.extensions.get(key)?.downcast_ref::<T>()
235    }
236
237    /// Deserialize the body into type `T`.
238    ///
239    /// Uses built-in conversions for `String`, `Vec<u8>`, [`bytes::Bytes`], and
240    /// `serde_json::Value`. For custom types, implement [`FromBody`] or use
241    /// the `impl_from_body_via_serde!` macro.
242    ///
243    /// # Example
244    /// ```rust,ignore
245    /// let text: String = exchange.body_as::<String>()?;
246    /// let raw: Vec<u8> = exchange.body_as::<Vec<u8>>()?;
247    /// ```
248    pub fn body_as<T: FromBody>(&self) -> Result<T, CamelError> {
249        T::from_body(&self.input.body)
250    }
251
252    /// Clone for a fanout branch copy. Unlike [`Exchange::clone`], a
253    /// carried in-flight claim is split onto the copy (claimfamily):
254    /// the sibling counts the copy independently and releases when the
255    /// copy completes; the original claim stays live.
256    pub fn branch_clone(&self) -> Self {
257        let mut copy = self.clone();
258        copy.in_flight_claim = self.in_flight_claim.as_ref().map(|claim| claim.split());
259        copy
260    }
261}
262
263impl Clone for Exchange {
264    fn clone(&self) -> Self {
265        Self {
266            input: self.input.clone(),
267            output: self.output.clone(),
268            properties: self.properties.clone(),
269            extensions: self.extensions.clone(), // Arc ref-count bump, cheap
270            error: self.error.clone(),
271            pattern: self.pattern,
272            correlation_id: self.correlation_id.clone(),
273            otel_context: self.otel_context.clone(),
274            // claimfamily: clones carry no claim — duplicating one would
275            // double-release on drop. Fanout sites that need each copy
276            // counted split a sibling explicitly.
277            in_flight_claim: None,
278        }
279    }
280}
281
282impl Default for Exchange {
283    fn default() -> Self {
284        Self::new(Message::default())
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::Body;
292    use crate::in_flight::{InFlightClaim, InFlightGauge};
293    use serde_json::json;
294    use std::sync::Arc;
295
296    #[test]
297    fn remove_property_removes_and_is_idempotent() {
298        let mut ex = Exchange::new(Message::default());
299        ex.set_property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED, Value::Bool(true));
300        assert_eq!(
301            ex.property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED),
302            Some(&Value::Bool(true))
303        );
304
305        ex.remove_property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED);
306        assert_eq!(ex.property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED), None);
307
308        ex.remove_property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED);
309        assert_eq!(ex.property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED), None);
310    }
311
312    #[test]
313    fn remove_property_absent_is_noop() {
314        let mut ex = Exchange::new(Message::default());
315        assert_eq!(ex.property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED), None);
316
317        ex.remove_property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED);
318
319        assert_eq!(ex.property(PROPERTY_HTTP_CONTENT_TYPE_INHERITED), None);
320    }
321
322    #[test]
323    fn original_content_type_snapshot_roundtrips() {
324        let mut ex = Exchange::new(Message::default());
325        ex.set_extension(
326            ORIGINAL_CONTENT_TYPE_INHERITED_EXTENSION,
327            Arc::new(OriginalContentTypeInherited(true)),
328        );
329
330        let snapshot = ex
331            .get_extension::<OriginalContentTypeInherited>(
332                ORIGINAL_CONTENT_TYPE_INHERITED_EXTENSION,
333            )
334            .expect("snapshot stored under its extension key");
335        assert!(snapshot.0);
336
337        let fresh = Exchange::new(Message::default());
338        assert!(
339            fresh
340                .get_extension::<OriginalContentTypeInherited>(
341                    ORIGINAL_CONTENT_TYPE_INHERITED_EXTENSION
342                )
343                .is_none()
344        );
345    }
346
347    #[test]
348    fn test_exchange_new() {
349        let msg = Message::new("test");
350        let ex = Exchange::new(msg);
351        assert_eq!(ex.input.body.as_text(), Some("test"));
352        assert!(ex.output.is_none());
353        assert!(!ex.has_error());
354        assert_eq!(ex.pattern, ExchangePattern::InOnly);
355    }
356
357    #[test]
358    fn test_exchange_in_out() {
359        let ex = Exchange::new_in_out(Message::default());
360        assert_eq!(ex.pattern, ExchangePattern::InOut);
361    }
362
363    #[test]
364    fn test_exchange_properties() {
365        let mut ex = Exchange::default();
366        ex.set_property("key", Value::Bool(true));
367        assert_eq!(ex.property("key"), Some(&Value::Bool(true)));
368        assert_eq!(ex.property("missing"), None);
369    }
370
371    #[test]
372    fn test_exchange_error() {
373        let mut ex = Exchange::default();
374        assert!(!ex.has_error());
375        ex.set_error(CamelError::ProcessorError("test".into()));
376        assert!(ex.has_error());
377    }
378
379    #[test]
380    fn test_set_error_populates_properties() {
381        let mut ex = Exchange::default();
382        ex.set_error(CamelError::ProcessorError("boom".into()));
383
384        assert!(ex.has_error());
385        assert_eq!(
386            ex.properties.get(PROPERTY_EXCEPTION_MESSAGE),
387            Some(&Value::String("Processor error: boom".to_string()))
388        );
389        assert_eq!(
390            ex.properties.get(PROPERTY_EXCEPTION_KIND),
391            Some(&Value::String("processor".to_string()))
392        );
393        assert_eq!(
394            ex.properties.get(PROPERTY_EXCEPTION_CAUGHT),
395            Some(&Value::String("Processor error: boom".to_string()))
396        );
397    }
398
399    #[test]
400    fn test_set_error_http_failure_exposes_status_headers() {
401        let mut ex = Exchange::default();
402        ex.set_error(CamelError::HttpOperationFailed {
403            method: "GET".into(),
404            url: "http://example.test/x".into(),
405            status_code: 404,
406            status_text: "Not Found".into(),
407            response_body: None,
408        });
409
410        assert_eq!(
411            ex.input.header("CamelHttpResponseCode"),
412            Some(&Value::Number(404.into()))
413        );
414        assert_eq!(
415            ex.input.header("CamelHttpResponseText"),
416            Some(&Value::String("Not Found".to_string()))
417        );
418    }
419
420    #[test]
421    fn test_set_error_non_http_leaves_http_headers_untouched() {
422        let mut ex = Exchange::default();
423        ex.set_error(CamelError::ProcessorError("boom".into()));
424
425        assert!(ex.input.header("CamelHttpResponseCode").is_none());
426        assert!(ex.input.header("CamelHttpResponseText").is_none());
427    }
428
429    #[test]
430    fn test_clear_error_removes_properties() {
431        let mut ex = Exchange::default();
432        ex.set_error(CamelError::RouteError("fail".into()));
433        assert!(ex.has_error());
434        assert!(ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
435
436        ex.clear_error();
437
438        assert!(!ex.has_error());
439        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_MESSAGE));
440        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_KIND));
441        assert!(!ex.properties.contains_key(PROPERTY_EXCEPTION_CAUGHT));
442    }
443
444    #[test]
445    fn test_exchange_lifecycle() {
446        let mut ex = Exchange::new(Message::new("input data"));
447        assert_eq!(ex.input.body.as_text(), Some("input data"));
448
449        // Set some properties
450        ex.set_property("processed", Value::Bool(true));
451
452        // Set output
453        ex.output = Some(Message::new("output data"));
454        assert!(ex.output.is_some());
455
456        // Verify no error
457        assert!(!ex.has_error());
458    }
459
460    #[test]
461    fn test_exchange_otel_context_default() {
462        let ex = Exchange::default();
463        // Field must exist and be accessible — compilation is the test
464        // Also verify it's a fresh context (noop span)
465        use opentelemetry::trace::TraceContextExt;
466        assert!(!ex.otel_context.span().span_context().is_valid());
467    }
468
469    #[test]
470    fn test_exchange_otel_context_propagates_in_clone() {
471        let ex = Exchange::default();
472        let cloned = ex.clone();
473        // Both should have the same (empty) context
474        use opentelemetry::trace::TraceContextExt;
475        assert!(!cloned.otel_context.span().span_context().is_valid());
476    }
477
478    #[test]
479    fn test_set_and_get_extension() {
480        use std::sync::Arc;
481        let mut ex = Exchange::default();
482        ex.set_extension("my.key", Arc::new(42u32));
483        let val: Option<&u32> = ex.get_extension("my.key");
484        assert_eq!(val, Some(&42u32));
485    }
486
487    #[test]
488    fn test_get_extension_wrong_type_returns_none() {
489        use std::sync::Arc;
490        let mut ex = Exchange::default();
491        ex.set_extension("my.key", Arc::new(42u32));
492        let val: Option<&String> = ex.get_extension("my.key");
493        assert!(val.is_none());
494    }
495
496    #[test]
497    fn test_get_extension_missing_key_returns_none() {
498        let ex = Exchange::default();
499        let val: Option<&u32> = ex.get_extension("nope");
500        assert!(val.is_none());
501    }
502
503    #[test]
504    fn test_clone_shares_extension_arc() {
505        use std::sync::Arc;
506        let mut ex = Exchange::default();
507        ex.set_extension("shared", Arc::new(99u64));
508        let cloned = ex.clone();
509        // Both see the same value
510        assert_eq!(ex.get_extension::<u64>("shared"), Some(&99u64));
511        assert_eq!(cloned.get_extension::<u64>("shared"), Some(&99u64));
512    }
513
514    #[test]
515    fn test_body_as_string_from_text() {
516        let ex = Exchange::new(Message::new(Body::Text("hello".to_string())));
517
518        let result = ex.body_as::<String>();
519
520        assert_eq!(result.unwrap(), "hello");
521    }
522
523    #[test]
524    fn test_body_as_string_from_json_string() {
525        let ex = Exchange::new(Message::new(Body::Json(json!("hello"))));
526
527        let result = ex.body_as::<String>();
528
529        assert_eq!(result.unwrap(), "hello");
530    }
531
532    #[test]
533    fn test_body_as_json_value_from_json_number() {
534        let ex = Exchange::new(Message::new(Body::Json(json!(42))));
535
536        let result = ex.body_as::<serde_json::Value>();
537
538        assert_eq!(result.unwrap(), json!(42));
539    }
540
541    #[test]
542    fn test_body_as_vec_u8_from_bytes() {
543        let ex = Exchange::new(Message::new(Body::from(vec![1u8, 2, 3, 4])));
544
545        let result = ex.body_as::<Vec<u8>>();
546
547        assert_eq!(result.unwrap(), vec![1u8, 2, 3, 4]);
548    }
549
550    #[test]
551    fn test_body_as_string_from_empty_returns_err() {
552        let ex = Exchange::new(Message::new(Body::Empty));
553
554        let result = ex.body_as::<String>();
555
556        assert!(result.is_err());
557    }
558
559    #[test]
560    fn branch_clone_splits_claim_original_unaffected() {
561        let gauge = Arc::new(InFlightGauge::new());
562        let mut src = Exchange::new(Message::default());
563        src.in_flight_claim = Some(InFlightClaim::attach(&gauge));
564
565        let copy = src.branch_clone();
566
567        assert_eq!(gauge.total(), 2);
568        assert!(copy.in_flight_claim.is_some());
569        assert!(src.in_flight_claim.is_some());
570    }
571
572    #[test]
573    fn branch_clone_sibling_releases_independently() {
574        let gauge = Arc::new(InFlightGauge::new());
575        let mut src = Exchange::new(Message::default());
576        src.in_flight_claim = Some(InFlightClaim::attach(&gauge));
577
578        let copy = src.branch_clone();
579        drop(copy);
580        assert_eq!(gauge.total(), 1);
581
582        drop(src);
583        assert_eq!(gauge.total(), 0);
584    }
585
586    #[test]
587    fn plain_clone_carries_no_claim() {
588        let gauge = Arc::new(InFlightGauge::new());
589        let mut src = Exchange::new(Message::default());
590        src.in_flight_claim = Some(InFlightClaim::attach(&gauge));
591        assert_eq!(gauge.total(), 1);
592
593        let copy = src.clone();
594
595        assert_eq!(gauge.total(), 1);
596        assert!(copy.in_flight_claim.is_none());
597        assert!(src.in_flight_claim.is_some());
598    }
599}