Skip to main content

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}