dog_core/
config.rs

1//! # DogRS Configuration
2//!
3//! DogRS includes a minimal, framework-agnostic configuration
4//! system based on a simple string key/value store. This mirrors
5//! Feathers' `app.set()` / `app.get()` API and allows applications
6//! to layer configuration however they like.
7//!
8//! ## Setting and reading values
9//! ```rust
10//! use dog_core::DogApp;
11//! let mut app = DogApp::<(), ()>::new();
12//!
13//! app.set("paginate.default", "10");
14//! app.set("paginate.max", "50");
15//!
16//! assert_eq!(app.get("paginate.default"), Some("10".to_string()));
17//! ```
18//!
19//! ## Environment overrides
20//! DogRS core is intentionally environment-agnostic. Applications
21//! may choose to load environment variables using any convention.
22//!
23//! Here is a recommended helper:
24//!
25//! ```rust
26//! use dog_core::DogApp;
27//! pub fn load_env_config<R, P>(app: &mut DogApp<R, P>, prefix: &str)
28//! where
29//!     R: Send + 'static,
30//!     P: Send + Clone + 'static,
31//! {
32//!     for (key, value) in std::env::vars() {
33//!         if let Some(stripped) = key.strip_prefix(prefix) {
34//!             let normalized = stripped
35//!                 .to_lowercase()
36//!                 .replace("__", "."); // ADSDOG__PAGINATE__DEFAULT → paginate.default
37//!
38//!             app.set(normalized, value);
39//!         }
40//!     }
41//! }
42//! ```
43//!
44//! Applications can now override configuration using:
45//!
46//! ```bash
47//! export ADSDOG__PAGINATE__DEFAULT=25
48//! ```
49//!
50//! ## Why this design?
51//! - Works in any environment (cloud, edge, P2P, serverless)
52//! - No dependency on TOML/JSON/YAML formats
53//! - Zero stack lock-in
54//! - Multi-tenant friendly
55//! - Mirrors Feathers’ configuration style in a Rust-friendly way
56//!
57//! Higher-level loaders (TOML, JSON, Consul, Vault, etc.) are
58//! intentionally kept *out* of DogRS so each application remains
59//! free to choose its configuration strategy.
60
61use std::collections::HashMap;
62
63#[derive(Debug, Default)]
64pub struct DogConfig {
65    values: HashMap<String, String>,
66}
67
68impl DogConfig {
69    /// Create an empty config store.
70    pub fn new() -> Self {
71        Self {
72            values: HashMap::new(),
73        }
74    }
75
76    /// Set a configuration key to a string value.
77    ///
78    /// Example: app.set("paginate.default", "10")
79    pub fn set<K, V>(&mut self, key: K, value: V)
80    where
81        K: Into<String>,
82        V: Into<String>,
83    {
84        self.values.insert(key.into(), value.into());
85    }
86
87    /// Get a configuration value by key.
88    ///
89    /// Returns None if the key is not present.
90    pub fn get(&self, key: &str) -> Option<&str> {
91        self.values.get(key).map(|s| s.as_str())
92    }
93
94    /// Check whether a key is present.
95    pub fn has(&self, key: &str) -> bool {
96        self.values.contains_key(key)
97    }
98    pub fn snapshot(&self) -> DogConfigSnapshot {
99        DogConfigSnapshot::new(self.values.clone())
100    }
101}
102
103#[derive(Debug, Clone, Default)]
104pub struct DogConfigSnapshot {
105    map: HashMap<String, String>,
106}
107
108impl DogConfigSnapshot {
109    pub(crate) fn new(map: HashMap<String, String>) -> Self {
110        Self { map }
111    }
112
113    pub fn get(&self, key: &str) -> Option<&str> {
114        self.map.get(key).map(|s| s.as_str())
115    }
116
117    pub fn get_string(&self, key: &str) -> Option<String> {
118        self.map.get(key).cloned()
119    }
120
121    pub fn get_usize(&self, key: &str) -> Option<usize> {
122        self.get(key).and_then(|v| v.parse::<usize>().ok())
123    }
124
125    pub fn get_bool(&self, key: &str) -> Option<bool> {
126        self.get(key).and_then(|v| v.parse::<bool>().ok())
127    }
128}