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}