Skip to main content

ironflow_api/
state.rs

1//! Application state and dependency injection.
2//!
3//! [`AppState`] holds the shared [`RunStore`] and [`Engine`] used by all handlers.
4
5use std::sync::Arc;
6#[cfg(feature = "prometheus")]
7use std::sync::OnceLock;
8
9use axum::extract::FromRef;
10#[cfg(feature = "prometheus")]
11use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
12use uuid::Uuid;
13
14use ironflow_auth::jwt::JwtConfig;
15use ironflow_engine::engine::Engine;
16use ironflow_store::entities::Run;
17use ironflow_store::store::RunStore;
18use ironflow_store::user_store::UserStore;
19
20use crate::error::ApiError;
21
22/// Global application state.
23///
24/// Holds the shared run store and engine, extracted by handlers using Axum's
25/// state extraction mechanism.
26///
27/// # Examples
28///
29/// ```no_run
30/// use ironflow_api::state::AppState;
31/// use ironflow_auth::jwt::JwtConfig;
32/// use ironflow_store::prelude::*;
33/// use ironflow_engine::engine::Engine;
34/// use ironflow_core::providers::claude::ClaudeCodeProvider;
35/// use std::sync::Arc;
36///
37/// # async fn example() {
38/// let store = Arc::new(InMemoryStore::new());
39/// let user_store: Arc<dyn UserStore> = Arc::new(InMemoryStore::new());
40/// let provider = Arc::new(ClaudeCodeProvider::new());
41/// let engine = Arc::new(Engine::new(store.clone(), provider));
42/// let jwt_config = Arc::new(JwtConfig {
43///     secret: "secret".to_string(),
44///     access_token_ttl_secs: 900,
45///     refresh_token_ttl_secs: 604800,
46///     cookie_domain: None,
47///     cookie_secure: false,
48/// });
49/// let state = AppState::new(store, user_store, engine, jwt_config, "token".to_string());
50/// # }
51/// ```
52#[derive(Clone)]
53pub struct AppState {
54    /// The backing store for runs and steps.
55    pub store: Arc<dyn RunStore>,
56    /// The backing store for users.
57    pub user_store: Arc<dyn UserStore>,
58    /// The workflow orchestration engine.
59    pub engine: Arc<Engine>,
60    /// JWT configuration for auth tokens.
61    pub jwt_config: Arc<JwtConfig>,
62    /// Static token for worker-to-API authentication.
63    pub worker_token: String,
64    /// Prometheus metrics handle (only when `prometheus` feature is enabled).
65    #[cfg(feature = "prometheus")]
66    pub prometheus_handle: PrometheusHandle,
67}
68
69impl FromRef<AppState> for Arc<dyn RunStore> {
70    fn from_ref(state: &AppState) -> Self {
71        Arc::clone(&state.store)
72    }
73}
74
75impl FromRef<AppState> for Arc<dyn UserStore> {
76    fn from_ref(state: &AppState) -> Self {
77        Arc::clone(&state.user_store)
78    }
79}
80
81impl FromRef<AppState> for Arc<JwtConfig> {
82    fn from_ref(state: &AppState) -> Self {
83        Arc::clone(&state.jwt_config)
84    }
85}
86
87#[cfg(feature = "prometheus")]
88impl FromRef<AppState> for PrometheusHandle {
89    fn from_ref(state: &AppState) -> Self {
90        state.prometheus_handle.clone()
91    }
92}
93
94impl AppState {
95    /// Fetch a run by ID or return 404.
96    ///
97    /// # Errors
98    ///
99    /// Returns `ApiError::RunNotFound` if the run does not exist.
100    /// Returns `ApiError::Store` if there is a store error.
101    /// Create a new `AppState`.
102    ///
103    /// When the `prometheus` feature is enabled, a global Prometheus recorder
104    /// is installed (once) and its handle is stored in the state.
105    ///
106    /// # Panics
107    ///
108    /// Panics if a Prometheus recorder cannot be installed (should only
109    /// happen if another incompatible recorder was set elsewhere).
110    pub fn new(
111        store: Arc<dyn RunStore>,
112        user_store: Arc<dyn UserStore>,
113        engine: Arc<Engine>,
114        jwt_config: Arc<JwtConfig>,
115        worker_token: String,
116    ) -> Self {
117        Self {
118            store,
119            user_store,
120            engine,
121            jwt_config,
122            worker_token,
123            #[cfg(feature = "prometheus")]
124            prometheus_handle: Self::global_prometheus_handle(),
125        }
126    }
127
128    /// Install (or reuse) a global Prometheus recorder and return its handle.
129    #[cfg(feature = "prometheus")]
130    fn global_prometheus_handle() -> PrometheusHandle {
131        static HANDLE: OnceLock<PrometheusHandle> = OnceLock::new();
132        HANDLE
133            .get_or_init(|| {
134                PrometheusBuilder::new()
135                    .install_recorder()
136                    .expect("failed to install Prometheus recorder")
137            })
138            .clone()
139    }
140
141    /// Fetch a run by ID or return 404.
142    ///
143    /// # Errors
144    ///
145    /// Returns `ApiError::RunNotFound` if the run does not exist.
146    /// Returns `ApiError::Store` if there is a store error.
147    pub async fn get_run_or_404(&self, id: Uuid) -> Result<Run, ApiError> {
148        self.store
149            .get_run(id)
150            .await
151            .map_err(ApiError::from)?
152            .ok_or(ApiError::RunNotFound(id))
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use ironflow_core::providers::claude::ClaudeCodeProvider;
160    use ironflow_store::memory::InMemoryStore;
161
162    fn test_state() -> AppState {
163        let store = Arc::new(InMemoryStore::new());
164        let user_store: Arc<dyn UserStore> = Arc::new(InMemoryStore::new());
165        let provider = Arc::new(ClaudeCodeProvider::new());
166        let engine = Arc::new(Engine::new(store.clone(), provider));
167        let jwt_config = Arc::new(JwtConfig {
168            secret: "test-secret".to_string(),
169            access_token_ttl_secs: 900,
170            refresh_token_ttl_secs: 604800,
171            cookie_domain: None,
172            cookie_secure: false,
173        });
174        AppState::new(
175            store,
176            user_store,
177            engine,
178            jwt_config,
179            "test-worker-token".to_string(),
180        )
181    }
182
183    #[test]
184    fn app_state_cloneable() {
185        let state = test_state();
186        let _cloned = state.clone();
187    }
188
189    #[test]
190    fn app_state_from_ref() {
191        let state = test_state();
192        let extracted: Arc<dyn RunStore> = Arc::from_ref(&state);
193        assert!(Arc::ptr_eq(&extracted, &state.store));
194    }
195}