Skip to main content

juncture_core/node/
into_node.rs

1use crate::{
2    State, command::Command, config::RunnableConfig, error::JunctureError, node::Node,
3    runtime::Runtime,
4};
5use std::{marker::PhantomData, sync::Arc};
6
7/// Conversion trait for creating nodes from async functions
8///
9/// This trait allows async functions with various signatures to be used
10/// as nodes in a Juncture graph.
11///
12/// # Supported Function Signatures
13///
14/// The trait supports multiple function signatures for flexibility:
15///
16/// ## Forms A-D (existing, without Runtime)
17///
18/// ```ignore
19/// use juncture_core::{IntoNode, State, Node, Command};
20/// use juncture_core::node::NodeFnUpdate;
21/// use std::sync::Arc;
22///
23/// # struct MyState;
24/// # impl State for MyState { type Update = MyStateUpdate; }
25/// # struct MyStateUpdate;
26///
27/// // Form A: fn(S) -> Result<S::Update>
28/// async fn simple_node(state: MyState) -> Result<MyStateUpdate, juncture_core::JunctureError> {
29///     Ok(MyStateUpdate)
30/// }
31///
32/// // Form B: fn(S, RunnableConfig) -> Result<S::Update>
33/// async fn with_config(state: MyState, config: RunnableConfig) -> Result<MyStateUpdate, juncture_core::JunctureError> {
34///     Ok(MyStateUpdate)
35/// }
36///
37/// // Form C: fn(S) -> Result<Command<S>>
38/// async fn with_command(state: MyState) -> Result<Command<MyState>, juncture_core::JunctureError> {
39///     Ok(Command::end())
40/// }
41///
42/// // Form D: fn(S, RunnableConfig) -> Result<Command<S>>
43/// async fn full_form(state: MyState, config: RunnableConfig) -> Result<Command<MyState>, juncture_core::JunctureError> {
44///     Ok(Command::end())
45/// }
46/// ```
47///
48/// ## Forms E-F (new, with Runtime<C> for dependency injection)
49///
50/// ```ignore
51/// use juncture_core::{IntoNode, State, Runtime};
52/// use juncture_core::node::NodeFnUpdateWithRuntime;
53///
54/// # struct MyState;
55/// # impl State for MyState { type Update = MyStateUpdate; }
56/// # struct MyStateUpdate;
57/// # struct MyContext { user_id: String }
58///
59/// // Form E: fn(S, Runtime<C>) -> Result<S::Update>
60/// async fn with_runtime(state: MyState, runtime: Runtime<MyContext>) -> Result<MyStateUpdate, juncture_core::JunctureError> {
61///     // Access runtime context
62///     let user_id = &runtime.context.user_id;
63///     Ok(MyStateUpdate)
64/// }
65///
66/// // Form F: fn(S, RunnableConfig, Runtime<C>) -> Result<S::Update>
67/// async fn with_all(state: MyState, config: RunnableConfig, runtime: Runtime<MyContext>) -> Result<MyStateUpdate, juncture_core::JunctureError> {
68///     // Full access to state, config, and runtime
69///     Ok(MyStateUpdate)
70/// }
71/// ```
72///
73/// # Examples
74///
75/// ```ignore
76/// use juncture_core::{IntoNode, State, Runtime};
77/// use juncture_core::node::NodeFnUpdateWithRuntime;
78/// use std::sync::Arc;
79///
80/// # struct MyState;
81/// # impl State for MyState { type Update = MyStateUpdate; }
82/// # struct MyStateUpdate;
83/// # struct MyContext { user_id: String }
84///
85/// // Create a Runtime with custom context
86/// let runtime = Runtime::with_context(MyContext { user_id: "user-123".to_string() });
87///
88/// // Wrap a Runtime-aware function
89/// let wrapper = NodeFnUpdateWithRuntime::new(
90///     async fn my_node(state: MyState, runtime: Runtime<MyContext>) -> Result<MyStateUpdate, JunctureError> {
91///         // Use runtime context
92///         Ok(MyStateUpdate)
93///     },
94///     runtime
95/// );
96///
97/// // Convert to a node
98/// let node: Arc<dyn Node<MyState>> = wrapper.into_node("my_node");
99/// ```
100pub trait IntoNode<S: State> {
101    /// Convert this value into a node with the given name
102    fn into_node(self, name: &str) -> Arc<dyn Node<S>>;
103}
104
105/// Wrapper for async functions returning `Result<S::Update, JunctureError>`
106#[derive(Debug)]
107pub struct NodeFnUpdate<F>(pub F);
108
109/// Wrapper for async functions taking `RunnableConfig` and returning `Result<S::Update, JunctureError>`
110#[derive(Debug)]
111pub struct NodeFnUpdateWithConfig<F>(pub F);
112
113/// Wrapper for async functions returning `Result<Command<S>, JunctureError>`
114#[derive(Debug)]
115pub struct NodeFnCommand<F>(pub F);
116
117/// Wrapper for async functions taking `RunnableConfig` and returning `Result<Command<S>, JunctureError>`
118#[derive(Debug)]
119pub struct NodeFnCommandWithConfig<F>(pub F);
120
121/// Wrapper for async functions taking `Runtime<C>` and returning `Result<S::Update, JunctureError>`
122///
123/// Form E: async functions that receive Runtime for dependency injection
124/// and return a state update.
125///
126/// # Examples
127///
128/// ```ignore
129/// use juncture_core::{IntoNode, State, Runtime};
130/// use juncture_core::node::NodeFnUpdateWithRuntime;
131///
132/// struct MyContext;
133/// struct MyState;
134/// impl State for MyState {
135///     type Update = MyStateUpdate;
136/// }
137///
138/// async fn my_node(state: MyState, runtime: Runtime<MyContext>) -> Result<MyStateUpdate, JunctureError> {
139///     Ok(MyStateUpdate)
140/// }
141///
142/// let node = NodeFnUpdateWithRuntime::new(my_node).into_node("my_node");
143/// ```
144pub struct NodeFnUpdateWithRuntime<F, C>
145where
146    C: Clone + Send + Sync + 'static,
147{
148    /// The wrapped async function
149    pub func: F,
150    /// Runtime context to inject into the function
151    pub runtime: Runtime<C>,
152    _phantom: PhantomData<fn() -> C>,
153}
154
155impl<F, C> std::fmt::Debug for NodeFnUpdateWithRuntime<F, C>
156where
157    F: std::fmt::Debug,
158    C: Clone + Send + Sync + 'static + std::fmt::Debug,
159{
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("NodeFnUpdateWithRuntime")
162            .field("func", &self.func)
163            .field("runtime", &self.runtime)
164            .field("_phantom", &self._phantom)
165            .finish()
166    }
167}
168
169impl<F, C> NodeFnUpdateWithRuntime<F, C>
170where
171    C: Clone + Send + Sync + 'static,
172{
173    /// Create a new wrapper for a Runtime-aware async function
174    ///
175    /// # Arguments
176    ///
177    /// * `func` - Async function accepting (state, Runtime<C>)
178    /// * `runtime` - Runtime context to inject
179    #[must_use]
180    pub const fn new(func: F, runtime: Runtime<C>) -> Self {
181        Self {
182            func,
183            runtime,
184            _phantom: PhantomData,
185        }
186    }
187}
188
189/// Wrapper for async functions taking `(S, RunnableConfig, Runtime<C>)` and returning `Result<S::Update, JunctureError>`
190///
191/// Form F: async functions that receive state, config, and Runtime
192/// and return a state update.
193pub struct NodeFnUpdateWithConfigAndRuntime<F, C>
194where
195    C: Clone + Send + Sync + 'static,
196{
197    /// The wrapped async function
198    pub func: F,
199    /// Runtime context to inject into the function
200    pub runtime: Runtime<C>,
201    _phantom: PhantomData<fn() -> C>,
202}
203
204impl<F, C> std::fmt::Debug for NodeFnUpdateWithConfigAndRuntime<F, C>
205where
206    F: std::fmt::Debug,
207    C: Clone + Send + Sync + 'static + std::fmt::Debug,
208{
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        f.debug_struct("NodeFnUpdateWithConfigAndRuntime")
211            .field("func", &self.func)
212            .field("runtime", &self.runtime)
213            .field("_phantom", &self._phantom)
214            .finish()
215    }
216}
217
218impl<F, C> NodeFnUpdateWithConfigAndRuntime<F, C>
219where
220    C: Clone + Send + Sync + 'static,
221{
222    /// Create a new wrapper for a Runtime-aware async function with config
223    ///
224    /// # Arguments
225    ///
226    /// * `func` - Async function accepting (state, config, Runtime<C>)
227    /// * `runtime` - Runtime context to inject
228    #[must_use]
229    pub const fn new(func: F, runtime: Runtime<C>) -> Self {
230        Self {
231            func,
232            runtime,
233            _phantom: PhantomData,
234        }
235    }
236}
237
238/// Wrapper for async functions taking `Runtime<C>` and returning `Result<Command<S>, JunctureError>`
239///
240/// Form E (Command variant): async functions that receive Runtime
241/// and return a Command.
242pub struct NodeFnCommandWithRuntime<F, C>
243where
244    C: Clone + Send + Sync + 'static,
245{
246    /// The wrapped async function
247    pub func: F,
248    /// Runtime context to inject into the function
249    pub runtime: Runtime<C>,
250    _phantom: PhantomData<fn() -> C>,
251}
252
253impl<F, C> std::fmt::Debug for NodeFnCommandWithRuntime<F, C>
254where
255    F: std::fmt::Debug,
256    C: Clone + Send + Sync + 'static + std::fmt::Debug,
257{
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        f.debug_struct("NodeFnCommandWithRuntime")
260            .field("func", &self.func)
261            .field("runtime", &self.runtime)
262            .field("_phantom", &self._phantom)
263            .finish()
264    }
265}
266
267impl<F, C> NodeFnCommandWithRuntime<F, C>
268where
269    C: Clone + Send + Sync + 'static,
270{
271    /// Create a new wrapper for a Runtime-aware async function returning Command
272    ///
273    /// # Arguments
274    ///
275    /// * `func` - Async function accepting (state, Runtime<C>)
276    /// * `runtime` - Runtime context to inject
277    #[must_use]
278    pub const fn new(func: F, runtime: Runtime<C>) -> Self {
279        Self {
280            func,
281            runtime,
282            _phantom: PhantomData,
283        }
284    }
285}
286
287/// Wrapper for async functions taking `(S, RunnableConfig, Runtime<C>)` and returning `Result<Command<S>, JunctureError>`
288///
289/// Form F (Command variant): async functions that receive state, config, and Runtime
290/// and return a Command.
291pub struct NodeFnCommandWithConfigAndRuntime<F, C>
292where
293    C: Clone + Send + Sync + 'static,
294{
295    /// The wrapped async function
296    pub func: F,
297    /// Runtime context to inject into the function
298    pub runtime: Runtime<C>,
299    _phantom: PhantomData<fn() -> C>,
300}
301
302impl<F, C> std::fmt::Debug for NodeFnCommandWithConfigAndRuntime<F, C>
303where
304    F: std::fmt::Debug,
305    C: Clone + Send + Sync + 'static + std::fmt::Debug,
306{
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        f.debug_struct("NodeFnCommandWithConfigAndRuntime")
309            .field("func", &self.func)
310            .field("runtime", &self.runtime)
311            .field("_phantom", &self._phantom)
312            .finish()
313    }
314}
315
316impl<F, C> NodeFnCommandWithConfigAndRuntime<F, C>
317where
318    C: Clone + Send + Sync + 'static,
319{
320    /// Create a new wrapper for a Runtime-aware async function with config returning Command
321    ///
322    /// # Arguments
323    ///
324    /// * `func` - Async function accepting (state, config, Runtime<C>)
325    /// * `runtime` - Runtime context to inject
326    #[must_use]
327    pub const fn new(func: F, runtime: Runtime<C>) -> Self {
328        Self {
329            func,
330            runtime,
331            _phantom: PhantomData,
332        }
333    }
334}
335
336// Existing blanket impls for forms A-D
337
338impl<S, F, Fut> IntoNode<S> for NodeFnUpdate<F>
339where
340    S: State,
341    F: Fn(&S) -> Fut + Send + Sync + 'static,
342    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
343{
344    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
345        Arc::new(FnNodeUpdateOnly {
346            name: name.to_string(),
347            func: self.0,
348            _phantom: PhantomData,
349        })
350    }
351}
352
353impl<S, F, Fut> IntoNode<S> for NodeFnUpdateWithConfig<F>
354where
355    S: State,
356    F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
357    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
358{
359    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
360        Arc::new(FnNodeUpdateWithConfig {
361            name: name.to_string(),
362            func: self.0,
363            _phantom: PhantomData,
364        })
365    }
366}
367
368impl<S, F, Fut> IntoNode<S> for NodeFnCommand<F>
369where
370    S: State,
371    F: Fn(&S) -> Fut + Send + Sync + 'static,
372    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
373{
374    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
375        Arc::new(FnNodeCommandOnly {
376            name: name.to_string(),
377            func: self.0,
378            _phantom: PhantomData,
379        })
380    }
381}
382
383impl<S, F, Fut> IntoNode<S> for NodeFnCommandWithConfig<F>
384where
385    S: State,
386    F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
387    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
388{
389    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
390        Arc::new(FnNodeCommandWithConfig {
391            name: name.to_string(),
392            func: self.0,
393            _phantom: PhantomData,
394        })
395    }
396}
397
398// Form E (Update variant): Runtime<C> parameter, returns Update
399impl<S, F, Fut, C> IntoNode<S> for NodeFnUpdateWithRuntime<F, C>
400where
401    S: State,
402    C: Clone + Send + Sync + 'static,
403    F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
404    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
405{
406    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
407        Arc::new(FnNodeUpdateWithRuntime {
408            name: name.to_string(),
409            func: self.func,
410            runtime: self.runtime,
411            _phantom: PhantomData,
412        })
413    }
414}
415
416// Form F (Update variant): config + Runtime<C> parameter, returns Update
417impl<S, F, Fut, C> IntoNode<S> for NodeFnUpdateWithConfigAndRuntime<F, C>
418where
419    S: State,
420    C: Clone + Send + Sync + 'static,
421    F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
422    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
423{
424    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
425        Arc::new(FnNodeUpdateWithConfigAndRuntime {
426            name: name.to_string(),
427            func: self.func,
428            runtime: self.runtime,
429            _phantom: PhantomData,
430        })
431    }
432}
433
434// Form E (Command variant): Runtime<C> parameter, returns Command
435impl<S, F, Fut, C> IntoNode<S> for NodeFnCommandWithRuntime<F, C>
436where
437    S: State,
438    C: Clone + Send + Sync + 'static,
439    F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
440    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
441{
442    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
443        Arc::new(FnNodeCommandWithRuntime {
444            name: name.to_string(),
445            func: self.func,
446            runtime: self.runtime,
447            _phantom: PhantomData,
448        })
449    }
450}
451
452// Form F (Command variant): config + Runtime<C> parameter, returns Command
453impl<S, F, Fut, C> IntoNode<S> for NodeFnCommandWithConfigAndRuntime<F, C>
454where
455    S: State,
456    C: Clone + Send + Sync + 'static,
457    F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
458    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
459{
460    fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
461        Arc::new(FnNodeCommandWithConfigAndRuntime {
462            name: name.to_string(),
463            func: self.func,
464            runtime: self.runtime,
465            _phantom: PhantomData,
466        })
467    }
468}
469
470#[allow(
471    dead_code,
472    reason = "fields used via Node trait, not directly accessed"
473)]
474struct FnNodeUpdateOnly<S, F, Fut>
475where
476    S: State,
477    F: Fn(&S) -> Fut + Send + Sync + 'static,
478    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
479{
480    name: String,
481    func: F,
482    _phantom: PhantomData<fn(&S) -> Fut>,
483}
484
485impl<S, F, Fut> Node<S> for FnNodeUpdateOnly<S, F, Fut>
486where
487    S: State,
488    F: Fn(&S) -> Fut + Send + Sync + 'static,
489    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
490{
491    fn call(
492        &self,
493        state: &S,
494        _config: &RunnableConfig,
495    ) -> std::pin::Pin<
496        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
497    > {
498        let state_clone = state.clone();
499        let func = &self.func;
500        Box::pin(async move {
501            let update = func(&state_clone).await?;
502            Ok(Command::update(update))
503        })
504    }
505
506    fn call_arc(
507        &self,
508        state: std::sync::Arc<S>,
509        _config: &RunnableConfig,
510    ) -> std::pin::Pin<
511        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
512    > {
513        let state_arc = std::sync::Arc::clone(&state);
514        let func = &self.func;
515        Box::pin(async move {
516            let update = func(&state_arc).await?;
517            Ok(Command::update(update))
518        })
519    }
520
521    fn name(&self) -> &str {
522        &self.name
523    }
524}
525
526#[allow(
527    dead_code,
528    reason = "fields used via Node trait, not directly accessed"
529)]
530struct FnNodeUpdateWithConfig<S, F, Fut>
531where
532    S: State,
533    F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
534    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
535{
536    name: String,
537    func: F,
538    _phantom: PhantomData<fn(&S, RunnableConfig) -> Fut>,
539}
540
541impl<S, F, Fut> Node<S> for FnNodeUpdateWithConfig<S, F, Fut>
542where
543    S: State,
544    F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
545    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
546{
547    fn call(
548        &self,
549        state: &S,
550        config: &RunnableConfig,
551    ) -> std::pin::Pin<
552        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
553    > {
554        let config = config.clone();
555        let state_clone = state.clone();
556        let func = &self.func;
557        Box::pin(async move {
558            let update = func(&state_clone, config).await?;
559            Ok(Command::update(update))
560        })
561    }
562
563    fn call_arc(
564        &self,
565        state: std::sync::Arc<S>,
566        config: &RunnableConfig,
567    ) -> std::pin::Pin<
568        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
569    > {
570        let config = config.clone();
571        let state_arc = std::sync::Arc::clone(&state);
572        let func = &self.func;
573        Box::pin(async move {
574            let update = func(&state_arc, config).await?;
575            Ok(Command::update(update))
576        })
577    }
578
579    fn name(&self) -> &str {
580        &self.name
581    }
582}
583
584#[allow(
585    dead_code,
586    reason = "fields used via Node trait, not directly accessed"
587)]
588struct FnNodeCommandOnly<S, F, Fut>
589where
590    S: State,
591    F: Fn(&S) -> Fut + Send + Sync + 'static,
592    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
593{
594    name: String,
595    func: F,
596    _phantom: PhantomData<fn(&S) -> Fut>,
597}
598
599impl<S, F, Fut> Node<S> for FnNodeCommandOnly<S, F, Fut>
600where
601    S: State,
602    F: Fn(&S) -> Fut + Send + Sync + 'static,
603    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
604{
605    fn call(
606        &self,
607        state: &S,
608        _config: &RunnableConfig,
609    ) -> std::pin::Pin<
610        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
611    > {
612        let state_clone = state.clone();
613        let func = &self.func;
614        Box::pin(async move { func(&state_clone).await })
615    }
616
617    fn call_arc(
618        &self,
619        state: std::sync::Arc<S>,
620        _config: &RunnableConfig,
621    ) -> std::pin::Pin<
622        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
623    > {
624        let state_arc = std::sync::Arc::clone(&state);
625        let func = &self.func;
626        Box::pin(async move { func(&state_arc).await })
627    }
628
629    fn name(&self) -> &str {
630        &self.name
631    }
632}
633
634#[allow(
635    dead_code,
636    reason = "fields used via Node trait, not directly accessed"
637)]
638struct FnNodeCommandWithConfig<S, F, Fut>
639where
640    S: State,
641    F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
642    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
643{
644    name: String,
645    func: F,
646    _phantom: PhantomData<fn(&S, RunnableConfig) -> Fut>,
647}
648
649impl<S, F, Fut> Node<S> for FnNodeCommandWithConfig<S, F, Fut>
650where
651    S: State,
652    F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
653    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
654{
655    fn call(
656        &self,
657        state: &S,
658        config: &RunnableConfig,
659    ) -> std::pin::Pin<
660        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
661    > {
662        let config = config.clone();
663        let state_clone = state.clone();
664        let func = &self.func;
665        Box::pin(async move { func(&state_clone, config).await })
666    }
667
668    fn call_arc(
669        &self,
670        state: std::sync::Arc<S>,
671        config: &RunnableConfig,
672    ) -> std::pin::Pin<
673        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
674    > {
675        let config = config.clone();
676        let state_arc = std::sync::Arc::clone(&state);
677        let func = &self.func;
678        Box::pin(async move { func(&state_arc, config).await })
679    }
680
681    fn name(&self) -> &str {
682        &self.name
683    }
684}
685
686// Form E (Update variant): Runtime<C> parameter
687#[allow(
688    dead_code,
689    reason = "fields used via Node trait, not directly accessed"
690)]
691struct FnNodeUpdateWithRuntime<S, F, Fut, C>
692where
693    S: State,
694    C: Clone + Send + Sync + 'static,
695    F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
696    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
697{
698    name: String,
699    func: F,
700    runtime: Runtime<C>,
701    #[allow(
702        clippy::type_complexity,
703        reason = "PhantomData needs to capture all generic parameters including complex Future type"
704    )]
705    _phantom: PhantomData<fn(&S, Runtime<C>) -> Fut>,
706}
707
708impl<S, F, Fut, C> Node<S> for FnNodeUpdateWithRuntime<S, F, Fut, C>
709where
710    S: State,
711    C: Clone + Send + Sync + 'static,
712    F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
713    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
714{
715    fn call(
716        &self,
717        state: &S,
718        _config: &RunnableConfig,
719    ) -> std::pin::Pin<
720        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
721    > {
722        let runtime = self.runtime.clone();
723        let state_clone = state.clone();
724        let func = &self.func;
725        Box::pin(async move {
726            let update = func(&state_clone, runtime).await?;
727            Ok(Command::update(update))
728        })
729    }
730
731    fn call_arc(
732        &self,
733        state: std::sync::Arc<S>,
734        _config: &RunnableConfig,
735    ) -> std::pin::Pin<
736        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
737    > {
738        let runtime = self.runtime.clone();
739        let state_arc = std::sync::Arc::clone(&state);
740        let func = &self.func;
741        Box::pin(async move {
742            let update = func(&state_arc, runtime).await?;
743            Ok(Command::update(update))
744        })
745    }
746
747    fn name(&self) -> &str {
748        &self.name
749    }
750}
751
752// Form F (Update variant): config + Runtime<C> parameter
753#[allow(
754    dead_code,
755    reason = "fields used via Node trait, not directly accessed"
756)]
757struct FnNodeUpdateWithConfigAndRuntime<S, F, Fut, C>
758where
759    S: State,
760    C: Clone + Send + Sync + 'static,
761    F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
762    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
763{
764    name: String,
765    func: F,
766    runtime: Runtime<C>,
767    #[allow(
768        clippy::type_complexity,
769        reason = "PhantomData needs to capture all generic parameters including complex Future type"
770    )]
771    _phantom: PhantomData<fn(&S, RunnableConfig, Runtime<C>) -> Fut>,
772}
773
774impl<S, F, Fut, C> Node<S> for FnNodeUpdateWithConfigAndRuntime<S, F, Fut, C>
775where
776    S: State,
777    C: Clone + Send + Sync + 'static,
778    F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
779    Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
780{
781    fn call(
782        &self,
783        state: &S,
784        config: &RunnableConfig,
785    ) -> std::pin::Pin<
786        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
787    > {
788        let config = config.clone();
789        let runtime = self.runtime.clone();
790        let state = state.clone();
791        Box::pin(async move {
792            let update = (self.func)(&state, config, runtime).await?;
793            Ok(Command::update(update))
794        })
795    }
796
797    fn call_arc(
798        &self,
799        state: std::sync::Arc<S>,
800        config: &RunnableConfig,
801    ) -> std::pin::Pin<
802        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
803    > {
804        let config = config.clone();
805        let runtime = self.runtime.clone();
806        let state_arc = std::sync::Arc::clone(&state);
807        Box::pin(async move {
808            let update = (self.func)(&state_arc, config, runtime).await?;
809            Ok(Command::update(update))
810        })
811    }
812
813    fn name(&self) -> &str {
814        &self.name
815    }
816}
817
818// Form E (Command variant): Runtime<C> parameter
819#[allow(
820    dead_code,
821    reason = "fields used via Node trait, not directly accessed"
822)]
823struct FnNodeCommandWithRuntime<S, F, Fut, C>
824where
825    S: State,
826    C: Clone + Send + Sync + 'static,
827    F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
828    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
829{
830    name: String,
831    func: F,
832    runtime: Runtime<C>,
833    #[allow(
834        clippy::type_complexity,
835        reason = "PhantomData needs to capture all generic parameters including complex Future type"
836    )]
837    _phantom: PhantomData<fn(&S, Runtime<C>) -> Fut>,
838}
839
840impl<S, F, Fut, C> Node<S> for FnNodeCommandWithRuntime<S, F, Fut, C>
841where
842    S: State,
843    C: Clone + Send + Sync + 'static,
844    F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
845    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
846{
847    fn call(
848        &self,
849        state: &S,
850        _config: &RunnableConfig,
851    ) -> std::pin::Pin<
852        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
853    > {
854        let runtime = self.runtime.clone();
855        let state = state.clone();
856        Box::pin(async move { (self.func)(&state, runtime).await })
857    }
858
859    fn call_arc(
860        &self,
861        state: std::sync::Arc<S>,
862        _config: &RunnableConfig,
863    ) -> std::pin::Pin<
864        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
865    > {
866        let runtime = self.runtime.clone();
867        let state_arc = std::sync::Arc::clone(&state);
868        Box::pin(async move { (self.func)(&state_arc, runtime).await })
869    }
870
871    fn name(&self) -> &str {
872        &self.name
873    }
874}
875
876// Form F (Command variant): config + Runtime<C> parameter
877#[allow(
878    dead_code,
879    reason = "fields used via Node trait, not directly accessed"
880)]
881struct FnNodeCommandWithConfigAndRuntime<S, F, Fut, C>
882where
883    S: State,
884    C: Clone + Send + Sync + 'static,
885    F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
886    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
887{
888    name: String,
889    func: F,
890    runtime: Runtime<C>,
891    #[allow(
892        clippy::type_complexity,
893        reason = "PhantomData needs to capture all generic parameters including complex Future type"
894    )]
895    _phantom: PhantomData<fn(&S, RunnableConfig, Runtime<C>) -> Fut>,
896}
897
898impl<S, F, Fut, C> Node<S> for FnNodeCommandWithConfigAndRuntime<S, F, Fut, C>
899where
900    S: State,
901    C: Clone + Send + Sync + 'static,
902    F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
903    Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
904{
905    fn call(
906        &self,
907        state: &S,
908        config: &RunnableConfig,
909    ) -> std::pin::Pin<
910        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
911    > {
912        let config = config.clone();
913        let runtime = self.runtime.clone();
914        let state = state.clone();
915        Box::pin(async move { (self.func)(&state, config, runtime).await })
916    }
917
918    fn call_arc(
919        &self,
920        state: std::sync::Arc<S>,
921        config: &RunnableConfig,
922    ) -> std::pin::Pin<
923        Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
924    > {
925        let config = config.clone();
926        let runtime = self.runtime.clone();
927        let state_arc = std::sync::Arc::clone(&state);
928        Box::pin(async move { (self.func)(&state_arc, config, runtime).await })
929    }
930
931    fn name(&self) -> &str {
932        &self.name
933    }
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939    use crate::FieldsChanged;
940    use crate::state::FieldVersions;
941
942    type BoxResult<T> = std::pin::Pin<
943        Box<dyn std::future::Future<Output = Result<T, crate::JunctureError>> + Send>,
944    >;
945
946    // Test state types
947    #[derive(Debug, Clone, Default, PartialEq)]
948    struct TestState {
949        value: i32,
950    }
951
952    #[derive(Debug, Clone, Default, PartialEq)]
953    struct TestStateUpdate {
954        value: Option<i32>,
955    }
956
957    impl State for TestState {
958        type Update = TestStateUpdate;
959        type FieldVersions = FieldVersions;
960
961        fn apply(&mut self, update: Self::Update) -> FieldsChanged {
962            if update.value.is_some() {
963                self.value = update.value.unwrap();
964                FieldsChanged(1u64) // Field 0 changed
965            } else {
966                FieldsChanged(0)
967            }
968        }
969
970        fn reset_ephemeral(&mut self) {
971            // No ephemeral fields in TestState
972        }
973    }
974
975    // Test context type
976    #[derive(Debug, Clone, Default)]
977    struct TestContext {
978        user_id: String,
979    }
980
981    // Test helper functions for forms E/F (kept for reference but inlined in tests
982    // due to higher-ranked lifetime constraints with &State closures)
983    #[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
984    #[allow(
985        clippy::unused_async,
986        reason = "kept as reference for async node pattern"
987    )]
988    async fn form_e_update_node(
989        state: &TestState,
990        runtime: Runtime<TestContext>,
991    ) -> Result<TestStateUpdate, JunctureError> {
992        assert_eq!(runtime.context.user_id, "test-user");
993        Ok(TestStateUpdate {
994            value: Some(state.value + 10),
995        })
996    }
997
998    #[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
999    #[allow(
1000        clippy::unused_async,
1001        reason = "kept as reference for async node pattern"
1002    )]
1003    async fn form_f_update_node(
1004        state: &TestState,
1005        config: RunnableConfig,
1006        _runtime: Runtime<TestContext>,
1007    ) -> Result<TestStateUpdate, JunctureError> {
1008        assert_eq!(config.recursion_limit, 0);
1009        Ok(TestStateUpdate {
1010            value: Some(state.value + 20),
1011        })
1012    }
1013
1014    #[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
1015    #[allow(
1016        clippy::unused_async,
1017        reason = "kept as reference for async node pattern"
1018    )]
1019    async fn form_e_command_node(
1020        state: &TestState,
1021        runtime: Runtime<TestContext>,
1022    ) -> Result<Command<TestState>, JunctureError> {
1023        assert_eq!(runtime.context.user_id, "test-user-3");
1024        Ok(Command::update(TestStateUpdate {
1025            value: Some(state.value + 30),
1026        }))
1027    }
1028
1029    #[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
1030    #[allow(
1031        clippy::unused_async,
1032        reason = "kept as reference for async node pattern"
1033    )]
1034    async fn form_f_command_node(
1035        state: &TestState,
1036        config: RunnableConfig,
1037        _runtime: Runtime<TestContext>,
1038    ) -> Result<Command<TestState>, JunctureError> {
1039        assert_eq!(config.recursion_limit, 0);
1040        Ok(Command::update(TestStateUpdate {
1041            value: Some(state.value + 40),
1042        }))
1043    }
1044
1045    #[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
1046    #[allow(
1047        clippy::unused_async,
1048        reason = "kept as reference for async node pattern"
1049    )]
1050    async fn shared_runtime_node(
1051        state: &TestState,
1052        _runtime: Runtime<TestContext>,
1053    ) -> Result<TestStateUpdate, JunctureError> {
1054        Ok(TestStateUpdate {
1055            value: Some(state.value + 1),
1056        })
1057    }
1058
1059    // Form E test: Runtime<C> parameter, returns Update
1060    #[tokio::test]
1061    async fn test_form_e_update_with_runtime() {
1062        let runtime = Runtime::with_context(TestContext {
1063            user_id: "test-user".to_string(),
1064        });
1065
1066        let wrapper = NodeFnUpdateWithRuntime::new(
1067            |state: &TestState, rt: Runtime<TestContext>| -> BoxResult<_> {
1068                let value = state.value;
1069                Box::pin(async move {
1070                    assert_eq!(rt.context.user_id, "test-user");
1071                    Ok(TestStateUpdate {
1072                        value: Some(value + 10),
1073                    })
1074                })
1075            },
1076            runtime,
1077        );
1078        let node = wrapper.into_node("test_node");
1079
1080        let result = node
1081            .call(&TestState { value: 5 }, &RunnableConfig::default())
1082            .await
1083            .unwrap();
1084
1085        assert_eq!(result.update.unwrap().value, Some(15));
1086        assert_eq!(node.name(), "test_node");
1087    }
1088
1089    // Form F test: config + Runtime<C> parameter, returns Update
1090    #[tokio::test]
1091    async fn test_form_f_update_with_config_and_runtime() {
1092        let runtime = Runtime::with_context(TestContext {
1093            user_id: "test-user-2".to_string(),
1094        });
1095
1096        let wrapper = NodeFnUpdateWithConfigAndRuntime::new(
1097            |state: &TestState, cfg: RunnableConfig, rt: Runtime<TestContext>| -> BoxResult<_> {
1098                let value = state.value;
1099                Box::pin(async move {
1100                    assert_eq!(rt.context.user_id, "test-user-2");
1101                    assert_eq!(cfg.recursion_limit, 0);
1102                    Ok(TestStateUpdate {
1103                        value: Some(value + 20),
1104                    })
1105                })
1106            },
1107            runtime,
1108        );
1109        let node = wrapper.into_node("test_node");
1110
1111        let result = node
1112            .call(&TestState { value: 5 }, &RunnableConfig::default())
1113            .await
1114            .unwrap();
1115
1116        assert_eq!(result.update.unwrap().value, Some(25));
1117    }
1118
1119    // Form E test: Runtime<C> parameter, returns Command
1120    #[tokio::test]
1121    async fn test_form_e_command_with_runtime() {
1122        let runtime = Runtime::with_context(TestContext {
1123            user_id: "test-user-3".to_string(),
1124        });
1125
1126        let wrapper = NodeFnCommandWithRuntime::new(
1127            |state: &TestState, rt: Runtime<TestContext>| -> BoxResult<_> {
1128                let value = state.value;
1129                Box::pin(async move {
1130                    assert_eq!(rt.context.user_id, "test-user-3");
1131                    Ok(crate::Command::update(TestStateUpdate {
1132                        value: Some(value + 30),
1133                    }))
1134                })
1135            },
1136            runtime,
1137        );
1138        let node = wrapper.into_node("test_node");
1139
1140        let result = node
1141            .call(&TestState { value: 5 }, &RunnableConfig::default())
1142            .await
1143            .unwrap();
1144
1145        assert_eq!(result.update.unwrap().value, Some(35));
1146    }
1147
1148    // Form F test: config + Runtime<C> parameter, returns Command
1149    #[tokio::test]
1150    async fn test_form_f_command_with_config_and_runtime() {
1151        let runtime = Runtime::with_context(TestContext {
1152            user_id: "test-user-4".to_string(),
1153        });
1154
1155        let wrapper = NodeFnCommandWithConfigAndRuntime::new(
1156            |state: &TestState, cfg: RunnableConfig, rt: Runtime<TestContext>| -> BoxResult<_> {
1157                let value = state.value;
1158                Box::pin(async move {
1159                    assert_eq!(rt.context.user_id, "test-user-4");
1160                    assert_eq!(cfg.recursion_limit, 0);
1161                    Ok(crate::Command::update(TestStateUpdate {
1162                        value: Some(value + 40),
1163                    }))
1164                })
1165            },
1166            runtime,
1167        );
1168        let node = wrapper.into_node("test_node");
1169
1170        let result = node
1171            .call(&TestState { value: 5 }, &RunnableConfig::default())
1172            .await
1173            .unwrap();
1174
1175        assert_eq!(result.update.unwrap().value, Some(45));
1176    }
1177
1178    // Test that Runtime can be cloned and used across multiple invocations
1179    #[tokio::test]
1180    async fn test_runtime_clone_multiple_invocations() {
1181        let runtime = Runtime::with_context(TestContext {
1182            user_id: "shared-user".to_string(),
1183        });
1184
1185        let wrapper = NodeFnUpdateWithRuntime::new(
1186            |state: &TestState, rt: Runtime<TestContext>| -> BoxResult<_> {
1187                let value = state.value;
1188                Box::pin(async move {
1189                    let _ = rt;
1190                    Ok(TestStateUpdate {
1191                        value: Some(value + 1),
1192                    })
1193                })
1194            },
1195            runtime,
1196        );
1197        let node = wrapper.into_node("test_node");
1198
1199        // First invocation
1200        let result1 = node
1201            .call(&TestState { value: 0 }, &RunnableConfig::default())
1202            .await
1203            .unwrap();
1204        assert_eq!(result1.update.unwrap().value, Some(1));
1205
1206        // Second invocation (should use same Runtime)
1207        let result2 = node
1208            .call(&TestState { value: 10 }, &RunnableConfig::default())
1209            .await
1210            .unwrap();
1211        assert_eq!(result2.update.unwrap().value, Some(11));
1212    }
1213}
1214
1215// Rust guideline compliant 2026-05-23