Skip to main content

behavior_actors/composition/
message_adapter.rs

1//! Typed protocol adaptation through an ordinary actor hop.
2
3use behavior::{Actions, Behavior, BehaviorActed, BehaviorBase, Never, NoBirths, Recipient, User};
4
5use super::DeliveryRoute;
6
7/// A pure actor that maps one input protocol into one destination protocol.
8///
9/// For every accepted `Input`, the adapter invokes its function pointer exactly
10/// once. If that invocation returns normally, the transition emits exactly one
11/// effect selected by [`DeliveryRoute`] and continues with the same behavior.
12/// A logical recipient produces `Delivery` and an established recipient
13/// produces `EstablishedDelivery`. Initialization emits no effects; the
14/// adapter cannot create actors, enter another phase, stop itself, or return a
15/// controlled error. In particular, it cannot address a creator-local child:
16/// its [`behavior::NoBirths`] algebra supplies no local binding for an
17/// interpreter to resolve.
18///
19/// This is a derived Bombay protocol composition, not an additional actor-model
20/// primitive. Delivery is interpreted as an ordinary actor communication, so
21/// the destination observes the adapter actor as the sender. The adapter has no
22/// channel, runtime handle, task, I/O, or hidden effect.
23///
24/// The mapper is deliberately a function pointer rather than a closure or
25/// erased callable. Consequently, the complete adapter type remains nameable
26/// in birth products, child products, supervisors, and topology evidence. If
27/// the mapper panics, unwinding follows the same runtime policy as a panic from
28/// any other [`Behavior`] fold; no successful [`Actions`] value is returned.
29///
30/// A creator-local child route is not a valid standalone adapter destination:
31///
32/// ```
33/// struct Destination;
34/// impl behavior::Protocol for Destination {
35///     type Addr = behavior::MailAddr;
36///     type Msg = u16;
37/// }
38/// fn adapt(value: u8) -> u16 { u16::from(value) }
39/// let destination = behavior::Recipient::<Destination>::global(behavior::MailAddr(1));
40/// let _ = behavior_actors::MessageAdapterWithRoute::new(destination, adapt);
41/// ```
42///
43/// ```compile_fail,E0277
44/// struct Destination;
45/// impl behavior::Protocol for Destination {
46///     type Addr = behavior::MailAddr;
47///     type Msg = u16;
48/// }
49/// fn adapt(value: u8) -> u16 { u16::from(value) }
50/// let mut sequence = behavior::CreationSequence::new();
51/// let creation = sequence.issue().expect("fixture creation ID");
52/// let child = behavior::ChildDelivery::<Destination, behavior::ChildHead>::after(creation, 1);
53/// let _ = behavior_actors::MessageAdapterWithRoute::new(child, adapt);
54/// ```
55pub struct MessageAdapterWithRoute<Input, Route>
56where
57    Route: DeliveryRoute,
58{
59    destination: Route,
60    adapt: fn(Input) -> <Route::Protocol as behavior::Protocol>::Msg,
61}
62
63/// An adapter whose destination is a logical protocol name.
64pub type MessageAdapter<Input, Destination> =
65    MessageAdapterWithRoute<Input, Recipient<Destination>>;
66
67impl<Input, Route> MessageAdapterWithRoute<Input, Route>
68where
69    Route: DeliveryRoute,
70{
71    /// Construct an adapter for one concrete destination protocol.
72    #[must_use]
73    pub const fn new(
74        destination: Route,
75        adapt: fn(Input) -> <Route::Protocol as behavior::Protocol>::Msg,
76    ) -> Self {
77        Self { destination, adapt }
78    }
79
80    /// Return the destination routing intent.
81    #[must_use]
82    pub const fn destination(&self) -> &Route {
83        &self.destination
84    }
85}
86
87impl<Input, Route> BehaviorBase for MessageAdapterWithRoute<Input, Route>
88where
89    Route: DeliveryRoute,
90{
91    type Base = Self;
92
93    fn base(&self) -> &Self {
94        self
95    }
96}
97
98impl<Input, Route> behavior::Protocol for MessageAdapterWithRoute<Input, Route>
99where
100    Route: DeliveryRoute,
101{
102    type Addr = <Route::Protocol as behavior::Protocol>::Addr;
103    type Msg = Input;
104}
105
106impl<Input, Route> Behavior for MessageAdapterWithRoute<Input, Route>
107where
108    Route: DeliveryRoute + Clone,
109    Route::Sends: behavior::SendsFor<User<<Route::Protocol as behavior::Protocol>::Addr, Input>>,
110{
111    type Protocol = Self;
112    type Event = User<<Route::Protocol as behavior::Protocol>::Addr, Input>;
113    type Sends = Route::Sends;
114    type Ph = Never;
115    type Error = Never;
116    type Birth = NoBirths;
117
118    fn transition(&mut self, _: behavior::ActiveTurn, event: Self::Event) -> BehaviorActed<Self> {
119        let message = (self.adapt)(event.message);
120        Ok(Actions::send(self.destination.clone().deliver(message)))
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::Activate as _;
128    use behavior::MailAddr;
129    use core::sync::atomic::{AtomicUsize, Ordering};
130
131    struct Destination;
132
133    impl behavior::Protocol for Destination {
134        type Addr = MailAddr;
135        type Msg = String;
136    }
137
138    impl Behavior for Destination {
139        type Protocol = Self;
140        type Event = User<MailAddr, String>;
141        type Sends = Vec<Never>;
142        type Ph = Never;
143        type Error = Never;
144        type Birth = NoBirths;
145
146        fn transition(&mut self, _: behavior::ActiveTurn, _: Self::Event) -> BehaviorActed<Self> {
147            Ok(Actions::cont())
148        }
149    }
150
151    static INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
152
153    fn describe(value: u8) -> String {
154        INVOCATIONS.fetch_add(1, Ordering::Relaxed);
155        format!("value={value}")
156    }
157
158    #[test]
159    fn initialization_is_empty() {
160        let destination = Recipient::<Destination>::global(MailAddr(7));
161        let initialized = MessageAdapter::new(destination, describe)
162            .initialize()
163            .unwrap();
164
165        assert!(initialized.actions.sends.is_empty());
166        assert!(initialized.actions.creates.is_empty());
167        assert_eq!(initialized.actions.become_, behavior::Step::Continue);
168        assert_eq!(*initialized.behavior.destination(), destination);
169    }
170
171    #[test]
172    fn each_input_maps_once_and_emits_one_delivery_then_continues() {
173        INVOCATIONS.store(0, Ordering::Relaxed);
174        let destination = Recipient::<Destination>::global(MailAddr(7));
175        let mut adapter = MessageAdapter::new(destination, describe)
176            .initialize()
177            .unwrap()
178            .behavior;
179
180        for (input, expected) in [(3, "value=3"), (9, "value=9")] {
181            let actions = adapter.receive(MailAddr(99), input).unwrap();
182            assert_eq!(actions.sends.len(), 1);
183            assert_eq!(actions.sends[0].to, destination);
184            assert_eq!(actions.sends[0].message, expected);
185            assert!(actions.creates.is_empty());
186            assert_eq!(actions.become_, behavior::Step::Continue);
187        }
188
189        assert_eq!(INVOCATIONS.load(Ordering::Relaxed), 2);
190    }
191}