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, 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    /// Optional JSON Schema for the input type of this transition.
117    ///
118    /// When `#[transition(schema)]` is used, this returns the JSON Schema
119    /// generated from the input type via `schemars::schema_for!()`.
120    fn input_schema(&self) -> Option<serde_json::Value> {
121        None
122    }
123
124    /// Execute the transition.
125    ///
126    /// # Parameters
127    ///
128    /// * `state` - The input state of type `From`
129    /// * `resources` - Typed access to required resources
130    /// * `bus` - The base Bus (for cross-cutting concerns like telemetry)
131    ///
132    /// # Returns
133    ///
134    /// An `Outcome<To, Self::Error>` determining the next step.
135    async fn run(
136        &self,
137        state: From,
138        resources: &Self::Resources,
139        bus: &mut Bus,
140    ) -> Outcome<To, Self::Error>;
141}
142
143/// Blanket implementation for `Arc<T>` where `T: Transition`.
144///
145/// This allows sharing transitions across multiple Axons.
146#[async_trait]
147impl<T, From, To> Transition<From, To> for std::sync::Arc<T>
148where
149    T: Transition<From, To> + Send + Sync + 'static,
150    From: Send + 'static,
151    To: Send + 'static,
152{
153    type Error = T::Error;
154    type Resources = T::Resources;
155
156    async fn run(
157        &self,
158        state: From,
159        resources: &Self::Resources,
160        bus: &mut Bus,
161    ) -> Outcome<To, Self::Error> {
162        self.as_ref().run(state, resources, bus).await
163    }
164
165    fn bus_access_policy(&self) -> Option<BusAccessPolicy> {
166        self.as_ref().bus_access_policy()
167    }
168
169    fn input_schema(&self) -> Option<serde_json::Value> {
170        self.as_ref().input_schema()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    struct AddOne;
179
180    #[async_trait]
181    impl Transition<i32, i32> for AddOne {
182        type Error = std::convert::Infallible;
183        type Resources = ();
184
185        async fn run(
186            &self,
187            state: i32,
188            _resources: &Self::Resources,
189            _bus: &mut Bus,
190        ) -> Outcome<i32, Self::Error> {
191            Outcome::Next(state + 1)
192        }
193    }
194
195    #[tokio::test]
196    async fn test_transition_basic() {
197        let mut bus = Bus::new();
198        let result = AddOne.run(41, &(), &mut bus).await;
199        assert!(matches!(result, Outcome::Next(42)));
200    }
201
202    #[test]
203    fn default_input_schema_returns_none() {
204        let t = AddOne;
205        assert!(t.input_schema().is_none());
206    }
207
208    struct WithSchema;
209
210    #[async_trait]
211    impl Transition<String, String> for WithSchema {
212        type Error = String;
213        type Resources = ();
214
215        fn input_schema(&self) -> Option<serde_json::Value> {
216            Some(serde_json::json!({"type": "string"}))
217        }
218
219        async fn run(
220            &self,
221            state: String,
222            _resources: &Self::Resources,
223            _bus: &mut Bus,
224        ) -> Outcome<String, Self::Error> {
225            Outcome::Next(state)
226        }
227    }
228
229    #[test]
230    fn custom_input_schema_returns_value() {
231        let t = WithSchema;
232        let schema = t.input_schema().unwrap();
233        assert_eq!(schema["type"], "string");
234    }
235
236    #[test]
237    fn arc_wrapped_transition_forwards_input_schema() {
238        let t: std::sync::Arc<WithSchema> = std::sync::Arc::new(WithSchema);
239        let schema = t.input_schema().unwrap();
240        assert_eq!(schema["type"], "string");
241    }
242}