Skip to main content

systemprompt_traits/
domain_config.rs

1//! Domain configuration trait for startup validation.
2//!
3//! All domain crates MUST implement this trait to participate in
4//! startup validation. This ensures consistent config loading and
5//! validation across the entire system.
6
7use std::fmt::Debug;
8
9use crate::context::ConfigProvider;
10use crate::validation_report::ValidationReport;
11
12/// Error type for domain config operations.
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum DomainConfigError {
16    #[error("Failed to load config: {0}")]
17    LoadError(String),
18
19    #[error("Config file not found: {0}")]
20    NotFound(String),
21
22    #[error("Failed to parse config: {0}")]
23    ParseError(String),
24
25    #[error("Validation failed: {0}")]
26    ValidationError(String),
27
28    #[error(transparent)]
29    Other(#[from] anyhow::Error),
30}
31
32/// Trait for domain configuration validation.
33///
34/// # Implementation Requirements
35///
36/// Every domain crate MUST implement this trait. The implementation should:
37///
38/// 1. **`domain_id()`**: Return a unique identifier for the domain
39/// 2. **`load()`**: Load and parse config files, storing state internally
40/// 3. **`validate()`**: Run semantic validation on loaded config
41/// 4. **`dependencies()`**: Declare any domains that must load first
42///
43/// # Example
44///
45/// ```rust,ignore
46/// use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError, ValidationReport};
47/// use systemprompt_traits::validation_report::ValidationError;
48///
49/// pub struct ContentConfigValidator {
50///     config: Option<ContentConfig>,
51/// }
52///
53/// impl DomainConfig for ContentConfigValidator {
54///     fn domain_id(&self) -> &'static str {
55///         "content"
56///     }
57///
58///     fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
59///         let path = config.system_path(); // Use ConfigProvider methods
60///         let content = std::fs::read_to_string(path)
61///             .map_err(|e| DomainConfigError::LoadError(e.to_string()))?;
62///
63///         let parsed: ContentConfig = serde_yaml::from_str(&content)
64///             .map_err(|e| DomainConfigError::ParseError(e.to_string()))?;
65///
66///         self.config = Some(parsed);
67///         Ok(())
68///     }
69///
70///     fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
71///         let config = self.config.as_ref()
72///             .ok_or_else(|| DomainConfigError::ValidationError("Not loaded".into()))?;
73///
74///         let mut report = ValidationReport::new("content");
75///
76///         // Semantic validation
77///         for source in &config.sources {
78///             if !std::path::Path::new(&source.path).exists() {
79///                 report.add_error(ValidationError::new(
80///                     format!("sources.{}", source.name),
81///                     "Source directory does not exist",
82///                 ).with_path(&source.path));
83///             }
84///         }
85///
86///         Ok(report)
87///     }
88/// }
89/// ```
90pub trait DomainConfig: Send + Sync + Debug {
91    /// Unique identifier for this domain.
92    ///
93    /// Used in validation reports and error messages.
94    /// Examples: "web", "content", "agents", "mcp"
95    fn domain_id(&self) -> &'static str;
96
97    /// Load configuration from the given config.
98    ///
99    /// This method should:
100    /// 1. Read the config file(s) from paths in `config`
101    /// 2. Parse the content (YAML, JSON, etc.)
102    /// 3. Store the parsed config internally
103    ///
104    /// # Errors
105    ///
106    /// Returns `DomainConfigError` if:
107    /// - Config file cannot be read
108    /// - Config file cannot be parsed
109    fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError>;
110
111    /// Validate the loaded configuration.
112    ///
113    /// This method should:
114    /// 1. Check semantic validity (not just syntax)
115    /// 2. Verify referenced resources exist
116    /// 3. Check for conflicts or inconsistencies
117    ///
118    /// Must be called after `load()`.
119    ///
120    /// # Returns
121    ///
122    /// A `ValidationReport` containing any errors or warnings.
123    fn validate(&self) -> Result<ValidationReport, DomainConfigError>;
124
125    /// Dependencies on other domains.
126    ///
127    /// Return domain IDs that must be loaded before this one.
128    /// Default: no dependencies.
129    fn dependencies(&self) -> &[&'static str] {
130        &[]
131    }
132
133    /// Priority for load order (lower = earlier).
134    ///
135    /// Default: 100. Core domains use 1-10, extensions use 100+.
136    fn priority(&self) -> u32 {
137        100
138    }
139}
140
141/// Registry of domain config validators.
142///
143/// Used by `StartupValidator` to collect and run all domain validators.
144pub struct DomainConfigRegistry {
145    validators: Vec<Box<dyn DomainConfig>>,
146}
147
148impl DomainConfigRegistry {
149    #[must_use]
150    pub fn new() -> Self {
151        Self {
152            validators: Vec::new(),
153        }
154    }
155
156    /// Register a domain validator.
157    pub fn register(&mut self, validator: Box<dyn DomainConfig>) {
158        self.validators.push(validator);
159    }
160
161    /// Get validators sorted by priority and dependencies.
162    pub fn validators_sorted(&self) -> Vec<&dyn DomainConfig> {
163        let mut validators: Vec<_> = self.validators.iter().map(AsRef::as_ref).collect();
164        validators.sort_by_key(|v| v.priority());
165        validators
166    }
167
168    /// Get mutable validators for loading.
169    pub fn validators_mut(&mut self) -> impl Iterator<Item = &mut Box<dyn DomainConfig>> {
170        // Sort by priority first
171        self.validators.sort_by_key(|v| v.priority());
172        self.validators.iter_mut()
173    }
174}
175
176impl Default for DomainConfigRegistry {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl Debug for DomainConfigRegistry {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("DomainConfigRegistry")
185            .field("validator_count", &self.validators.len())
186            .finish()
187    }
188}
189
190// Tests are in crates/shared/traits-tests/ per architecture policy