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, BusAccessPolicy};
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/// # type Resources = ();
42/// # async fn run(
43/// # &self,
44/// # input: String,
45/// # _resources: &Self::Resources,
46/// # _bus: &mut Bus,
47/// # ) -> Outcome<String, Self::Error> {
48/// # Outcome::next(format!("validated: {}", input))
49/// # }
50/// # }
51/// #
52/// # #[async_trait::async_trait]
53/// # impl Transition<i32, i32> for DoubleValue {
54/// # type Error = std::convert::Infallible;
55/// # type Resources = ();
56/// # async fn run(
57/// # &self,
58/// # input: i32,
59/// # _resources: &Self::Resources,
60/// # _bus: &mut Bus,
61/// # ) -> Outcome<i32, Self::Error> {
62/// # Outcome::next(input * 2)
63/// # }
64/// # }
65/// # struct DoubleValue;
66/// ```
67#[async_trait]
68pub trait Transition<From, To>: Send + Sync + 'static
69where
70 From: Send + 'static,
71 To: Send + 'static,
72{
73 /// Domain-specific error type (e.g., AuthError, ValidationError)
74 type Error: Send + Sync + Debug + 'static;
75
76 /// The type of resources required by this transition.
77 /// This follows the "Hard-Wired Types" principle from the Master Plan.
78 type Resources: ResourceRequirement;
79
80 /// Execute the transition.
81 ///
82 /// # Parameters
83 ///
84 /// * `state` - The input state of type `From`
85 /// * `resources` - Typed access to required resources
86 /// * `bus` - The base Bus (for cross-cutting concerns like telemetry)
87 ///
88 /// # Returns
89 ///
90 /// An `Outcome<To, Self::Error>` determining the next step.
91 /// Returns a human-readable label for this transition.
92 /// Defaults to the type name.
93 fn label(&self) -> String {
94 let full = std::any::type_name::<Self>();
95 full.split("::").last().unwrap_or(full).to_string()
96 }
97
98 /// Returns a detailed description of what this transition does.
99 fn description(&self) -> Option<String> {
100 None
101 }
102
103 /// Returns the visual position of this transition in a schematic.
104 /// (x, y) coordinates.
105 fn position(&self) -> Option<(f32, f32)> {
106 None
107 }
108
109 /// Optional transition-scoped Bus access policy (M143).
110 ///
111 /// Default is unrestricted access for backward compatibility.
112 fn bus_access_policy(&self) -> Option<BusAccessPolicy> {
113 None
114 }
115
116 /// Execute the transition.
117 ///
118 /// # Parameters
119 ///
120 /// * `state` - The input state of type `From`
121 /// * `resources` - Typed access to required resources
122 /// * `bus` - The base Bus (for cross-cutting concerns like telemetry)
123 ///
124 /// # Returns
125 ///
126 /// An `Outcome<To, Self::Error>` determining the next step.
127 async fn run(
128 &self,
129 state: From,
130 resources: &Self::Resources,
131 bus: &mut Bus,
132 ) -> Outcome<To, Self::Error>;
133}
134
135/// Blanket implementation for `Arc<T>` where `T: Transition`.
136///
137/// This allows sharing transitions across multiple Axons.
138#[async_trait]
139impl<T, From, To> Transition<From, To> for std::sync::Arc<T>
140where
141 T: Transition<From, To> + Send + Sync + 'static,
142 From: Send + 'static,
143 To: Send + 'static,
144{
145 type Error = T::Error;
146 type Resources = T::Resources;
147
148 async fn run(
149 &self,
150 state: From,
151 resources: &Self::Resources,
152 bus: &mut Bus,
153 ) -> Outcome<To, Self::Error> {
154 self.as_ref().run(state, resources, bus).await
155 }
156
157 fn bus_access_policy(&self) -> Option<BusAccessPolicy> {
158 self.as_ref().bus_access_policy()
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 struct AddOne;
167
168 #[async_trait]
169 impl Transition<i32, i32> for AddOne {
170 type Error = std::convert::Infallible;
171 type Resources = ();
172
173 async fn run(
174 &self,
175 state: i32,
176 _resources: &Self::Resources,
177 _bus: &mut Bus,
178 ) -> Outcome<i32, Self::Error> {
179 Outcome::Next(state + 1)
180 }
181 }
182
183 #[tokio::test]
184 async fn test_transition_basic() {
185 let mut bus = Bus::new();
186 let result = AddOne.run(41, &(), &mut bus).await;
187 assert!(matches!(result, Outcome::Next(42)));
188 }
189}