Skip to main content

reovim_kernel/core/
config.rs

1//! Configuration mechanism for kernel.
2//!
3//! Linux equivalent: `/proc/sys/` + configuration subsystem
4//!
5//! This module provides the **MECHANISM** for configuration storage.
6//! **POLICY** (what to store, validation rules) belongs in modules.
7//!
8//! # Design
9//!
10//! - `ConfigValue`: Type-safe primitive values (bool, int, string, array, table)
11//! - `Config`: Thread-safe key-value store with flat key access
12//! - `ConfigPaths`: XDG-compliant path resolution for config/data/cache directories
13//! - `ConfigError`: Error types for config operations
14//!
15//! # TOML Parsing
16//!
17//! The kernel does NOT parse TOML directly (no serde dependency).
18//! TOML parsing is provided by modules (not the kernel).
19//! See [`archive/pre_kernel/lib/core/src/config/loader.rs`](https://github.com/ds1sqe/reovim/blob/81806439/archive/pre_kernel/lib/core/src/config/loader.rs) for the original implementation.
20//! This keeps the kernel dependency-free and policy-agnostic.
21//!
22//! # Example
23//!
24//! ```ignore
25//! use reovim_kernel::api::v1::{Config, ConfigValue, ConfigPaths};
26//!
27//! let config = Config::new();
28//!
29//! // Set values
30//! config.set_str("editor.theme", "dark");
31//! config.set_int("editor.tabwidth", 4);
32//! config.set_bool("editor.number", true);
33//!
34//! // Get values
35//! assert_eq!(config.get_str("editor.theme"), Some("dark".to_string()));
36//! assert_eq!(config.get_int("editor.tabwidth"), Some(4));
37//!
38//! // Get config paths
39//! let config_file = ConfigPaths::config_file()?;
40//! ```
41
42use std::{collections::HashMap, fmt, path::PathBuf};
43
44use reovim_arch::sync::RwLock;
45
46// ============================================================================
47// ConfigValue - Type-safe configuration values
48// ============================================================================
49
50/// Type-safe configuration value.
51///
52/// This is a kernel-level primitive for configuration storage.
53/// Higher layers (`OptionRegistry`, `ProfileManager`) add validation
54/// and metadata on top.
55#[derive(Debug, Clone, PartialEq)]
56pub enum ConfigValue {
57    /// Boolean value.
58    Bool(bool),
59    /// Integer value (i64 for flexibility).
60    Integer(i64),
61    /// String value.
62    String(String),
63    /// Array of values (homogeneous).
64    Array(Vec<Self>),
65    /// Nested table/section.
66    Table(HashMap<String, Self>),
67}
68
69impl ConfigValue {
70    /// Get the type name for error messages.
71    #[must_use]
72    pub const fn type_name(&self) -> &'static str {
73        match self {
74            Self::Bool(_) => "bool",
75            Self::Integer(_) => "integer",
76            Self::String(_) => "string",
77            Self::Array(_) => "array",
78            Self::Table(_) => "table",
79        }
80    }
81
82    /// Try to get as boolean.
83    #[must_use]
84    pub const fn as_bool(&self) -> Option<bool> {
85        match self {
86            Self::Bool(b) => Some(*b),
87            _ => None,
88        }
89    }
90
91    /// Try to get as integer.
92    #[must_use]
93    pub const fn as_int(&self) -> Option<i64> {
94        match self {
95            Self::Integer(i) => Some(*i),
96            _ => None,
97        }
98    }
99
100    /// Try to get as string reference.
101    #[must_use]
102    pub fn as_str(&self) -> Option<&str> {
103        match self {
104            Self::String(s) => Some(s),
105            _ => None,
106        }
107    }
108
109    /// Try to get as array reference.
110    #[must_use]
111    pub fn as_array(&self) -> Option<&[Self]> {
112        match self {
113            Self::Array(a) => Some(a),
114            _ => None,
115        }
116    }
117
118    /// Try to get as table reference.
119    #[must_use]
120    pub const fn as_table(&self) -> Option<&HashMap<String, Self>> {
121        match self {
122            Self::Table(t) => Some(t),
123            _ => None,
124        }
125    }
126
127    /// Try to get as mutable table reference.
128    #[must_use]
129    pub const fn as_table_mut(&mut self) -> Option<&mut HashMap<String, Self>> {
130        match self {
131            Self::Table(t) => Some(t),
132            _ => None,
133        }
134    }
135}
136
137#[cfg_attr(coverage_nightly, coverage(off))]
138impl fmt::Display for ConfigValue {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            Self::Bool(b) => write!(f, "{b}"),
142            Self::Integer(i) => write!(f, "{i}"),
143            Self::String(s) => write!(f, "{s}"),
144            Self::Array(arr) => write!(f, "[{} items]", arr.len()),
145            Self::Table(t) => write!(f, "{{{} entries}}", t.len()),
146        }
147    }
148}
149
150// Convenient From implementations
151#[cfg_attr(coverage_nightly, coverage(off))]
152impl From<bool> for ConfigValue {
153    fn from(b: bool) -> Self {
154        Self::Bool(b)
155    }
156}
157
158#[cfg_attr(coverage_nightly, coverage(off))]
159impl From<i64> for ConfigValue {
160    fn from(i: i64) -> Self {
161        Self::Integer(i)
162    }
163}
164
165#[cfg_attr(coverage_nightly, coverage(off))]
166impl From<i32> for ConfigValue {
167    fn from(i: i32) -> Self {
168        Self::Integer(i64::from(i))
169    }
170}
171
172#[cfg_attr(coverage_nightly, coverage(off))]
173impl From<String> for ConfigValue {
174    fn from(s: String) -> Self {
175        Self::String(s)
176    }
177}
178
179#[cfg_attr(coverage_nightly, coverage(off))]
180impl From<&str> for ConfigValue {
181    fn from(s: &str) -> Self {
182        Self::String(s.to_string())
183    }
184}
185
186#[cfg_attr(coverage_nightly, coverage(off))]
187impl<T: Into<Self>> From<Vec<T>> for ConfigValue {
188    fn from(v: Vec<T>) -> Self {
189        Self::Array(v.into_iter().map(Into::into).collect())
190    }
191}
192
193// ============================================================================
194// ConfigError - Error types
195// ============================================================================
196
197/// Errors that can occur during configuration operations.
198#[derive(Debug, Clone, PartialEq, Eq)]
199pub enum ConfigError {
200    /// Key not found in configuration.
201    NotFound(String),
202    /// Type mismatch when accessing value.
203    TypeMismatch {
204        /// The key that was accessed
205        key: String,
206        /// Expected type
207        expected: &'static str,
208        /// Actual type
209        got: &'static str,
210    },
211    /// Path-related error (missing home, invalid path).
212    PathError(String),
213    /// IO error (file not found, permission denied).
214    Io(String),
215    /// Parse error (invalid format).
216    Parse(String),
217    /// Serialization error.
218    Serialize(String),
219}
220
221#[cfg_attr(coverage_nightly, coverage(off))]
222impl fmt::Display for ConfigError {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        match self {
225            Self::NotFound(key) => write!(f, "config key not found: {key}"),
226            Self::TypeMismatch { key, expected, got } => {
227                write!(f, "type mismatch for '{key}': expected {expected}, got {got}")
228            }
229            Self::PathError(msg) => write!(f, "path error: {msg}"),
230            Self::Io(msg) => write!(f, "IO error: {msg}"),
231            Self::Parse(msg) => write!(f, "parse error: {msg}"),
232            Self::Serialize(msg) => write!(f, "serialize error: {msg}"),
233        }
234    }
235}
236
237impl std::error::Error for ConfigError {}
238
239// ============================================================================
240// Config - Thread-safe configuration store
241// ============================================================================
242
243/// Thread-safe configuration store.
244///
245/// Provides flat key-value storage with type-safe accessors.
246/// Keys use dot notation for logical grouping: `editor.theme`, `plugin.lsp.timeout`
247///
248/// # Thread Safety
249///
250/// All operations are thread-safe via internal `RwLock`.
251/// Multiple readers allowed, single writer for mutations.
252///
253/// # Example
254///
255/// ```ignore
256/// use reovim_kernel::api::v1::{Config, ConfigValue};
257///
258/// let config = Config::new();
259///
260/// // Set values
261/// config.set_str("editor.theme", "dark");
262/// config.set_int("editor.tabwidth", 4);
263///
264/// // Get values
265/// assert_eq!(config.get_str("editor.theme"), Some("dark".to_string()));
266/// assert_eq!(config.get_int("editor.tabwidth"), Some(4));
267/// ```
268#[derive(Debug, Default)]
269pub struct Config {
270    /// Root configuration data (flat key-value store).
271    data: RwLock<HashMap<String, ConfigValue>>,
272    /// Associated file path (if loaded from file).
273    path: RwLock<Option<PathBuf>>,
274}
275
276impl Config {
277    /// Create a new empty configuration.
278    #[must_use]
279    pub fn new() -> Self {
280        Self::default()
281    }
282
283    /// Create a configuration with an associated file path.
284    #[must_use]
285    pub fn with_path(path: PathBuf) -> Self {
286        Self {
287            data: RwLock::new(HashMap::new()),
288            path: RwLock::new(Some(path)),
289        }
290    }
291
292    /// Get the associated file path.
293    #[must_use]
294    pub fn path(&self) -> Option<PathBuf> {
295        self.path.read().clone()
296    }
297
298    /// Set the associated file path.
299    pub fn set_path(&self, path: PathBuf) {
300        *self.path.write() = Some(path);
301    }
302
303    // ========================================================================
304    // Get operations
305    // ========================================================================
306
307    /// Get a configuration value by key.
308    ///
309    /// Keys use dot notation: `editor.theme`, `plugin.lsp.timeout`
310    #[must_use]
311    pub fn get(&self, key: &str) -> Option<ConfigValue> {
312        let data = self.data.read();
313        data.get(key).cloned()
314    }
315
316    /// Get a boolean value.
317    #[must_use]
318    pub fn get_bool(&self, key: &str) -> Option<bool> {
319        self.get(key).and_then(|v| v.as_bool())
320    }
321
322    /// Get an integer value.
323    #[must_use]
324    pub fn get_int(&self, key: &str) -> Option<i64> {
325        self.get(key).and_then(|v| v.as_int())
326    }
327
328    /// Get a string value.
329    #[must_use]
330    pub fn get_str(&self, key: &str) -> Option<String> {
331        self.get(key).and_then(|v| v.as_str().map(String::from))
332    }
333
334    /// Get a value with a default fallback.
335    #[must_use]
336    pub fn get_or(&self, key: &str, default: ConfigValue) -> ConfigValue {
337        self.get(key).unwrap_or(default)
338    }
339
340    /// Get a boolean with a default.
341    #[must_use]
342    pub fn get_bool_or(&self, key: &str, default: bool) -> bool {
343        self.get_bool(key).unwrap_or(default)
344    }
345
346    /// Get an integer with a default.
347    #[must_use]
348    pub fn get_int_or(&self, key: &str, default: i64) -> i64 {
349        self.get_int(key).unwrap_or(default)
350    }
351
352    /// Get a string with a default.
353    #[must_use]
354    pub fn get_str_or(&self, key: &str, default: &str) -> String {
355        self.get_str(key).unwrap_or_else(|| default.to_string())
356    }
357
358    // ========================================================================
359    // Set operations
360    // ========================================================================
361
362    /// Set a configuration value.
363    ///
364    /// Keys use dot notation. Previous value is overwritten.
365    pub fn set(&self, key: &str, value: ConfigValue) {
366        let mut data = self.data.write();
367        data.insert(key.to_string(), value);
368    }
369
370    /// Set a boolean value.
371    pub fn set_bool(&self, key: &str, value: bool) {
372        self.set(key, ConfigValue::Bool(value));
373    }
374
375    /// Set an integer value.
376    pub fn set_int(&self, key: &str, value: i64) {
377        self.set(key, ConfigValue::Integer(value));
378    }
379
380    /// Set a string value.
381    pub fn set_str(&self, key: &str, value: impl Into<String>) {
382        self.set(key, ConfigValue::String(value.into()));
383    }
384
385    // ========================================================================
386    // Bulk operations
387    // ========================================================================
388
389    /// Remove a configuration key.
390    ///
391    /// Returns the removed value if it existed.
392    pub fn remove(&self, key: &str) -> Option<ConfigValue> {
393        let mut data = self.data.write();
394        data.remove(key)
395    }
396
397    /// Check if a key exists.
398    #[must_use]
399    pub fn contains(&self, key: &str) -> bool {
400        let data = self.data.read();
401        data.contains_key(key)
402    }
403
404    /// Get all keys.
405    #[must_use]
406    pub fn keys(&self) -> Vec<String> {
407        let data = self.data.read();
408        data.keys().cloned().collect()
409    }
410
411    /// Get all keys matching a prefix.
412    #[must_use]
413    pub fn keys_with_prefix(&self, prefix: &str) -> Vec<String> {
414        let data = self.data.read();
415        data.keys()
416            .filter(|k| k.starts_with(prefix))
417            .cloned()
418            .collect()
419    }
420
421    /// Clear all configuration data.
422    pub fn clear(&self) {
423        let mut data = self.data.write();
424        data.clear();
425    }
426
427    /// Get the number of entries.
428    #[must_use]
429    pub fn len(&self) -> usize {
430        let data = self.data.read();
431        data.len()
432    }
433
434    /// Check if empty.
435    #[must_use]
436    pub fn is_empty(&self) -> bool {
437        let data = self.data.read();
438        data.is_empty()
439    }
440
441    /// Merge another config into this one.
442    ///
443    /// Values from `other` overwrite values in `self`.
444    pub fn merge(&self, other: &Self) {
445        let other_data = other.data.read();
446        let mut self_data = self.data.write();
447
448        for (key, value) in other_data.iter() {
449            self_data.insert(key.clone(), value.clone());
450        }
451    }
452
453    /// Export all data as a `HashMap`.
454    #[must_use]
455    pub fn to_map(&self) -> HashMap<String, ConfigValue> {
456        let data = self.data.read();
457        data.clone()
458    }
459
460    /// Import data from a `HashMap`.
461    pub fn from_map(&self, map: HashMap<String, ConfigValue>) {
462        let mut data = self.data.write();
463        *data = map;
464    }
465}
466
467// ============================================================================
468// ConfigPaths - XDG-compliant path resolution
469// ============================================================================
470
471/// Configuration path utilities.
472///
473/// Uses `reovim-arch::dirs` for platform-agnostic path resolution.
474/// Follows XDG Base Directory Specification on Unix.
475///
476/// # Environment Variable Overrides
477///
478/// All paths can be overridden via environment variables for worktree
479/// isolation and testing:
480///
481/// | Variable | Overrides | Purpose |
482/// |----------|-----------|---------|
483/// | `REOVIM_CONFIG_DIR` | `config_dir()` | User config (modules.toml, profiles) |
484/// | `REOVIM_DATA_DIR` | `data_dir()` | Runtime data (.so modules, lock files, logs) |
485/// | `REOVIM_CACHE_DIR` | `cache_dir()` | Cache (derived from data if unset) |
486///
487/// Resolution order: `$REOVIM_*_DIR` > XDG/platform default.
488pub struct ConfigPaths;
489
490impl ConfigPaths {
491    /// Get the reovim config directory.
492    ///
493    /// Resolution: `$REOVIM_CONFIG_DIR` > `$XDG_CONFIG_HOME/reovim` > `~/.config/reovim`
494    ///
495    /// # Errors
496    ///
497    /// Returns `ConfigError::PathError` if the config directory cannot be determined
498    /// and no env override is set.
499    pub fn config_dir() -> Result<PathBuf, ConfigError> {
500        Self::resolve_dir(
501            std::env::var("REOVIM_CONFIG_DIR").ok(),
502            reovim_arch::dirs::config_dir(),
503            "config",
504        )
505    }
506
507    /// Get the reovim data directory.
508    ///
509    /// Resolution: `$REOVIM_DATA_DIR` > `$XDG_DATA_HOME/reovim` > `~/.local/share/reovim`
510    ///
511    /// # Errors
512    ///
513    /// Returns `ConfigError::PathError` if the data directory cannot be determined
514    /// and no env override is set.
515    pub fn data_dir() -> Result<PathBuf, ConfigError> {
516        Self::resolve_dir(
517            std::env::var("REOVIM_DATA_DIR").ok(),
518            reovim_arch::dirs::data_local_dir(),
519            "data",
520        )
521    }
522
523    /// Get the reovim cache directory.
524    ///
525    /// Resolution: `$REOVIM_CACHE_DIR` > `$XDG_CACHE_HOME/reovim` > `~/.cache/reovim`
526    ///
527    /// # Errors
528    ///
529    /// Returns `ConfigError::PathError` if the cache directory cannot be determined
530    /// and no env override is set.
531    pub fn cache_dir() -> Result<PathBuf, ConfigError> {
532        Self::resolve_dir(
533            std::env::var("REOVIM_CACHE_DIR").ok(),
534            reovim_arch::dirs::cache_dir(),
535            "cache",
536        )
537    }
538
539    /// Resolve a directory path from env override or platform default.
540    ///
541    /// This is the pure testable core of the path resolution logic.
542    /// The env override takes priority; if absent, the platform default
543    /// is used with `/reovim` appended.
544    fn resolve_dir(
545        env_override: Option<String>,
546        platform_default: Option<PathBuf>,
547        kind: &str,
548    ) -> Result<PathBuf, ConfigError> {
549        if let Some(dir) = env_override {
550            return Ok(PathBuf::from(dir));
551        }
552        platform_default
553            .map(|p| p.join("reovim"))
554            .ok_or_else(|| ConfigError::PathError(format!("cannot determine {kind} directory")))
555    }
556
557    /// Get the path to the main config file.
558    ///
559    /// Returns `{config_dir}/config.toml`
560    ///
561    /// # Errors
562    ///
563    /// Returns `ConfigError::PathError` if the config directory cannot be determined.
564    pub fn config_file() -> Result<PathBuf, ConfigError> {
565        Self::config_dir().map(|p| p.join("config.toml"))
566    }
567
568    /// Get the profiles directory.
569    ///
570    /// Returns `{config_dir}/profiles/`
571    ///
572    /// # Errors
573    ///
574    /// Returns `ConfigError::PathError` if the config directory cannot be determined.
575    pub fn profiles_dir() -> Result<PathBuf, ConfigError> {
576        Self::config_dir().map(|p| p.join("profiles"))
577    }
578}
579
580#[cfg(test)]
581#[path = "config_tests.rs"]
582mod tests;