Skip to main content

trait_kit/kit/
kit.rs

1// Copyright (c) 2026 Kirky.X
2// SPDX-License-Identifier: MIT
3//! Kit — the capability and configuration management center.
4//!
5//! Uses typestate pattern: `Kit` (unbuilt) → `Kit<Ready>` (after `build()`).
6
7use std::any::{Any, TypeId};
8use std::cell::RefCell;
9use std::collections::HashMap;
10#[cfg(feature = "hot-reload")]
11use std::rc::Rc;
12
13use crate::core::{AutoBuilder, BuildFn};
14use crate::error::TraitKitError;
15
16#[cfg(feature = "encryption")]
17use super::EncryptedBlob;
18use super::TypeMap;
19use super::{DependencyGraph, GraphError, ModuleEntry};
20
21/// HKDF key-derivation version label bound into every per-field key.
22/// Bumping this rotates all encrypted configs without changing master keys.
23#[cfg(feature = "encryption")]
24const KEY_DERIVATION_VERSION: &str = "v1";
25
26/// Derive a per-field encryption key, mapping HKDF failures to `TraitKitError`.
27#[cfg(feature = "encryption")]
28fn derive_kit_field_key(
29    master_key: &[u8],
30    path: &'static str,
31    context: &'static str,
32) -> Result<[u8; 32], TraitKitError> {
33    super::config::derive_field_key(master_key, path, KEY_DERIVATION_VERSION).map_err(|e| {
34        TraitKitError::BuildFailed {
35            context,
36            source: Box::new(e),
37        }
38    })
39}
40
41/// Marker type for the unbuilt state.
42pub struct Unbuilt;
43
44/// Marker type for the ready (built) state.
45pub struct Ready;
46
47/// Type alias for hot-reload subscriber callbacks (single-threaded, `!Sync`).
48#[cfg(feature = "hot-reload")]
49type SubscriberMap = RefCell<HashMap<TypeId, Vec<Rc<dyn Fn()>>>>;
50
51/// Type alias for the encrypted config store (single-threaded, `!Sync`).
52#[cfg(feature = "encryption")]
53type EncryptedConfigMap = RefCell<HashMap<TypeId, EncryptedBlob>>;
54
55/// The capability and configuration management center.
56pub struct Kit<S = Unbuilt> {
57    builders: RefCell<HashMap<TypeId, BuildFn>>,
58    graph: DependencyGraph,
59    configs: TypeMap,
60    capabilities: TypeMap,
61    #[cfg(feature = "hot-reload")]
62    subscribers: SubscriberMap,
63    #[cfg(feature = "encryption")]
64    encrypted_configs: EncryptedConfigMap,
65    _state: std::marker::PhantomData<S>,
66}
67
68impl Kit {
69    /// Create a new empty Kit.
70    #[must_use]
71    pub fn new() -> Self {
72        Kit {
73            builders: RefCell::new(HashMap::new()),
74            graph: DependencyGraph::new(),
75            configs: TypeMap::new(),
76            capabilities: TypeMap::new(),
77            #[cfg(feature = "hot-reload")]
78            subscribers: RefCell::new(HashMap::new()),
79            #[cfg(feature = "encryption")]
80            encrypted_configs: RefCell::new(HashMap::new()),
81            _state: std::marker::PhantomData,
82        }
83    }
84
85    /// Register a module for construction.
86    ///
87    /// # Errors
88    ///
89    /// Returns `TraitKitError::AlreadyRegistered` if a module with the same `TypeId` was already registered.
90    pub fn register<M: AutoBuilder>(&mut self) -> Result<(), TraitKitError> {
91        let entry = ModuleEntry {
92            type_id: TypeId::of::<M>(),
93            name: M::NAME,
94            dependencies: M::dependencies().iter().map(|(n, id)| (*n, *id)).collect(),
95        };
96
97        self.graph
98            .add(entry)
99            .map_err(|name| TraitKitError::AlreadyRegistered { module: name })?;
100
101        let build_fn: BuildFn = Box::new(|kit| {
102            let capability = M::build(kit)
103                .map_err(|e| -> Box<dyn std::error::Error + Send + 'static> { Box::new(e) })?;
104            Ok(Box::new(capability) as Box<dyn Any>)
105        });
106
107        self.builders
108            .borrow_mut()
109            .insert(TypeId::of::<M>(), build_fn);
110        Ok(())
111    }
112
113    /// Set a configuration value.
114    pub fn set_config<C: Clone + 'static>(&self, config: C) {
115        self.configs.insert(config);
116    }
117
118    /// Load a configuration via its [`Configurable`] implementation and store it.
119    ///
120    /// Requires the `confers` feature. The type must implement `Configurable`,
121    /// typically by delegating to `confers::Config`'s derived `load_sync()`.
122    /// The loaded value overrides any prior `set_config` of the same type.
123    ///
124    /// # Errors
125    ///
126    /// Returns `TraitKitError::BuildFailed` if `Configurable::load` fails.
127    #[cfg(feature = "confers")]
128    pub fn load_config<C: super::Configurable>(&self) -> Result<(), TraitKitError> {
129        let config = C::load().map_err(|e| TraitKitError::BuildFailed {
130            context: "load_config",
131            source: e,
132        })?;
133        self.set_config(config);
134        Ok(())
135    }
136
137    /// Validate the dependency graph and build all modules in topological order.
138    ///
139    /// After this call, all capabilities are available via `require()`.
140    ///
141    /// # Errors
142    ///
143    /// Returns `TraitKitError::DependencyMissing` if a registered module depends on an unregistered module.
144    /// Returns `TraitKitError::CycleDetected` if a dependency cycle is found.
145    /// Returns `TraitKitError::MissingCapability` if a build function is missing for a sorted module.
146    /// Returns `TraitKitError::BuildFailed` if a module's `build` callback returns an error.
147    pub fn build(self) -> Result<Kit<Ready>, TraitKitError> {
148        let sorted = match self.graph.validate() {
149            Ok(sorted) => sorted,
150            Err(GraphError::DependencyMissing { module, missing }) => {
151                return Err(TraitKitError::DependencyMissing { module, missing });
152            }
153            Err(GraphError::CycleDetected { cycle }) => {
154                return Err(TraitKitError::CycleDetected { cycle });
155            }
156        };
157
158        {
159            let kit_ref: &Self = &self;
160
161            for type_id in &sorted {
162                let build_fn = kit_ref.builders.borrow_mut().remove(type_id).ok_or(
163                    TraitKitError::MissingCapability {
164                        key: kit_ref.module_name(*type_id),
165                    },
166                )?;
167
168                let module_name = kit_ref.module_name(*type_id);
169
170                let result = (build_fn)(kit_ref);
171                match result {
172                    Ok(boxed) => {
173                        kit_ref.capabilities.insert_boxed(*type_id, boxed);
174                    }
175                    Err(e) => {
176                        return Err(TraitKitError::BuildFailed {
177                            context: module_name,
178                            source: e,
179                        });
180                    }
181                }
182            }
183        }
184
185        Ok(Kit {
186            builders: self.builders,
187            graph: self.graph,
188            configs: self.configs,
189            capabilities: self.capabilities,
190            #[cfg(feature = "hot-reload")]
191            subscribers: self.subscribers,
192            #[cfg(feature = "encryption")]
193            encrypted_configs: self.encrypted_configs,
194            _state: std::marker::PhantomData,
195        })
196    }
197
198    fn module_name(&self, type_id: TypeId) -> &'static str {
199        self.graph.name_of(type_id).unwrap_or("<unknown>")
200    }
201}
202
203impl<S> Kit<S> {
204    /// Retrieve a capability by its module type.
205    ///
206    /// Available on both `Kit<Unbuilt>` (inside `AutoBuilder::build` callbacks)
207    /// and `Kit<Ready>` (after `build()` completes).
208    ///
209    /// # Errors
210    ///
211    /// Returns `TraitKitError::MissingCapability` if the module has not been built yet.
212    pub fn require<M: AutoBuilder>(&self) -> Result<M::Capability, TraitKitError> {
213        let type_id = TypeId::of::<M>();
214        self.capabilities
215            .get_cloned_by_type_id::<M::Capability>(type_id)
216            .ok_or(TraitKitError::MissingCapability { key: M::NAME })
217    }
218
219    /// Get a configuration value.
220    ///
221    /// # Errors
222    ///
223    /// Returns `TraitKitError::MissingConfig` if no value of type `C` was set.
224    pub fn config<C: Clone + 'static>(&self) -> Result<C, TraitKitError> {
225        self.configs
226            .get_cloned::<C>()
227            .ok_or(TraitKitError::MissingConfig {
228                key: std::any::type_name::<C>(),
229            })
230    }
231
232    /// Subscribe a callback to be invoked when config of type `C` is reloaded.
233    ///
234    /// Requires the `hot-reload` feature. The callback receives no
235    /// arguments; use `Kit::config::<C>()` inside it to read the new value.
236    /// Callbacks are stored in a `RefCell` (single-threaded, `!Sync`).
237    ///
238    /// Layer 2 of the inheritance system: cargo feature chain
239    /// `hot-reload` → `confers-macros` → `confers`.
240    #[cfg(feature = "hot-reload")]
241    pub fn subscribe<C: 'static>(&self, callback: impl Fn() + 'static) {
242        let callback: Rc<dyn Fn()> = Rc::new(callback);
243        self.subscribers
244            .borrow_mut()
245            .entry(TypeId::of::<C>())
246            .or_default()
247            .push(callback);
248    }
249
250    /// Reload a configuration via its [`Configurable`] implementation and
251    /// notify all subscribers of type `C`.
252    ///
253    /// Requires the `hot-reload` feature. Calls `C::load()`, stores
254    /// the result via `set_config`, then invokes every `subscribe::<C>`
255    /// callback. Errors from `load()` are mapped to `TraitKitError::BuildFailed`.
256    ///
257    /// # Panics
258    ///
259    /// The new config is stored *before* invoking callbacks. If a callback
260    /// panics, the config has already been updated but remaining subscribers
261    /// in the chain are skipped (panic unwinds through `reload_config`).
262    /// Use `std::panic::catch_unwind` inside callbacks if you need to
263    /// guarantee notification of all subscribers.
264    ///
265    /// # Errors
266    ///
267    /// Returns `TraitKitError::BuildFailed` if `Configurable::load` fails.
268    #[cfg(feature = "hot-reload")]
269    pub fn reload_config<C: super::Configurable>(&self) -> Result<(), TraitKitError> {
270        let config = C::load().map_err(|e| TraitKitError::BuildFailed {
271            context: "reload_config",
272            source: e,
273        })?;
274        self.configs.insert(config);
275        // Clone the Rc list out to avoid holding the RefCell borrow across
276        // user callbacks (which may re-enter subscribe).
277        let callbacks: Vec<Rc<dyn Fn()>> = self
278            .subscribers
279            .borrow()
280            .get(&TypeId::of::<C>())
281            .cloned()
282            .unwrap_or_default();
283        for cb in &callbacks {
284            cb();
285        }
286        Ok(())
287    }
288}
289
290impl Kit {
291    /// Encrypt and store a configuration value.
292    ///
293    /// Requires the `encryption` feature. Serializes `value` to JSON,
294    /// derives a per-field key from `master_key` and `C::PATH` via HKDF, then
295    /// encrypts with XChaCha20-Poly1305. The resulting nonce + ciphertext is
296    /// stored in `encrypted_configs`, separate from the plaintext `TypeMap`.
297    ///
298    /// Layer 3 of the inheritance system: the encryption key is bound to
299    /// `ModuleConfig::PATH`, so the same master key produces different field
300    /// keys for different modules.
301    ///
302    /// # Errors
303    ///
304    /// Returns `TraitKitError::BuildFailed` if serialization, key derivation, or
305    /// encryption fails.
306    #[cfg(feature = "encryption")]
307    pub fn set_encrypted<C>(&self, value: &C, master_key: &[u8]) -> Result<(), TraitKitError>
308    where
309        C: super::ModuleConfig + serde::Serialize,
310    {
311        use super::XChaCha20Crypto;
312
313        let plaintext = serde_json::to_vec(value).map_err(|e| TraitKitError::BuildFailed {
314            context: "set_encrypted",
315            source: Box::new(e),
316        })?;
317
318        let field_key = derive_kit_field_key(master_key, C::PATH, "set_encrypted")?;
319
320        let (nonce, ciphertext) = XChaCha20Crypto::new()
321            .encrypt(&plaintext, &field_key)
322            .map_err(|e| TraitKitError::BuildFailed {
323                context: "set_encrypted",
324                source: Box::new(e),
325            })?;
326
327        self.encrypted_configs
328            .borrow_mut()
329            .insert(TypeId::of::<C>(), EncryptedBlob { nonce, ciphertext });
330        Ok(())
331    }
332
333    /// Check if an encrypted config of type `C` is registered.
334    #[cfg(feature = "encryption")]
335    pub fn contains_encrypted<C: super::ModuleConfig>(&self) -> bool {
336        self.encrypted_configs
337            .borrow()
338            .contains_key(&TypeId::of::<C>())
339    }
340
341    /// Load a configuration via `Configurable::load`, falling back to
342    /// `ModuleConfig::default_value` if loading fails.
343    ///
344    /// Requires the `confers-macros` feature. Stores the resulting value
345    /// via `set_config`, overriding any prior value of the same type.
346    ///
347    /// # Errors
348    ///
349    /// Never returns an error: load failures are silently replaced by the
350    /// module's declared default. Inspect the stored value via `config::<C>()`
351    /// if you need to distinguish "loaded" from "defaulted".
352    #[cfg(feature = "confers-macros")]
353    pub fn load_config_or_default<C>(&self) -> Result<(), TraitKitError>
354    where
355        C: super::Configurable + super::ModuleConfig,
356    {
357        let config = match C::load() {
358            Ok(value) => value,
359            Err(_) => C::default_value(),
360        };
361        self.set_config(config);
362        Ok(())
363    }
364}
365
366impl Kit<Ready> {
367    /// Retrieve an optional capability. Returns `None` if not built.
368    pub fn optional<M: AutoBuilder>(&self) -> Option<M::Capability> {
369        let type_id = TypeId::of::<M>();
370        self.capabilities
371            .get_cloned_by_type_id::<M::Capability>(type_id)
372    }
373
374    /// Check if a capability has been built.
375    pub fn contains<M: AutoBuilder>(&self) -> bool {
376        self.capabilities.contains_by_type_id(TypeId::of::<M>())
377    }
378
379    /// Check if a config is registered.
380    pub fn contains_config<C: Clone + 'static>(&self) -> bool {
381        self.configs.contains::<C>()
382    }
383
384    /// Retrieve and decrypt a configuration value.
385    ///
386    /// Requires the `encryption` feature. Looks up the encrypted
387    /// blob for type `C`, derives the per-field key from `master_key` and
388    /// `C::PATH`, decrypts with XChaCha20-Poly1305, then deserializes from
389    /// JSON. The `master_key` must match the one passed to `set_encrypted`.
390    ///
391    /// # Errors
392    ///
393    /// Returns `TraitKitError::MissingConfig` if no encrypted blob for `C` exists.
394    /// Returns `TraitKitError::BuildFailed` if key derivation, decryption, or
395    /// deserialization fails (e.g. wrong master key, tampered ciphertext).
396    #[cfg(feature = "encryption")]
397    pub fn get_encrypted<C>(&self, master_key: &[u8]) -> Result<C, TraitKitError>
398    where
399        C: super::ModuleConfig + serde::de::DeserializeOwned,
400    {
401        use super::XChaCha20Crypto;
402
403        let blob = self
404            .encrypted_configs
405            .borrow()
406            .get(&TypeId::of::<C>())
407            .cloned()
408            .ok_or(TraitKitError::MissingConfig {
409                key: std::any::type_name::<C>(),
410            })?;
411
412        let field_key = derive_kit_field_key(master_key, C::PATH, "get_encrypted")?;
413
414        let plaintext = XChaCha20Crypto::new()
415            .decrypt(&blob.nonce, &blob.ciphertext, &field_key)
416            .map_err(|e| TraitKitError::BuildFailed {
417                context: "get_encrypted",
418                source: Box::new(e),
419            })?;
420
421        serde_json::from_slice(&plaintext).map_err(|e| TraitKitError::BuildFailed {
422            context: "get_encrypted",
423            source: Box::new(e),
424        })
425    }
426}
427
428impl Default for Kit {
429    fn default() -> Self {
430        Self::new()
431    }
432}
433
434impl std::fmt::Debug for Kit {
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        f.debug_struct("Kit<Unbuilt>")
437            .field("modules", &self.graph.entries().len())
438            .field("configs", &self.configs.len())
439            .finish()
440    }
441}
442
443impl std::fmt::Debug for Kit<Ready> {
444    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445        f.debug_struct("Kit<Ready>")
446            .field("modules", &self.graph.entries().len())
447            .field("configs", &self.configs.len())
448            .finish()
449    }
450}