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;