Skip to main content

camel_integration_test/
adapters.rs

1//! Partner adapters and the endpoint-keyed router (ADR-0069 §5, §7).
2//!
3//! A [`PartnerAdapter`] is the harness-owned far side of the wire: the
4//! listener or client that observes what the system under test puts on
5//! the wire. The runner talks to adapters through this trait only, so
6//! the scenario vocabulary stays transport-agnostic.
7//!
8//! Every adapter operation is bounded: `receive` carries the action's
9//! deadline, and the runner bounds `send` with a fixed timeout. No
10//! adapter call hangs (ADR-0069 §7).
11//!
12//! [`FakeAdapter`] is the in-memory test double: it records sent
13//! messages, plays a scripted incoming queue, and can fail sends and
14//! receives on demand.
15
16use std::collections::BTreeMap;
17use std::sync::Arc;
18use std::sync::Mutex;
19use std::sync::MutexGuard;
20use std::time::Duration;
21
22use camel_api::Body;
23use camel_api::CamelError;
24use camel_api::Exchange;
25use camel_api::Message;
26use camel_api::Value;
27use camel_component_api::NoOpComponentContext;
28use camel_core::CamelContext;
29use futures::future::BoxFuture;
30use tokio::sync::Mutex as AsyncMutex;
31use tokio::sync::mpsc;
32use tower::ServiceExt;
33
34/// The HTTP partner adapter (feature `http`): a loopback listener
35/// plus client that play both wire roles against the system under
36/// test.
37#[cfg(feature = "http")]
38pub mod http;
39
40/// A message the scenario sends to a partner endpoint.
41#[derive(Debug, Clone, PartialEq)]
42pub struct OutgoingMessage {
43    /// Message body; `Null` when the action declares none.
44    pub body: Value,
45    /// Message headers; empty when the action declares none.
46    pub headers: BTreeMap<String, Value>,
47    /// The resolved HTTP method for client-role sends (explicit from
48    /// the action's `method`, or the inferred `GET`/`POST`). Validated
49    /// as an HTTP token at parse time.
50    pub method: String,
51}
52
53/// A message the harness received from a partner endpoint.
54#[derive(Debug, Clone, PartialEq)]
55pub struct IncomingMessage {
56    /// Received body.
57    pub body: Value,
58    /// Received headers.
59    pub headers: BTreeMap<String, Value>,
60    /// Transport status code when the partner protocol carries one
61    /// (HTTP response status); `None` for transports without a status
62    /// concept.
63    pub status: Option<u16>,
64    /// Request method when the partner protocol carries a request line
65    /// (HTTP server role: the method of the request that reached the
66    /// partner listener); `None` otherwise.
67    pub method: Option<String>,
68    /// Request path (with query, when present) when the partner
69    /// protocol carries a request line; `None` otherwise.
70    pub path: Option<String>,
71}
72
73/// A send or receive failed at the transport layer, before any
74/// assertion ran (`action-transport-failure`, ADR-0069 §7).
75///
76/// Apparatus class: the scenario never got a meaningful answer.
77#[derive(Debug, Clone, PartialEq, thiserror::Error)]
78#[non_exhaustive]
79pub enum TransportError {
80    /// No adapter is registered for the endpoint URI.
81    #[error("no partner adapter bound for endpoint {endpoint}")]
82    Unbound {
83        /// The endpoint URI the scenario referenced.
84        endpoint: String,
85    },
86    /// The transport reported a failure.
87    #[error("{message}")]
88    Other {
89        /// Transport-reported failure detail.
90        message: String,
91    },
92    /// The runner's bounded send deadline elapsed.
93    #[error("send did not complete within {after:?}")]
94    Deadline {
95        /// The bounded send deadline the call exceeded.
96        after: Duration,
97    },
98}
99
100/// Nothing reached the partner endpoint before the deadline
101/// (`receive-timeout`, ADR-0069 §7).
102///
103/// Verdict class: the scenario ran and the system under test failed
104/// it. A struct, not an enum: the taxonomy has one receive failure.
105#[derive(Debug, Clone, PartialEq, thiserror::Error)]
106#[error("nothing reached {endpoint} within {deadline:?} (waited {elapsed:?})")]
107pub struct ReceiveTimeout {
108    /// The endpoint URI that delivered nothing.
109    pub endpoint: String,
110    /// The deadline that elapsed.
111    pub deadline: Duration,
112    /// How long the receive actually waited before giving up.
113    pub elapsed: Duration,
114}
115
116/// Why a `receive` call did not deliver a message (ADR-0069 §7).
117///
118/// The variants carry the failure class: [`ReceiveError::Timeout`] is
119/// verdict class (the system under test delivered nothing in time);
120/// [`ReceiveError::Transport`] is apparatus class (the receive failed
121/// at the transport before the scenario got a meaningful answer).
122#[derive(Debug, Clone, PartialEq, thiserror::Error)]
123#[non_exhaustive]
124pub enum ReceiveError {
125    /// Nothing reached the partner endpoint before the deadline
126    /// (`receive-timeout`, verdict class).
127    #[error("{0}")]
128    Timeout(ReceiveTimeout),
129    /// The receive failed at the transport layer
130    /// (`action-transport-failure`, apparatus class).
131    #[error("{0}")]
132    Transport(TransportError),
133}
134
135/// The harness-owned far side of the wire for one endpoint family.
136///
137/// Implementations must be `Send + Sync`; calls return boxed futures
138/// so the trait stays object-safe behind `Box<dyn PartnerAdapter>`.
139///
140/// The two-key contract splits the lane from the wire: `lane_key` is
141/// the registered router key whose lane (queue, parked roundtrip) the
142/// call belongs to, `target_uri`/`source_uri` is the resolved address
143/// the scenario referenced. Adapters that own a listener (the http
144/// partner) read their arrival lane by the registered key's request
145/// path; adapters that dial treat the target URI as the wire address.
146pub trait PartnerAdapter: Send + Sync {
147    /// Send a message to the target URI, parking any roundtrip under
148    /// the lane key. Adapters without a client role keep the default
149    /// (a transport failure naming the gap); the http partner is one
150    /// such adapter — the router's own client lane performs every
151    /// http client-role send.
152    fn send<'a>(
153        &'a self,
154        lane_key: &'a str,
155        target_uri: &'a str,
156        msg: OutgoingMessage,
157    ) -> BoxFuture<'a, Result<(), TransportError>> {
158        let _ = (lane_key, target_uri, msg);
159        Box::pin(async {
160            Err(TransportError::Other {
161                message: "adapter does not implement client-role sends".to_string(),
162            })
163        })
164    }
165
166    /// Receive a message from the source URI before the deadline
167    /// passes. Implementations must respect the deadline; they never
168    /// hang past it.
169    fn receive<'a>(
170        &'a self,
171        lane_key: &'a str,
172        source_uri: &'a str,
173        deadline: Duration,
174    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>>;
175
176    /// The host:port authority this adapter's listener bound, when it
177    /// owns one (the http partner); `None` otherwise. The router uses
178    /// it to resolve declared and dynamic endpoint references to real
179    /// wire addresses.
180    fn bound_authority(&self) -> Option<String> {
181        None
182    }
183
184    /// The wire requests this adapter recorded, in arrival order —
185    /// the recorded-traffic snapshot a `partner` validate asserts
186    /// against (feature `http`). The default is empty: adapters
187    /// without a listener record nothing.
188    #[cfg(feature = "http")]
189    fn recorded_requests(&self) -> Vec<http::HttpWireRequest> {
190        Vec::new()
191    }
192}
193
194/// Dispatches adapter calls by declared endpoint key to the
195/// endpoint-keyed adapter map it wraps, and owns the shared http
196/// client lane (feature `http`).
197///
198/// Sends split into two keys — the declared endpoint string and the
199/// interpolated wire address — and http-scheme sends route through
200/// the router's own [`ClientLane`](http::ClientLane) (feature
201/// `http`): a declared `:0` harness key dials the partner's bound
202/// address, a dynamic reference resolves by interpolated authority,
203/// and a plain string dials its literal URI — no `Unbound` failure
204/// for http schemes. Non-http schemes dispatch to the registered
205/// adapter as before: an endpoint URI with no registered adapter
206/// fails the send at the transport ([`TransportError::Unbound`]) and
207/// the receive at the transport too
208/// ([`ReceiveError::Transport`]) — no partner exists that could ever
209/// deliver, the failure is apparatus class, and the call never hangs.
210///
211/// Receives are client-role-first: a roundtrip parked by the router's
212/// own client lane wins over the partner adapter's server-role
213/// arrivals.
214pub struct PartnerRouter {
215    /// Declared endpoint key to adapter.
216    adapters: BTreeMap<String, Box<dyn PartnerAdapter>>,
217    /// The shared http client lane (feature `http`): one parked
218    /// roundtrip per lane key, filled by every http-scheme send.
219    /// `Arc`-shared because a launch's spawned exchange keeps the
220    /// handle alive to park its own failure.
221    #[cfg(feature = "http")]
222    client_lane: Arc<http::ClientLane>,
223}
224
225impl PartnerRouter {
226    /// Builds a router over the given endpoint-keyed adapters.
227    pub fn new(adapters: BTreeMap<String, Box<dyn PartnerAdapter>>) -> Self {
228        Self {
229            adapters,
230            #[cfg(feature = "http")]
231            client_lane: Arc::new(http::ClientLane::new()),
232        }
233    }
234
235    /// The adapter registered under `key`, if any.
236    pub fn adapter(&self, key: &str) -> Option<&dyn PartnerAdapter> {
237        self.adapters.get(key).map(|boxed| boxed.as_ref())
238    }
239
240    /// The wire requests the partner registered under `key` recorded,
241    /// in arrival order (feature `http`); empty when no adapter is
242    /// registered under `key` or the registered adapter records
243    /// nothing — an unregistered key reads as an empty snapshot, so
244    /// a partner validate on it fails the count, never the run.
245    #[cfg(feature = "http")]
246    pub fn recorded_requests(&self, key: &str) -> Vec<http::HttpWireRequest> {
247        self.adapters
248            .get(key)
249            .map(|adapter| adapter.recorded_requests())
250            .unwrap_or_default()
251    }
252
253    /// Every registered partner that owns a bound authority, as
254    /// `(declared key, bound authority)` pairs.
255    pub fn authorities(&self) -> Vec<(String, String)> {
256        self.adapters
257            .iter()
258            .filter_map(|(key, adapter)| Some((key.clone(), adapter.bound_authority()?)))
259            .collect()
260    }
261
262    /// The lane key a receive under `(declared, interpolated)` reads:
263    /// the declared string itself when it names a registered partner
264    /// key (lane reads by declared key, today's behavior); otherwise
265    /// the registered key of the partner whose bound authority equals
266    /// the interpolated URI's authority. `None` when neither resolves
267    /// (plain strings): the caller falls back to the declared string.
268    pub fn lane_key_for(&self, declared: &str, interpolated: &str) -> Option<String> {
269        if self.adapters.contains_key(declared) {
270            return Some(declared.to_string());
271        }
272        let authority = uri_authority(interpolated)?;
273        self.authorities()
274            .into_iter()
275            .find(|(_, bound)| bound == authority)
276            .map(|(key, _)| key)
277    }
278
279    /// The wire target a send under `(declared_key, interpolated_uri)`
280    /// dials, when it differs from plain literal dialing.
281    ///
282    /// - `declared_key` names a registered partner AND carries the
283    ///   unroutable port-0 authority (the harness-declared form,
284    ///   ADR-0069 §8): rewrite only the authority of
285    ///   `interpolated_uri` to that partner's bound authority,
286    ///   preserving the interpolated path and query.
287    /// - `declared_key` names no partner but the interpolated URI's
288    ///   authority equals a bound partner's authority: return that
289    ///   partner's authority rewrite (path preserved) — the resolved
290    ///   URI for a dynamic reference.
291    /// - Anything else — a routable declared key (a partner registered
292    ///   under its own bound address, or under a foreign endpoint as
293    ///   a client-role vehicle) or an address no partner owns — is
294    ///   `None`: the caller dials the interpolated URI literally.
295    pub fn wire_target(&self, declared_key: &str, interpolated_uri: &str) -> Option<String> {
296        if let Some(adapter) = self.adapters.get(declared_key) {
297            let bound = adapter.bound_authority()?;
298            if !authority_is_port_zero(declared_key) {
299                return None;
300            }
301            return rewrite_authority(interpolated_uri, &bound);
302        }
303        self.partner_by_authority(interpolated_uri)
304            .and_then(|(_, bound)| rewrite_authority(interpolated_uri, &bound))
305    }
306
307    /// Sends `msg` under the two-key contract: dispatch by declared
308    /// endpoint key, dial by resolved address (see the type docs for
309    /// the http cases).
310    pub async fn send(
311        &self,
312        declared: &str,
313        interpolated: &str,
314        msg: OutgoingMessage,
315    ) -> Result<(), TransportError> {
316        #[cfg(feature = "http")]
317        if interpolated.starts_with("http://") {
318            return self.send_http(declared, interpolated, msg).await;
319        }
320        match self.adapters.get(declared) {
321            Some(adapter) => adapter.send(declared, interpolated, msg).await,
322            None => Err(TransportError::Unbound {
323                endpoint: declared.to_string(),
324            }),
325        }
326    }
327
328    /// The http-scheme send dispatch (feature `http`): every case goes
329    /// through the router's own client lane.
330    #[cfg(feature = "http")]
331    async fn send_http(
332        &self,
333        declared: &str,
334        interpolated: &str,
335        msg: OutgoingMessage,
336    ) -> Result<(), TransportError> {
337        // (a) The declared key registers an http partner: the
338        // harness-declared endpoint — dial its bound address when the
339        // `:0` form resolves one, the literal URI otherwise. A
340        // non-http adapter registered under an http-scheme key keeps
341        // today's equality dispatch.
342        if let Some(adapter) = self.adapters.get(declared) {
343            if adapter.bound_authority().is_some() {
344                let target = self
345                    .wire_target(declared, interpolated)
346                    .unwrap_or_else(|| interpolated.to_string());
347                return Arc::clone(&self.client_lane)
348                    .launch(declared, &target, msg)
349                    .await;
350            }
351            return adapter.send(declared, interpolated, msg).await;
352        }
353        // (b) The declared key is not registered, but the interpolated
354        // authority resolves to a partner: dial the resolved URI under
355        // that partner's REGISTERED key, so `lane_key_for` finds the
356        // roundtrip on receive.
357        if let Some((lane_key, target)) = self
358            .partner_by_authority(interpolated)
359            .and_then(|(key, bound)| Some((key, rewrite_authority(interpolated, &bound)?)))
360        {
361            return Arc::clone(&self.client_lane)
362                .launch(&lane_key, &target, msg)
363                .await;
364        }
365        // (c) Neither: a plain-string reference dials its literal URI
366        // with no partner involved.
367        Arc::clone(&self.client_lane)
368            .launch(declared, interpolated, msg)
369            .await
370    }
371
372    /// Receives under the two-key contract, client-role-first: derive
373    /// the lane key ([`Self::lane_key_for`], falling back to the
374    /// declared string for plain strings), return a roundtrip parked
375    /// by the router's own client lane when one exists, and otherwise
376    /// delegate the server-role receive to the adapter registered
377    /// under that key. [`TransportError::Unbound`] only when neither a
378    /// parked roundtrip nor a registered adapter exists.
379    pub async fn receive(
380        &self,
381        declared: &str,
382        interpolated: &str,
383        deadline: Duration,
384    ) -> Result<IncomingMessage, ReceiveError> {
385        let lane_key = self
386            .lane_key_for(declared, interpolated)
387            .unwrap_or_else(|| declared.to_string());
388        #[cfg(feature = "http")]
389        if let Some(parked) = self.client_lane.take(&lane_key) {
390            return self
391                .client_lane
392                .await_parked(interpolated, deadline, parked)
393                .await;
394        }
395        match self.adapters.get(lane_key.as_str()) {
396            Some(adapter) => adapter.receive(&lane_key, interpolated, deadline).await,
397            None => Err(ReceiveError::Transport(TransportError::Unbound {
398                endpoint: declared.to_string(),
399            })),
400        }
401    }
402
403    /// The registered partner whose bound authority equals the URI's
404    /// authority, as `(registered key, bound authority)`; the
405    /// post-interpolation resolution of a dynamic reference.
406    fn partner_by_authority(&self, uri: &str) -> Option<(String, String)> {
407        let authority = uri_authority(uri)?;
408        self.authorities()
409            .into_iter()
410            .find(|(_, bound)| bound == authority)
411    }
412}
413
414/// The authority span of an absolute URI (`scheme://authority/rest`);
415/// `None` when the string carries no `://` separator. Userinfo is not
416/// part of this grammar.
417fn uri_authority(uri: &str) -> Option<&str> {
418    let start = uri.find("://")? + 3;
419    let rest = &uri[start..];
420    let end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
421    Some(&rest[..end])
422}
423
424/// Whether the URI's authority is the unroutable port-0 placeholder —
425/// the harness-declared endpoint form (`http://127.0.0.1:0/...`,
426/// ADR-0069 §8). A declared key with any other authority addresses a
427/// routable endpoint and dials literally.
428fn authority_is_port_zero(uri: &str) -> bool {
429    let Some(authority) = uri_authority(uri) else {
430        return false;
431    };
432    match authority.rsplit_once(':') {
433        Some((_, port)) => port == "0",
434        None => false,
435    }
436}
437
438/// Rewrites the URI's authority, preserving scheme, path, and query.
439fn rewrite_authority(uri: &str, authority: &str) -> Option<String> {
440    let rest_start = uri.find("://")? + 3;
441    let rest = &uri[rest_start..];
442    let path_start = rest.find(['/', '?', '#']).unwrap_or(rest.len());
443    let mut rewritten = String::with_capacity(uri.len());
444    rewritten.push_str(&uri[..rest_start]);
445    rewritten.push_str(authority);
446    rewritten.push_str(&rest[path_start..]);
447    Some(rewritten)
448}
449
450/// One recorded send: the endpoint the scenario addressed and the
451/// message it put on the wire.
452#[derive(Debug, Clone, PartialEq)]
453pub struct RecordedSend {
454    /// Endpoint URI the message was sent to.
455    pub endpoint: String,
456    /// The message as sent.
457    pub message: OutgoingMessage,
458}
459
460/// A handle onto a [`FakeAdapter`]'s recorded sends, valid after the
461/// adapter itself moved into the router.
462#[derive(Clone)]
463pub struct FakeRecorder {
464    sent: Arc<Mutex<Vec<RecordedSend>>>,
465}
466
467impl FakeRecorder {
468    /// A snapshot of everything the scenario sent through the fake.
469    pub fn sent_messages(&self) -> Vec<RecordedSend> {
470        lock_through(&self.sent).clone()
471    }
472}
473
474/// Inner state shared between a `FakeAdapter` and its clones.
475struct FakeInner {
476    /// When set, every send fails with this reason.
477    fail_send: Option<String>,
478    /// When set, every receive fails at the transport with this
479    /// reason.
480    fail_receive: Option<String>,
481    /// Recorded sends, in order, shared with `FakeRecorder` handles.
482    sent: Arc<Mutex<Vec<RecordedSend>>>,
483    /// The receiving half of the scripted queue; the sender half is
484    /// dropped after seeding, so a drained queue reports closed and
485    /// receive maps that to a timeout.
486    queue_rx: AsyncMutex<mpsc::Receiver<IncomingMessage>>,
487}
488
489/// In-memory [`PartnerAdapter`] for tests: records sent messages,
490/// plays a scripted incoming queue, and can fail sends on demand.
491///
492/// `Clone` shares state; keep a clone (or a [`FakeRecorder`]) to
493/// inspect sends after moving the adapter into a [`PartnerRouter`].
494#[derive(Clone)]
495pub struct FakeAdapter {
496    inner: Arc<FakeInner>,
497}
498
499impl FakeAdapter {
500    /// A fake that plays the given messages, in order, on receive;
501    /// once the queue drains, further receives time out.
502    pub fn scripted(queue: Vec<IncomingMessage>) -> Self {
503        let capacity = queue.len().max(1);
504        let (queue_tx, queue_rx) = mpsc::channel(capacity);
505        for message in queue {
506            // Capacity equals the queue length, so the try-send cannot
507            // hit a full buffer; a closed receiver is impossible here.
508            if queue_tx.try_send(message).is_err() {
509                break;
510            }
511        }
512        drop(queue_tx);
513        Self {
514            inner: Arc::new(FakeInner {
515                fail_send: None,
516                fail_receive: None,
517                sent: Arc::new(Mutex::new(Vec::new())),
518                queue_rx: AsyncMutex::new(queue_rx),
519            }),
520        }
521    }
522
523    /// A fake whose every send fails at the transport.
524    pub fn failing_send(reason: impl Into<String>) -> Self {
525        Self {
526            inner: Arc::new(FakeInner {
527                fail_send: Some(reason.into()),
528                fail_receive: None,
529                sent: Arc::new(Mutex::new(Vec::new())),
530                queue_rx: AsyncMutex::new(mpsc::channel(1).1),
531            }),
532        }
533    }
534
535    /// A fake whose every receive fails at the transport, so a
536    /// mid-scenario receive transport failure is expressible.
537    pub fn failing_receive(reason: impl Into<String>) -> Self {
538        Self {
539            inner: Arc::new(FakeInner {
540                fail_send: None,
541                fail_receive: Some(reason.into()),
542                sent: Arc::new(Mutex::new(Vec::new())),
543                queue_rx: AsyncMutex::new(mpsc::channel(1).1),
544            }),
545        }
546    }
547
548    /// A handle onto this fake's recorded sends.
549    pub fn recorder(&self) -> FakeRecorder {
550        FakeRecorder {
551            sent: Arc::clone(&self.inner.sent),
552        }
553    }
554}
555
556impl PartnerAdapter for FakeAdapter {
557    fn send<'a>(
558        &'a self,
559        lane_key: &'a str,
560        _target_uri: &'a str,
561        msg: OutgoingMessage,
562    ) -> BoxFuture<'a, Result<(), TransportError>> {
563        Box::pin(async move {
564            if let Some(reason) = &self.inner.fail_send {
565                return Err(TransportError::Other {
566                    message: reason.clone(),
567                });
568            }
569            lock_through(&self.inner.sent).push(RecordedSend {
570                endpoint: lane_key.to_string(),
571                message: msg,
572            });
573            Ok(())
574        })
575    }
576
577    fn receive<'a>(
578        &'a self,
579        _lane_key: &'a str,
580        source_uri: &'a str,
581        deadline: Duration,
582    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
583        Box::pin(async move {
584            if let Some(reason) = &self.inner.fail_receive {
585                return Err(ReceiveError::Transport(TransportError::Other {
586                    message: reason.clone(),
587                }));
588            }
589            let mut queue_rx = self.inner.queue_rx.lock().await;
590            let started = tokio::time::Instant::now();
591            let outcome = tokio::time::timeout(deadline, queue_rx.recv()).await;
592            match outcome {
593                Ok(Some(message)) => Ok(message),
594                Ok(None) | Err(_) => Err(ReceiveError::Timeout(ReceiveTimeout {
595                    endpoint: source_uri.to_string(),
596                    deadline,
597                    elapsed: started.elapsed(),
598                })),
599            }
600        })
601    }
602}
603
604/// Locks, recovering the guard through poisoning: fake state is plain
605/// data, a poisoned lock carries no invariant to protect.
606fn lock_through<T>(lock: &Mutex<T>) -> MutexGuard<'_, T> {
607    lock.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
608}
609
610// ---------------------------------------------------------------------------
611// Route stimulus through the booted context
612// ---------------------------------------------------------------------------
613
614/// Startup-race retry sleep for `direct:` producer delivery (the
615/// camel-test / camel-run stimulus mechanism).
616const STIMULUS_RETRY_SLEEP: Duration = Duration::from_millis(20);
617/// Startup-race retry deadline for `direct:` producer delivery.
618const STIMULUS_RETRY_DEADLINE: Duration = Duration::from_secs(1);
619
620/// The route stimulus for a booted scenario (ADR-0069 section 5): a
621/// scenario `send` addressed to a CONTEXT component endpoint
622/// (`direct:`) must reach the booted system under test, not a
623/// partner. This adapter delivers the message through the context's
624/// own producer path — a fresh `direct:` endpoint and producer per
625/// send, one `oneshot` per exchange, retrying the consumer-startup
626/// race — the same mechanism `camel-test` and `camel run` use to
627/// stimulate routes.
628///
629/// Key the router map by the exact endpoint URI the scenario's `send`
630/// addresses (`direct:start`). `receive` is not a context role: it
631/// fails at the transport, apparatus class.
632pub struct DirectStimulus {
633    /// The booted context, shared with the boot-owning caller (the
634    /// caller wraps `ScenarioRun::ctx` after
635    /// [`boot_scenario`](crate::boot_scenario) returns).
636    ctx: Arc<AsyncMutex<CamelContext>>,
637}
638
639impl DirectStimulus {
640    /// Wraps the booted context the scenario sends into.
641    pub fn new(ctx: Arc<AsyncMutex<CamelContext>>) -> Self {
642        Self { ctx }
643    }
644}
645
646impl PartnerAdapter for DirectStimulus {
647    fn send<'a>(
648        &'a self,
649        lane_key: &'a str,
650        _target_uri: &'a str,
651        msg: OutgoingMessage,
652    ) -> BoxFuture<'a, Result<(), TransportError>> {
653        Box::pin(async move {
654            let exchange = stimulus_exchange(msg);
655            let transport = |detail: String| TransportError::Other { message: detail };
656            let deadline = tokio::time::Instant::now() + STIMULUS_RETRY_DEADLINE;
657            loop {
658                let producer = {
659                    let ctx = self.ctx.lock().await;
660                    let producer_ctx = ctx.producer_context();
661                    let component = ctx
662                        .registry()
663                        .get("direct")
664                        .ok_or_else(|| transport("direct component not registered".to_string()))?;
665                    let endpoint = component.create_endpoint(lane_key, &*ctx).map_err(|e| {
666                        transport(format!("failed to create endpoint {lane_key}: {e}"))
667                    })?;
668                    endpoint
669                        .create_producer(Arc::new(NoOpComponentContext), &producer_ctx)
670                        .map_err(|e| {
671                            transport(format!("failed to create producer for {lane_key}: {e}"))
672                        })?
673                };
674                match producer.oneshot(exchange.clone()).await {
675                    // The stimulus exchange completed the route; the
676                    // reply carries no scenario meaning in v1.
677                    Ok(_reply) => return Ok(()),
678                    Err(e) => {
679                        // The direct producer types the startup race as
680                        // EndpointCreationFailed at both error sites:
681                        // poll_ready ("direct endpoint '{}' not registered",
682                        // camel-direct/src/lib.rs) and call ("no consumer
683                        // registered for direct:{name}"). No string matching.
684                        let is_startup_race = matches!(e, CamelError::EndpointCreationFailed(_));
685                        if is_startup_race && tokio::time::Instant::now() < deadline {
686                            tokio::time::sleep(STIMULUS_RETRY_SLEEP).await;
687                            continue;
688                        }
689                        return Err(transport(format!("send to {lane_key} failed: {e}")));
690                    }
691                }
692            }
693        })
694    }
695
696    fn receive<'a>(
697        &'a self,
698        _lane_key: &'a str,
699        source_uri: &'a str,
700        _deadline: Duration,
701    ) -> BoxFuture<'a, Result<IncomingMessage, ReceiveError>> {
702        Box::pin(async move {
703            Err(ReceiveError::Transport(TransportError::Other {
704                message: format!(
705                    "{source_uri} is a context stimulus endpoint; receive is a partner role"
706                ),
707            }))
708        })
709    }
710}
711
712/// Builds the stimulus exchange: strings pass through as text bodies,
713/// `Null` is empty, structured values travel as JSON; headers carry
714/// over verbatim.
715fn stimulus_exchange(msg: OutgoingMessage) -> Exchange {
716    let body = match &msg.body {
717        Value::Null => Body::Empty,
718        Value::String(text) => Body::Text(text.clone()),
719        other => Body::Json(other.clone()),
720    };
721    let mut message = Message::new(body);
722    for (name, value) in &msg.headers {
723        message.set_header(name.clone(), value.clone());
724    }
725    Exchange::new(message)
726}