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}