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 | |
| 9 | use crate::{ |
| 10 | prelude::*, |
| 11 | file::JdatMapFile, |
| 12 | }; |
| 13 | |
| 14 | use oxedyne_fe2o3_core::{ |
| 15 | prelude::*, |
| 16 | }; |
| 17 | |
| 18 | use std::path::Path; |
| 19 | |
| 20 | |
| 21 | pub 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 | |
| 48 | impl<T: Config> JdatMapFile for T {} |
| 49 | |
| 50 | |
| 51 | pub 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 | |
| 59 | fn 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 | |
| 91 | fn 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 | |
| 117 | pub 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)] |
| 142 | mod 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 | } |