rmw_config/
lib.rs

1use std::{
2  collections::BTreeMap,
3  ffi::OsStr,
4  fs::{create_dir_all, File},
5  io::{BufRead, BufReader, BufWriter, LineWriter, Write},
6  path::{Path, PathBuf},
7  sync::{
8    atomic::{AtomicBool, Ordering},
9    Arc,
10  },
11  thread::{sleep, spawn},
12  time::Duration,
13};
14
15pub use const_str::replace;
16use parking_lot::Mutex;
17use rmw_str::Str;
18
19type Map = Arc<Mutex<BTreeMap<Box<[u8]>, Box<[u8]>>>>;
20
21#[derive(Debug)]
22pub struct Config {
23  map: Map,
24  run: Arc<AtomicBool>,
25  env: PathBuf,
26  prefix: Box<str>,
27}
28
29const ENV: &str = "env";
30
31fn line_key(line: &str) -> Option<(&str, usize)> {
32  let t = line.trim_start();
33  if !t.starts_with('#') {
34    if let Some(pos) = t.find('=') {
35      return Some((t[..pos].trim_end(), pos + 1));
36    }
37  }
38  None
39}
40
41fn save(path: impl AsRef<Path>, map: &mut BTreeMap<Box<[u8]>, Box<[u8]>>) {
42  if map.is_empty() {
43    return;
44  }
45
46  let mut li = vec![];
47
48  macro_rules! push {
49    ($k:expr,$v:expr) => {
50      li.push([&$k, &b" = "[..], &$v, &[b'\n'][..]].concat());
51    };
52  }
53
54  if let Ok(env) = err::ok!(File::open(&path)) {
55    for line in BufReader::new(env).lines().flatten() {
56      if let Some((k, _)) = line_key(&line) {
57        let k = k.as_bytes();
58        if let Some(v) = map.get(k) {
59          push!(k, v);
60          map.remove(k);
61          continue;
62        }
63      }
64      let mut line = line.as_bytes().to_vec();
65      line.push(b'\n');
66      li.push(line);
67    }
68    for (k, v) in map.iter() {
69      push!(k, v);
70    }
71  }
72
73  *map = BTreeMap::new();
74
75  if let Ok(file) = err::ok!(File::create(path)) {
76    let mut w = LineWriter::new(BufWriter::new(file));
77    for i in li {
78      err::log!(w.write_all(&i));
79    }
80  }
81}
82
83impl Config {
84  pub fn new(prefix: Box<str>) -> Self {
85    let root = &env_dir::home(&prefix);
86    let env = root.join(ENV);
87    if err::ok!(create_dir_all(root)).is_ok() {
88      if env.exists() {
89        if let Ok(env) = err::ok!(File::open(&env)) {
90          for line in BufReader::new(env).lines().flatten() {
91            if let Some((key, pos)) = line_key(&line) {
92              let key = format!("{}_{}", prefix, key);
93              std::env::set_var(key, line[pos..].trim());
94            }
95          }
96        }
97      } else {
98        err::log!(File::create(&env));
99      }
100    }
101
102    Self {
103      run: Arc::new(AtomicBool::new(false)),
104      map: Map::default(),
105      env,
106      prefix,
107    }
108  }
109
110  pub fn get<T: Str>(&self, key: impl AsRef<str>, init: impl Fn() -> T) -> T {
111    let key_ref = key.as_ref();
112    let key = format!("{}_{}", self.prefix, key_ref);
113
114    self._get(&key, || {
115      let r = init();
116      let mut map = self.map.lock();
117
118      let val = r.encode();
119      std::env::set_var(&key, unsafe { &std::str::from_utf8_unchecked(&val) });
120      map.insert(Box::from(key_ref.as_bytes()), val);
121
122      if !self.run.fetch_or(true, Ordering::SeqCst) {
123        let map = self.map.clone();
124        let env = self.env.clone();
125        let run = self.run.clone();
126        spawn(move || {
127          sleep(Duration::from_secs(1));
128          save(env, &mut map.lock());
129          run.store(false, Ordering::Relaxed);
130        });
131      }
132
133      r
134    })
135  }
136
137  fn _get<T: Str>(&self, key: impl AsRef<OsStr>, init: impl Fn() -> T) -> T {
138    if let Ok(bin) = std::env::var(&key) {
139      if let Ok(r) = err::ok!(T::decode(bin.as_bytes())) {
140        return r;
141      }
142    }
143
144    init()
145  }
146}
147
148#[macro_export]
149macro_rules! config {
150  ($prefix:expr) => {
151    let config = $crate::Config::new(stringify!(prefix).into());
152    $crate::macro_def!(config, get);
153  };
154}
155
156#[macro_export]
157macro_rules! macro_def {
158  ( $config:expr, $action:ident) => {
159    macro_rules! $action {
160      ($key:expr, $default:expr) => {
161        $config.$action(
162          $crate::replace!($crate::replace!(stringify!($key), " ", ""), "/", "_"),
163          || $default,
164        )
165      };
166    }
167  };
168}