Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_jdat/src/cfg.rs

5.0 KiB, 16 runs

created by r1870400018:455, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! What a configuration is, and how a value in one is allowed to point
2//! somewhere else.
3//!
4//! [`Config`] is the trait a configuration type implements. [`resolve`] and
5//! [`resolve_dat`] are the indirection every Oxedyne configuration file has: a
6//! value that must not sit in the file itself is written `{file:secret}` or
7//! `{env:VAR}` and is fetched when the file is read.
8
9use crate::{
10 prelude::*,
11 file::JdatMapFile,
12};
13
14use oxedyne_fe2o3_core::{
15 prelude::*,
16};
17
18use std::path::Path;
19
20
21pub trait Config:
22 Clone
23 + std::fmt::Debug
24 + Default
25 + Eq
26 + PartialEq
27 + FromDatMap
28 + ToDatMap
29{
30 // Required.
31 fn check_and_fix(&mut self) -> Outcome<()> {
32 Err(err!(
33 "Don't forget to implement checks on the configuration.";
34 Unimplemented, Configuration))
35 }
36
37 // Provided.
38 fn dump(self) -> Outcome<()> {
39 let dat = Self::to_datmap(self);
40 for line in dat.to_lines(" ", true) {
41 info!("{}", line);
42 }
43 Ok(())
44 }
45
46}
47
48impl<T: Config> JdatMapFile for T {}
49
50
51pub fn resolve(value: &str, root: &Path)
52 -> Outcome<String>
53{
54 // The environment first, so an environment value may name a file.
55 let named = res!(resolve_env(value));
56 resolve_files(&named, value, root)
57}
58
59fn resolve_env(whole: &str)
60 -> Outcome<String>
61{
62 let mut out = fmt!("{}", whole);
63 while let Some(start) = out.find("{env:") {
64 let end = match out[start..].find('}') {
65 Some(i) => start + i,
66 None => return Err(err!(
67 "The configuration value {:?} opens an {{env: reference and does not \
68 close it.", whole;
69 Invalid, Input)),
70 };
71 let inner = fmt!("{}", &out[start + 5..end]);
72 let (name, fallback) = match inner.find(':') {
73 Some(i) => (&inner[..i], Some(&inner[i + 1..])),
74 None => (inner.as_str(), None),
75 };
76 let got = match std::env::var(name) {
77 Ok(v) if !v.is_empty() => v,
78 _ => match fallback {
79 Some(d) => fmt!("{}", d),
80 None => return Err(err!(
81 "The configuration names the environment variable {:?}, which is \
82 not set, and gives no default.", name;
83 Invalid, Input, Missing)),
84 },
85 };
86 out.replace_range(start..=end, &got);
87 }
88 Ok(out)
89}
90
91fn resolve_files(value: &str, whole: &str, root: &Path)
92 -> Outcome<String>
93{
94 let mut out = fmt!("{}", value);
95 while let Some(start) = out.find("{file:") {
96 let end = match out[start..].find('}') {
97 Some(i) => start + i,
98 None => return Err(err!(
99 "The configuration value {:?} opens a {{file: reference and does not \
100 close it.", whole;
101 Invalid, Input)),
102 };
103 let rel = fmt!("{}", &out[start + 6..end]);
104 let path = root.join(&rel);
105 let held = match std::fs::read_to_string(&path) {
106 Ok(t) => fmt!("{}", t.trim()),
107 Err(e) => return Err(err!(e,
108 "The configuration reads {{file:{}}}, and {:?} could not be read.",
109 rel, path;
110 IO, File, Read)),
111 };
112 out.replace_range(start..=end, &held);
113 }
114 Ok(out)
115}
116
117pub fn resolve_dat(dat: &Dat, root: &Path)
118 -> Outcome<Dat>
119{
120 Ok(match dat {
121 Dat::Str(s) => Dat::Str(res!(resolve(s, root))),
122 Dat::List(items) => {
123 let mut out = Vec::with_capacity(items.len());
124 for item in items {
125 out.push(res!(resolve_dat(item, root)));
126 }
127 Dat::List(out)
128 },
129 Dat::Map(map) => {
130 let mut out = DaticleMap::new();
131 for (k, v) in map {
132 out.insert(res!(resolve_dat(k, root)), res!(resolve_dat(v, root)));
133 }
134 Dat::Map(out)
135 },
136 other => other.clone(),
137 })
138}
139
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn a_reference_is_expanded_or_refused() -> Outcome<()> {
147 let root = Path::new(".");
148 std::env::set_var("FE2O3_JDAT_CFG_TEST_VALUE", "expanded");
149 assert_eq!(res!(resolve("{env:FE2O3_JDAT_CFG_TEST_VALUE}", root)), "expanded");
150 assert_eq!(res!(resolve("a {env:FE2O3_JDAT_CFG_TEST_VALUE} b", root)),
151 "a expanded b");
152 assert_eq!(res!(resolve("{env:FE2O3_JDAT_CFG_TEST_ABSENT:fallback}", root)),
153 "fallback");
154 assert!(resolve("{env:FE2O3_JDAT_CFG_TEST_ABSENT}", root).is_err(),
155 "a reference with no value and no default is refused");
156 assert!(resolve("{env:unclosed", root).is_err());
157 assert_eq!(res!(resolve("nothing to expand", root)), "nothing to expand");
158 Ok(())
159 }
160
161 #[test]
162 fn what_names_nothing_is_untouched() -> Outcome<()> {
163 let root = Path::new(".");
164 let dat = Dat::List(vec![
165 Dat::Str(fmt!("plain")),
166 Dat::U8(42),
167 ]);
168 let out = res!(resolve_dat(&dat, root));
169 assert_eq!(out, dat);
170 Ok(())
171 }
172}