Skip to main content

fabryk_core/
state.rs

1//! Application state management.
2//!
3//! Provides [`AppState<C>`], a thread-safe container for shared application
4//! state that is generic over the configuration provider.
5//!
6//! # Design
7//!
8//! `AppState` in fabryk-core is intentionally minimal — it holds only the
9//! configuration. Domain applications and higher-level Fabryk crates
10//! (fabryk-fts, fabryk-graph) may wrap or extend this with their own
11//! state (search backends, graph caches, etc.).
12//!
13//! # Example
14//!
15//! ```
16//! use std::path::PathBuf;
17//! use fabryk_core::{AppState, ConfigProvider, Result};
18//!
19//! #[derive(Clone)]
20//! struct MyConfig {
21//!     name: String,
22//!     base: PathBuf,
23//! }
24//!
25//! impl ConfigProvider for MyConfig {
26//!     fn project_name(&self) -> &str { &self.name }
27//!     fn base_path(&self) -> Result<PathBuf> { Ok(self.base.clone()) }
28//!     fn content_path(&self, t: &str) -> Result<PathBuf> { Ok(self.base.join(t)) }
29//! }
30//!
31//! let config = MyConfig {
32//!     name: "my-project".into(),
33//!     base: PathBuf::from("/data"),
34//! };
35//! let state = AppState::new(config);
36//!
37//! assert_eq!(state.config().project_name(), "my-project");
38//! ```
39
40use std::sync::Arc;
41
42use crate::traits::ConfigProvider;
43
44/// Thread-safe shared application state.
45///
46/// Generic over `C: ConfigProvider` so that any domain can use it
47/// with their own configuration type. The configuration is wrapped
48/// in an `Arc` for cheap cloning and thread-safe sharing.
49///
50/// # Type Parameters
51///
52/// - `C` — The domain-specific configuration provider
53///
54/// # Thread Safety
55///
56/// `AppState` is `Clone`, `Send`, and `Sync`. Cloning is cheap (Arc clone).
57/// Multiple request handlers can share the same state concurrently.
58#[derive(Debug)]
59pub struct AppState<C: ConfigProvider> {
60    config: Arc<C>,
61}
62
63impl<C: ConfigProvider> AppState<C> {
64    /// Create a new AppState wrapping the given configuration.
65    ///
66    /// The configuration is moved into an `Arc` for shared ownership.
67    ///
68    /// # Arguments
69    ///
70    /// * `config` — The domain-specific configuration
71    ///
72    /// # Example
73    ///
74    /// ```
75    /// # use fabryk_core::{AppState, ConfigProvider, Result};
76    /// # use std::path::PathBuf;
77    /// # #[derive(Clone)]
78    /// # struct Config { base: PathBuf }
79    /// # impl ConfigProvider for Config {
80    /// #     fn project_name(&self) -> &str { "test" }
81    /// #     fn base_path(&self) -> Result<PathBuf> { Ok(self.base.clone()) }
82    /// #     fn content_path(&self, t: &str) -> Result<PathBuf> { Ok(self.base.join(t)) }
83    /// # }
84    /// let config = Config { base: PathBuf::from("/data") };
85    /// let state = AppState::new(config);
86    /// ```
87    pub fn new(config: C) -> Self {
88        Self {
89            config: Arc::new(config),
90        }
91    }
92
93    /// Create AppState from an existing Arc-wrapped configuration.
94    ///
95    /// Useful when the configuration is already shared elsewhere.
96    ///
97    /// # Arguments
98    ///
99    /// * `config` — Arc-wrapped configuration
100    pub fn from_arc(config: Arc<C>) -> Self {
101        Self { config }
102    }
103
104    /// Get a reference to the configuration.
105    ///
106    /// # Example
107    ///
108    /// ```
109    /// # use fabryk_core::{AppState, ConfigProvider, Result};
110    /// # use std::path::PathBuf;
111    /// # #[derive(Clone)]
112    /// # struct Config { name: String, base: PathBuf }
113    /// # impl ConfigProvider for Config {
114    /// #     fn project_name(&self) -> &str { &self.name }
115    /// #     fn base_path(&self) -> Result<PathBuf> { Ok(self.base.clone()) }
116    /// #     fn content_path(&self, t: &str) -> Result<PathBuf> { Ok(self.base.join(t)) }
117    /// # }
118    /// # let config = Config { name: "test".into(), base: PathBuf::from("/data") };
119    /// # let state = AppState::new(config);
120    /// let project = state.config().project_name();
121    /// ```
122    pub fn config(&self) -> &C {
123        &self.config
124    }
125
126    /// Get a cloneable handle to the configuration.
127    ///
128    /// Returns an `Arc<C>` that can be passed to subsystems that need
129    /// their own owned reference to the configuration.
130    ///
131    /// # Example
132    ///
133    /// ```
134    /// # use fabryk_core::{AppState, ConfigProvider, Result};
135    /// # use std::path::PathBuf;
136    /// # #[derive(Clone)]
137    /// # struct Config { base: PathBuf }
138    /// # impl ConfigProvider for Config {
139    /// #     fn project_name(&self) -> &str { "test" }
140    /// #     fn base_path(&self) -> Result<PathBuf> { Ok(self.base.clone()) }
141    /// #     fn content_path(&self, t: &str) -> Result<PathBuf> { Ok(self.base.join(t)) }
142    /// # }
143    /// # let config = Config { base: PathBuf::from("/data") };
144    /// # let state = AppState::new(config);
145    /// let config_arc = state.config_arc();
146    /// // Pass config_arc to another subsystem
147    /// ```
148    pub fn config_arc(&self) -> Arc<C> {
149        Arc::clone(&self.config)
150    }
151
152    /// Get the project name from the configuration.
153    ///
154    /// Convenience method equivalent to `state.config().project_name()`.
155    pub fn project_name(&self) -> &str {
156        self.config.project_name()
157    }
158}
159
160impl<C: ConfigProvider> Clone for AppState<C> {
161    fn clone(&self) -> Self {
162        Self {
163            config: Arc::clone(&self.config),
164        }
165    }
166}
167
168// Safety: AppState is Send + Sync if C is Send + Sync (which ConfigProvider requires)
169unsafe impl<C: ConfigProvider> Send for AppState<C> {}
170unsafe impl<C: ConfigProvider> Sync for AppState<C> {}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::Result;
176    use std::path::PathBuf;
177
178    #[derive(Clone, Debug)]
179    struct TestConfig {
180        name: String,
181        base: PathBuf,
182    }
183
184    impl ConfigProvider for TestConfig {
185        fn project_name(&self) -> &str {
186            &self.name
187        }
188
189        fn base_path(&self) -> Result<PathBuf> {
190            Ok(self.base.clone())
191        }
192
193        fn content_path(&self, content_type: &str) -> Result<PathBuf> {
194            Ok(self.base.join(content_type))
195        }
196    }
197
198    fn test_config() -> TestConfig {
199        TestConfig {
200            name: "test-project".into(),
201            base: PathBuf::from("/tmp/test"),
202        }
203    }
204
205    #[test]
206    fn test_app_state_new() {
207        let config = test_config();
208        let state = AppState::new(config);
209        assert_eq!(state.config().project_name(), "test-project");
210    }
211
212    #[test]
213    fn test_app_state_from_arc() {
214        let config = Arc::new(test_config());
215        let state = AppState::from_arc(config);
216        assert_eq!(state.config().project_name(), "test-project");
217    }
218
219    #[test]
220    fn test_app_state_config_ref() {
221        let config = test_config();
222        let state = AppState::new(config);
223
224        let config_ref = state.config();
225        assert_eq!(config_ref.project_name(), "test-project");
226        assert_eq!(config_ref.base_path().unwrap(), PathBuf::from("/tmp/test"));
227    }
228
229    #[test]
230    fn test_app_state_config_arc() {
231        let config = test_config();
232        let state = AppState::new(config);
233
234        let arc1 = state.config_arc();
235        let arc2 = state.config_arc();
236
237        // Both should point to same allocation
238        assert!(Arc::ptr_eq(&arc1, &arc2));
239    }
240
241    #[test]
242    fn test_app_state_project_name() {
243        let config = test_config();
244        let state = AppState::new(config);
245        assert_eq!(state.project_name(), "test-project");
246    }
247
248    #[test]
249    fn test_app_state_clone() {
250        let config = test_config();
251        let state1 = AppState::new(config);
252        let state2 = state1.clone();
253
254        // Both should share the same config
255        assert_eq!(state1.project_name(), state2.project_name());
256        assert!(Arc::ptr_eq(&state1.config_arc(), &state2.config_arc()));
257    }
258
259    #[test]
260    fn test_app_state_clone_independence() {
261        let config = test_config();
262        let state1 = AppState::new(config);
263        let state2 = state1.clone();
264
265        // Dropping one shouldn't affect the other
266        drop(state1);
267        assert_eq!(state2.project_name(), "test-project");
268    }
269
270    #[test]
271    fn test_app_state_debug() {
272        let config = test_config();
273        let state = AppState::new(config);
274        let debug_str = format!("{:?}", state);
275        assert!(debug_str.contains("AppState"));
276    }
277
278    #[test]
279    fn test_app_state_send_sync() {
280        fn assert_send_sync<T: Send + Sync>() {}
281        assert_send_sync::<AppState<TestConfig>>();
282    }
283
284    #[test]
285    fn test_app_state_content_path() {
286        let config = test_config();
287        let state = AppState::new(config);
288
289        let concepts_path = state.config().content_path("concepts").unwrap();
290        assert_eq!(concepts_path, PathBuf::from("/tmp/test/concepts"));
291
292        let sources_path = state.config().content_path("sources").unwrap();
293        assert_eq!(sources_path, PathBuf::from("/tmp/test/sources"));
294    }
295
296    #[test]
297    fn test_app_state_arc_count() {
298        let config = test_config();
299        let state = AppState::new(config);
300
301        // Get arcs and check count
302        let arc1 = state.config_arc();
303        let arc2 = state.config_arc();
304
305        // Count: state.config + arc1 + arc2 = 3
306        assert_eq!(Arc::strong_count(&arc1), 3);
307
308        drop(arc2);
309        // Count: state.config + arc1 = 2
310        assert_eq!(Arc::strong_count(&arc1), 2);
311    }
312
313    #[tokio::test]
314    async fn test_app_state_across_tasks() {
315        let config = test_config();
316        let state = AppState::new(config);
317
318        let state_clone = state.clone();
319        let handle = tokio::spawn(async move { state_clone.project_name().to_string() });
320
321        let result = handle.await.unwrap();
322        assert_eq!(result, "test-project");
323        assert_eq!(state.project_name(), "test-project");
324    }
325}