Skip to main content

ranvier_core/
transition.rs

1//! # Transition: Typed State Transformation
2//!
3//! The `Transition` trait defines the contract for state transformations within a Decision Tree.
4//!
5//! ## Design Philosophy
6//!
7//! * **Explicit Input/Output**: Every transition declares `From` and `To` types
8//! * **No Hidden Effects**: All effects must go through the `Bus`
9//! * **Outcome-Based Control Flow**: Returns `Outcome` not `Result`
10
11use crate::bus::Bus;
12use crate::outcome::Outcome;
13use async_trait::async_trait;
14use std::fmt::Debug;
15
16/// Resource requirement for a transition.
17///
18/// This trait is used to mark types that can be injected as resources.
19/// Implementations should usually be a struct representing a bundle of resources.
20pub trait ResourceRequirement: Send + Sync + 'static {}
21
22/// Blanket implementation for () if no resources are needed.
23impl ResourceRequirement for () {}
24
25/// The contract for a Typed State Transition.
26///
27/// `Transition` converts state `From` to `Outcome<To, Error>`.
28/// All transitions are async and receive access to the `Bus` for resource injection.
29///
30/// ## Example
31///
32/// ```rust
33/// use async_trait::async_trait;
34/// use ranvier_core::prelude::*;
35///
36/// # #[derive(Clone)]
37/// # struct ValidateUser;
38/// # #[async_trait::async_trait]
39/// # impl Transition<String, String> for ValidateUser {
40/// #     type Error = std::convert::Infallible;
41/// #     async fn run(&self, input: String, _bus: &mut Bus) -> Outcome<String, Self::Error> {
42/// #         Outcome::next(format!("validated: {}", input))
43/// #     }
44/// # }
45/// #
46/// # #[async_trait::async_trait]
47/// # impl Transition<i32, i32> for DoubleValue {
48/// #     type Error = std::convert::Infallible;
49/// #     async fn run(&self, input: i32, _bus: &mut Bus) -> Outcome<i32, Self::Error> {
50/// #         Outcome::next(input * 2)
51/// #     }
52/// # }
53/// # struct DoubleValue;
54/// ```
55#[async_trait]
56pub trait Transition<From, To>: Send + Sync + 'static
57where
58    From: Send + 'static,
59    To: Send + 'static,
60{
61    /// Domain-specific error type (e.g., AuthError, ValidationError)
62    type Error: Send + Sync + Debug + 'static;
63
64    /// The type of resources required by this transition.
65    /// This follows the "Hard-Wired Types" principle from the Master Plan.
66    type Resources: ResourceRequirement;
67
68    /// Execute the transition.
69    ///
70    /// # Parameters
71    ///
72    /// * `state` - The input state of type `From`
73    /// * `resources` - Typed access to required resources
74    /// * `bus` - The base Bus (for cross-cutting concerns like telemetry)
75    ///
76    /// # Returns
77    ///
78    /// An `Outcome<To, Self::Error>` determining the next step.
79    /// Returns a human-readable label for this transition.
80    /// Defaults to the type name.
81    fn label(&self) -> String {
82        let full = std::any::type_name::<Self>();
83        full.split("::").last().unwrap_or(full).to_string()
84    }
85
86    /// Returns a detailed description of what this transition does.
87    fn description(&self) -> Option<String> {
88        None
89    }
90
91    /// Execute the transition.
92    ///
93    /// # Parameters
94    ///
95    /// * `state` - The input state of type `From`
96    /// * `resources` - Typed access to required resources
97    /// * `bus` - The base Bus (for cross-cutting concerns like telemetry)
98    ///
99    /// # Returns
100    ///
101    /// An `Outcome<To, Self::Error>` determining the next step.
102    async fn run(
103        &self,
104        state: From,
105        resources: &Self::Resources,
106        bus: &mut Bus,
107    ) -> Outcome<To, Self::Error>;
108}
109
110/// Blanket implementation for `Arc<T>` where `T: Transition`.
111///
112/// This allows sharing transitions across multiple Axons.
113#[async_trait]
114impl<T, From, To> Transition<From, To> for std::sync::Arc<T>
115where
116    T: Transition<From, To> + Send + Sync + 'static,
117    From: Send + 'static,
118    To: Send + 'static,
119{
120    type Error = T::Error;
121    type Resources = T::Resources;
122
123    async fn run(
124        &self,
125        state: From,
126        resources: &Self::Resources,
127        bus: &mut Bus,
128    ) -> Outcome<To, Self::Error> {
129        self.as_ref().run(state, resources, bus).await
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    struct AddOne;
138
139    #[async_trait]
140    impl Transition<i32, i32> for AddOne {
141        type Error = std::convert::Infallible;
142        type Resources = ();
143
144        async fn run(
145            &self,
146            state: i32,
147            _resources: &Self::Resources,
148            _bus: &mut Bus,
149        ) -> Outcome<i32, Self::Error> {
150            Outcome::Next(state + 1)
151        }
152    }
153
154    #[tokio::test]
155    async fn test_transition_basic() {
156        let mut bus = Bus::new();
157        let result = AddOne.run(41, &(), &mut bus).await;
158        assert!(matches!(result, Outcome::Next(42)));
159    }
160}