1use std::collections::HashMap;
2use std::env;
3
4#[derive(Debug, Default)]
5pub struct Config {
6 values: HashMap<String, String>,
7}
8
9impl Config {
10 pub fn new() -> Self {
11 Self::default()
12 }
13
14 pub fn from_env(prefix: &str) -> Self {
15 let mut values = HashMap::new();
16 let prefix_upper = format!("{}_", prefix.to_uppercase());
17 for (key, value) in env::vars() {
18 if let Some(stripped) = key.strip_prefix(&prefix_upper) {
19 values.insert(stripped.to_lowercase(), value);
20 }
21 }
22 Self { values }
23 }
24
25 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
26 self.values.insert(key.into(), value.into());
27 }
28
29 pub fn get(&self, key: &str) -> Option<&str> {
30 self.values.get(key).map(|s| s.as_str())
31 }
32
33 pub fn get_or(&self, key: &str, default: &str) -> String {
34 self.get(key).unwrap_or(default).to_string()
35 }
36
37 pub fn get_u64(&self, key: &str) -> Option<u64> {
38 self.get(key).and_then(|v| v.parse().ok())
39 }
40
41 pub fn len(&self) -> usize {
42 self.values.len()
43 }
44
45 pub fn is_empty(&self) -> bool {
46 self.values.is_empty()
47 }
48
49 pub fn keys(&self) -> impl Iterator<Item = &str> {
50 self.values.keys().map(|s| s.as_str())
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use runi_test::prelude::*;
58 use runi_test::pretty_assertions::assert_eq;
59
60 #[fixture]
61 fn config() -> Config {
62 let mut c = Config::new();
63 c.set("host", "localhost");
64 c.set("port", "8080");
65 c.set("name", "runi");
66 c
67 }
68
69 #[test]
70 fn empty_config() {
71 let c = Config::new();
72 assert!(c.is_empty());
73 assert_eq!(c.get("missing"), None);
74 }
75
76 #[rstest]
77 fn get_values(config: Config) {
78 assert_eq!(config.get("host"), Some("localhost"));
79 assert_eq!(config.get("port"), Some("8080"));
80 assert_eq!(config.len(), 3);
81 }
82
83 #[rstest]
84 fn get_with_default(config: Config) {
85 assert_eq!(config.get_or("missing", "fallback"), "fallback");
86 assert_eq!(config.get_or("host", "fallback"), "localhost");
87 }
88
89 #[rstest]
90 #[case("port", Some(8080))]
91 #[case("name", None)]
92 #[case("missing", None)]
93 fn parse_u64(config: Config, #[case] key: &str, #[case] expected: Option<u64>) {
94 assert_eq!(config.get_u64(key), expected);
95 }
96
97 #[test]
98 fn from_env() {
99 unsafe {
100 env::set_var("RUNI_TEST_HOST", "localhost");
101 env::set_var("RUNI_TEST_PORT", "9090");
102 }
103 let c = Config::from_env("RUNI_TEST");
104 assert_eq!(c.get("host"), Some("localhost"));
105 assert_eq!(c.get("port"), Some("9090"));
106 unsafe {
107 env::remove_var("RUNI_TEST_HOST");
108 env::remove_var("RUNI_TEST_PORT");
109 }
110 }
111}