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