use crate::{
State, command::Command, config::RunnableConfig, error::JunctureError, node::Node,
runtime::Runtime,
};
use std::{marker::PhantomData, sync::Arc};
pub trait IntoNode<S: State> {
fn into_node(self, name: &str) -> Arc<dyn Node<S>>;
}
#[derive(Debug)]
pub struct NodeFnUpdate<F>(pub F);
#[derive(Debug)]
pub struct NodeFnUpdateWithConfig<F>(pub F);
#[derive(Debug)]
pub struct NodeFnCommand<F>(pub F);
#[derive(Debug)]
pub struct NodeFnCommandWithConfig<F>(pub F);
pub struct NodeFnUpdateWithRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
pub func: F,
pub runtime: Runtime<C>,
_phantom: PhantomData<fn() -> C>,
}
impl<F, C> std::fmt::Debug for NodeFnUpdateWithRuntime<F, C>
where
F: std::fmt::Debug,
C: Clone + Send + Sync + 'static + std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeFnUpdateWithRuntime")
.field("func", &self.func)
.field("runtime", &self.runtime)
.field("_phantom", &self._phantom)
.finish()
}
}
impl<F, C> NodeFnUpdateWithRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
#[must_use]
pub const fn new(func: F, runtime: Runtime<C>) -> Self {
Self {
func,
runtime,
_phantom: PhantomData,
}
}
}
pub struct NodeFnUpdateWithConfigAndRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
pub func: F,
pub runtime: Runtime<C>,
_phantom: PhantomData<fn() -> C>,
}
impl<F, C> std::fmt::Debug for NodeFnUpdateWithConfigAndRuntime<F, C>
where
F: std::fmt::Debug,
C: Clone + Send + Sync + 'static + std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeFnUpdateWithConfigAndRuntime")
.field("func", &self.func)
.field("runtime", &self.runtime)
.field("_phantom", &self._phantom)
.finish()
}
}
impl<F, C> NodeFnUpdateWithConfigAndRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
#[must_use]
pub const fn new(func: F, runtime: Runtime<C>) -> Self {
Self {
func,
runtime,
_phantom: PhantomData,
}
}
}
pub struct NodeFnCommandWithRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
pub func: F,
pub runtime: Runtime<C>,
_phantom: PhantomData<fn() -> C>,
}
impl<F, C> std::fmt::Debug for NodeFnCommandWithRuntime<F, C>
where
F: std::fmt::Debug,
C: Clone + Send + Sync + 'static + std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeFnCommandWithRuntime")
.field("func", &self.func)
.field("runtime", &self.runtime)
.field("_phantom", &self._phantom)
.finish()
}
}
impl<F, C> NodeFnCommandWithRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
#[must_use]
pub const fn new(func: F, runtime: Runtime<C>) -> Self {
Self {
func,
runtime,
_phantom: PhantomData,
}
}
}
pub struct NodeFnCommandWithConfigAndRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
pub func: F,
pub runtime: Runtime<C>,
_phantom: PhantomData<fn() -> C>,
}
impl<F, C> std::fmt::Debug for NodeFnCommandWithConfigAndRuntime<F, C>
where
F: std::fmt::Debug,
C: Clone + Send + Sync + 'static + std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NodeFnCommandWithConfigAndRuntime")
.field("func", &self.func)
.field("runtime", &self.runtime)
.field("_phantom", &self._phantom)
.finish()
}
}
impl<F, C> NodeFnCommandWithConfigAndRuntime<F, C>
where
C: Clone + Send + Sync + 'static,
{
#[must_use]
pub const fn new(func: F, runtime: Runtime<C>) -> Self {
Self {
func,
runtime,
_phantom: PhantomData,
}
}
}
impl<S, F, Fut> IntoNode<S> for NodeFnUpdate<F>
where
S: State,
F: Fn(&S) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeUpdateOnly {
name: name.to_string(),
func: self.0,
_phantom: PhantomData,
})
}
}
impl<S, F, Fut> IntoNode<S> for NodeFnUpdateWithConfig<F>
where
S: State,
F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeUpdateWithConfig {
name: name.to_string(),
func: self.0,
_phantom: PhantomData,
})
}
}
impl<S, F, Fut> IntoNode<S> for NodeFnCommand<F>
where
S: State,
F: Fn(&S) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeCommandOnly {
name: name.to_string(),
func: self.0,
_phantom: PhantomData,
})
}
}
impl<S, F, Fut> IntoNode<S> for NodeFnCommandWithConfig<F>
where
S: State,
F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeCommandWithConfig {
name: name.to_string(),
func: self.0,
_phantom: PhantomData,
})
}
}
impl<S, F, Fut, C> IntoNode<S> for NodeFnUpdateWithRuntime<F, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeUpdateWithRuntime {
name: name.to_string(),
func: self.func,
runtime: self.runtime,
_phantom: PhantomData,
})
}
}
impl<S, F, Fut, C> IntoNode<S> for NodeFnUpdateWithConfigAndRuntime<F, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeUpdateWithConfigAndRuntime {
name: name.to_string(),
func: self.func,
runtime: self.runtime,
_phantom: PhantomData,
})
}
}
impl<S, F, Fut, C> IntoNode<S> for NodeFnCommandWithRuntime<F, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeCommandWithRuntime {
name: name.to_string(),
func: self.func,
runtime: self.runtime,
_phantom: PhantomData,
})
}
}
impl<S, F, Fut, C> IntoNode<S> for NodeFnCommandWithConfigAndRuntime<F, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn into_node(self, name: &str) -> Arc<dyn Node<S>> {
Arc::new(FnNodeCommandWithConfigAndRuntime {
name: name.to_string(),
func: self.func,
runtime: self.runtime,
_phantom: PhantomData,
})
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeUpdateOnly<S, F, Fut>
where
S: State,
F: Fn(&S) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
name: String,
func: F,
_phantom: PhantomData<fn(&S) -> Fut>,
}
impl<S, F, Fut> Node<S> for FnNodeUpdateOnly<S, F, Fut>
where
S: State,
F: Fn(&S) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let state_clone = state.clone();
let func = &self.func;
Box::pin(async move {
let update = func(&state_clone).await?;
Ok(Command::update(update))
})
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let state_arc = std::sync::Arc::clone(&state);
let func = &self.func;
Box::pin(async move {
let update = func(&state_arc).await?;
Ok(Command::update(update))
})
}
fn name(&self) -> &str {
&self.name
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeUpdateWithConfig<S, F, Fut>
where
S: State,
F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
name: String,
func: F,
_phantom: PhantomData<fn(&S, RunnableConfig) -> Fut>,
}
impl<S, F, Fut> Node<S> for FnNodeUpdateWithConfig<S, F, Fut>
where
S: State,
F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let state_clone = state.clone();
let func = &self.func;
Box::pin(async move {
let update = func(&state_clone, config).await?;
Ok(Command::update(update))
})
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let state_arc = std::sync::Arc::clone(&state);
let func = &self.func;
Box::pin(async move {
let update = func(&state_arc, config).await?;
Ok(Command::update(update))
})
}
fn name(&self) -> &str {
&self.name
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeCommandOnly<S, F, Fut>
where
S: State,
F: Fn(&S) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
name: String,
func: F,
_phantom: PhantomData<fn(&S) -> Fut>,
}
impl<S, F, Fut> Node<S> for FnNodeCommandOnly<S, F, Fut>
where
S: State,
F: Fn(&S) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let state_clone = state.clone();
let func = &self.func;
Box::pin(async move { func(&state_clone).await })
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let state_arc = std::sync::Arc::clone(&state);
let func = &self.func;
Box::pin(async move { func(&state_arc).await })
}
fn name(&self) -> &str {
&self.name
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeCommandWithConfig<S, F, Fut>
where
S: State,
F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
name: String,
func: F,
_phantom: PhantomData<fn(&S, RunnableConfig) -> Fut>,
}
impl<S, F, Fut> Node<S> for FnNodeCommandWithConfig<S, F, Fut>
where
S: State,
F: Fn(&S, RunnableConfig) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let state_clone = state.clone();
let func = &self.func;
Box::pin(async move { func(&state_clone, config).await })
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let state_arc = std::sync::Arc::clone(&state);
let func = &self.func;
Box::pin(async move { func(&state_arc, config).await })
}
fn name(&self) -> &str {
&self.name
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeUpdateWithRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
name: String,
func: F,
runtime: Runtime<C>,
#[allow(
clippy::type_complexity,
reason = "PhantomData needs to capture all generic parameters including complex Future type"
)]
_phantom: PhantomData<fn(&S, Runtime<C>) -> Fut>,
}
impl<S, F, Fut, C> Node<S> for FnNodeUpdateWithRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let runtime = self.runtime.clone();
let state_clone = state.clone();
let func = &self.func;
Box::pin(async move {
let update = func(&state_clone, runtime).await?;
Ok(Command::update(update))
})
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let runtime = self.runtime.clone();
let state_arc = std::sync::Arc::clone(&state);
let func = &self.func;
Box::pin(async move {
let update = func(&state_arc, runtime).await?;
Ok(Command::update(update))
})
}
fn name(&self) -> &str {
&self.name
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeUpdateWithConfigAndRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
name: String,
func: F,
runtime: Runtime<C>,
#[allow(
clippy::type_complexity,
reason = "PhantomData needs to capture all generic parameters including complex Future type"
)]
_phantom: PhantomData<fn(&S, RunnableConfig, Runtime<C>) -> Fut>,
}
impl<S, F, Fut, C> Node<S> for FnNodeUpdateWithConfigAndRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<S::Update, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let runtime = self.runtime.clone();
let state = state.clone();
Box::pin(async move {
let update = (self.func)(&state, config, runtime).await?;
Ok(Command::update(update))
})
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let runtime = self.runtime.clone();
let state_arc = std::sync::Arc::clone(&state);
Box::pin(async move {
let update = (self.func)(&state_arc, config, runtime).await?;
Ok(Command::update(update))
})
}
fn name(&self) -> &str {
&self.name
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeCommandWithRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
name: String,
func: F,
runtime: Runtime<C>,
#[allow(
clippy::type_complexity,
reason = "PhantomData needs to capture all generic parameters including complex Future type"
)]
_phantom: PhantomData<fn(&S, Runtime<C>) -> Fut>,
}
impl<S, F, Fut, C> Node<S> for FnNodeCommandWithRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let runtime = self.runtime.clone();
let state = state.clone();
Box::pin(async move { (self.func)(&state, runtime).await })
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
_config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let runtime = self.runtime.clone();
let state_arc = std::sync::Arc::clone(&state);
Box::pin(async move { (self.func)(&state_arc, runtime).await })
}
fn name(&self) -> &str {
&self.name
}
}
#[allow(
dead_code,
reason = "fields used via Node trait, not directly accessed"
)]
struct FnNodeCommandWithConfigAndRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
name: String,
func: F,
runtime: Runtime<C>,
#[allow(
clippy::type_complexity,
reason = "PhantomData needs to capture all generic parameters including complex Future type"
)]
_phantom: PhantomData<fn(&S, RunnableConfig, Runtime<C>) -> Fut>,
}
impl<S, F, Fut, C> Node<S> for FnNodeCommandWithConfigAndRuntime<S, F, Fut, C>
where
S: State,
C: Clone + Send + Sync + 'static,
F: Fn(&S, RunnableConfig, Runtime<C>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + 'static,
{
fn call(
&self,
state: &S,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let runtime = self.runtime.clone();
let state = state.clone();
Box::pin(async move { (self.func)(&state, config, runtime).await })
}
fn call_arc(
&self,
state: std::sync::Arc<S>,
config: &RunnableConfig,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<Command<S>, JunctureError>> + Send + '_>,
> {
let config = config.clone();
let runtime = self.runtime.clone();
let state_arc = std::sync::Arc::clone(&state);
Box::pin(async move { (self.func)(&state_arc, config, runtime).await })
}
fn name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FieldsChanged;
use crate::state::FieldVersions;
type BoxResult<T> = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<T, crate::JunctureError>> + Send>,
>;
#[derive(Debug, Clone, Default, PartialEq)]
struct TestState {
value: i32,
}
#[derive(Debug, Clone, Default, PartialEq)]
struct TestStateUpdate {
value: Option<i32>,
}
impl State for TestState {
type Update = TestStateUpdate;
type FieldVersions = FieldVersions;
fn apply(&mut self, update: Self::Update) -> FieldsChanged {
if update.value.is_some() {
self.value = update.value.unwrap();
FieldsChanged(1u64) } else {
FieldsChanged(0)
}
}
fn reset_ephemeral(&mut self) {
}
}
#[derive(Debug, Clone, Default)]
struct TestContext {
user_id: String,
}
#[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
#[allow(
clippy::unused_async,
reason = "kept as reference for async node pattern"
)]
async fn form_e_update_node(
state: &TestState,
runtime: Runtime<TestContext>,
) -> Result<TestStateUpdate, JunctureError> {
assert_eq!(runtime.context.user_id, "test-user");
Ok(TestStateUpdate {
value: Some(state.value + 10),
})
}
#[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
#[allow(
clippy::unused_async,
reason = "kept as reference for async node pattern"
)]
async fn form_f_update_node(
state: &TestState,
config: RunnableConfig,
_runtime: Runtime<TestContext>,
) -> Result<TestStateUpdate, JunctureError> {
assert_eq!(config.recursion_limit, 0);
Ok(TestStateUpdate {
value: Some(state.value + 20),
})
}
#[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
#[allow(
clippy::unused_async,
reason = "kept as reference for async node pattern"
)]
async fn form_e_command_node(
state: &TestState,
runtime: Runtime<TestContext>,
) -> Result<Command<TestState>, JunctureError> {
assert_eq!(runtime.context.user_id, "test-user-3");
Ok(Command::update(TestStateUpdate {
value: Some(state.value + 30),
}))
}
#[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
#[allow(
clippy::unused_async,
reason = "kept as reference for async node pattern"
)]
async fn form_f_command_node(
state: &TestState,
config: RunnableConfig,
_runtime: Runtime<TestContext>,
) -> Result<Command<TestState>, JunctureError> {
assert_eq!(config.recursion_limit, 0);
Ok(Command::update(TestStateUpdate {
value: Some(state.value + 40),
}))
}
#[expect(dead_code, reason = "inlined in tests due to lifetime constraints")]
#[allow(
clippy::unused_async,
reason = "kept as reference for async node pattern"
)]
async fn shared_runtime_node(
state: &TestState,
_runtime: Runtime<TestContext>,
) -> Result<TestStateUpdate, JunctureError> {
Ok(TestStateUpdate {
value: Some(state.value + 1),
})
}
#[tokio::test]
async fn test_form_e_update_with_runtime() {
let runtime = Runtime::with_context(TestContext {
user_id: "test-user".to_string(),
});
let wrapper = NodeFnUpdateWithRuntime::new(
|state: &TestState, rt: Runtime<TestContext>| -> BoxResult<_> {
let value = state.value;
Box::pin(async move {
assert_eq!(rt.context.user_id, "test-user");
Ok(TestStateUpdate {
value: Some(value + 10),
})
})
},
runtime,
);
let node = wrapper.into_node("test_node");
let result = node
.call(&TestState { value: 5 }, &RunnableConfig::default())
.await
.unwrap();
assert_eq!(result.update.unwrap().value, Some(15));
assert_eq!(node.name(), "test_node");
}
#[tokio::test]
async fn test_form_f_update_with_config_and_runtime() {
let runtime = Runtime::with_context(TestContext {
user_id: "test-user-2".to_string(),
});
let wrapper = NodeFnUpdateWithConfigAndRuntime::new(
|state: &TestState, cfg: RunnableConfig, rt: Runtime<TestContext>| -> BoxResult<_> {
let value = state.value;
Box::pin(async move {
assert_eq!(rt.context.user_id, "test-user-2");
assert_eq!(cfg.recursion_limit, 0);
Ok(TestStateUpdate {
value: Some(value + 20),
})
})
},
runtime,
);
let node = wrapper.into_node("test_node");
let result = node
.call(&TestState { value: 5 }, &RunnableConfig::default())
.await
.unwrap();
assert_eq!(result.update.unwrap().value, Some(25));
}
#[tokio::test]
async fn test_form_e_command_with_runtime() {
let runtime = Runtime::with_context(TestContext {
user_id: "test-user-3".to_string(),
});
let wrapper = NodeFnCommandWithRuntime::new(
|state: &TestState, rt: Runtime<TestContext>| -> BoxResult<_> {
let value = state.value;
Box::pin(async move {
assert_eq!(rt.context.user_id, "test-user-3");
Ok(crate::Command::update(TestStateUpdate {
value: Some(value + 30),
}))
})
},
runtime,
);
let node = wrapper.into_node("test_node");
let result = node
.call(&TestState { value: 5 }, &RunnableConfig::default())
.await
.unwrap();
assert_eq!(result.update.unwrap().value, Some(35));
}
#[tokio::test]
async fn test_form_f_command_with_config_and_runtime() {
let runtime = Runtime::with_context(TestContext {
user_id: "test-user-4".to_string(),
});
let wrapper = NodeFnCommandWithConfigAndRuntime::new(
|state: &TestState, cfg: RunnableConfig, rt: Runtime<TestContext>| -> BoxResult<_> {
let value = state.value;
Box::pin(async move {
assert_eq!(rt.context.user_id, "test-user-4");
assert_eq!(cfg.recursion_limit, 0);
Ok(crate::Command::update(TestStateUpdate {
value: Some(value + 40),
}))
})
},
runtime,
);
let node = wrapper.into_node("test_node");
let result = node
.call(&TestState { value: 5 }, &RunnableConfig::default())
.await
.unwrap();
assert_eq!(result.update.unwrap().value, Some(45));
}
#[tokio::test]
async fn test_runtime_clone_multiple_invocations() {
let runtime = Runtime::with_context(TestContext {
user_id: "shared-user".to_string(),
});
let wrapper = NodeFnUpdateWithRuntime::new(
|state: &TestState, rt: Runtime<TestContext>| -> BoxResult<_> {
let value = state.value;
Box::pin(async move {
let _ = rt;
Ok(TestStateUpdate {
value: Some(value + 1),
})
})
},
runtime,
);
let node = wrapper.into_node("test_node");
let result1 = node
.call(&TestState { value: 0 }, &RunnableConfig::default())
.await
.unwrap();
assert_eq!(result1.update.unwrap().value, Some(1));
let result2 = node
.call(&TestState { value: 10 }, &RunnableConfig::default())
.await
.unwrap();
assert_eq!(result2.update.unwrap().value, Some(11));
}
}