Skip to main content

trait_kit/kit/
kit.rs

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