mcpr_core/config.rs
1//! Configuration trait and validation types for mcpr modules.
2//!
3//! Each mcpr module (store, cloud, tunnel, logging, etc.) implements [`ModuleConfig`]
4//! on its TOML config section struct. This lets every module own its own defaults,
5//! validation logic, and documentation — while the CLI orchestrates validation by
6//! iterating over all registered modules.
7//!
8//! # Design rationale
9//!
10//! Without this trait, all config validation lives in one monolithic function in
11//! `mcpr-cli/src/config.rs`, and every new module must touch that file. With it,
12//! each crate validates itself — the CLI just loops over `&[&dyn ModuleConfig]`.
13
14use std::fmt;
15
16// ── Severity ───────────────────────────────────────────────────────────
17
18/// How serious a configuration issue is.
19///
20/// - `Error`: the proxy cannot start with this config (e.g., invalid URL, missing required field).
21/// - `Warn`: the proxy can start, but behavior may be surprising (e.g., port 0 binds randomly).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum Severity {
24 Error,
25 Warn,
26}
27
28impl fmt::Display for Severity {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 match self {
31 Severity::Error => write!(f, "error"),
32 Severity::Warn => write!(f, "warn"),
33 }
34 }
35}
36
37// ── ConfigIssue ────────────────────────────────────────────────────────
38
39/// A single validation issue found in a module's configuration.
40///
41/// Returned by [`ModuleConfig::validate`]. The CLI collects these from all
42/// modules and presents them to the user (in `mcpr validate` or on startup).
43#[derive(Debug, Clone)]
44pub struct ConfigIssue {
45 /// Whether this issue prevents startup or is just a warning.
46 pub severity: Severity,
47
48 /// Which module reported the issue (e.g., "store", "cloud", "tunnel").
49 /// Matches the TOML section name so the user knows where to look.
50 pub module: &'static str,
51
52 /// Human-readable description of what's wrong and how to fix it.
53 pub message: String,
54}
55
56impl fmt::Display for ConfigIssue {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 write!(f, "[{}] {}: {}", self.module, self.severity, self.message)
59 }
60}
61
62// ── ModuleConfig trait ─────────────────────────────────────────────────
63
64/// Trait for module-owned configuration sections.
65///
66/// Each mcpr module implements this on its file config struct (the struct
67/// that maps to a `[section]` in `mcpr.toml`). This gives each module
68/// ownership over:
69///
70/// - **Naming**: what TOML section key it lives under.
71/// - **Defaults**: runtime-aware defaults (e.g., platform-specific paths).
72/// - **Validation**: checking its own fields without the CLI knowing the rules.
73///
74/// # Example
75///
76/// ```rust,ignore
77/// use mcpr_core::config::{ModuleConfig, ConfigIssue, Severity};
78///
79/// #[derive(serde::Deserialize, Default)]
80/// pub struct FileStoreConfig {
81/// pub path: Option<String>,
82/// }
83///
84/// impl ModuleConfig for FileStoreConfig {
85/// fn name(&self) -> &'static str { "store" }
86///
87/// fn validate(&self) -> Vec<ConfigIssue> {
88/// let mut issues = vec![];
89/// if let Some(ref p) = self.path {
90/// if p.is_empty() {
91/// issues.push(ConfigIssue {
92/// severity: Severity::Error,
93/// module: "store",
94/// message: "store.path cannot be empty string".into(),
95/// });
96/// }
97/// }
98/// issues
99/// }
100/// }
101/// ```
102///
103/// # CLI integration
104///
105/// The CLI collects all `&dyn ModuleConfig` and calls `validate()` on each:
106///
107/// ```rust,ignore
108/// let modules: Vec<&dyn ModuleConfig> = vec![&config.store, &config.cloud, ...];
109/// let issues: Vec<ConfigIssue> = modules.iter().flat_map(|m| m.validate()).collect();
110/// ```
111pub trait ModuleConfig {
112 /// Module name — must match the TOML section key (e.g., "store", "cloud", "tunnel").
113 ///
114 /// Used in error messages and logging so the user knows which section
115 /// of `mcpr.toml` to fix.
116 fn name(&self) -> &'static str;
117
118 /// Validate this module's configuration.
119 ///
120 /// Returns an empty vec if everything is valid. Each issue carries its own
121 /// severity — the CLI decides whether to abort (on Error) or continue (on Warn).
122 ///
123 /// Implementations should validate field values, required combinations,
124 /// and cross-field constraints within this module. Cross-module validation
125 /// (e.g., "relay mode requires port") stays in the CLI.
126 fn validate(&self) -> Vec<ConfigIssue>;
127
128 /// Apply runtime-aware defaults that can't be expressed in `Default::default()`.
129 ///
130 /// Called after TOML deserialization, before validation. Use this for defaults
131 /// that depend on the platform, environment variables, or other runtime state
132 /// (e.g., platform-specific DB paths, env-based feature flags).
133 ///
134 /// The default implementation is a no-op.
135 fn apply_defaults(&mut self) {}
136}