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}