1pub use ground_env_derive::FromEnv;
2use std::borrow::Cow;
3
4#[cfg(test)]
5mod tests;
6
7pub struct Context {
8 prefix: Vec<&'static str>,
9 env: std::collections::HashMap<String, Result<String, std::ffi::OsString>>,
10}
11
12impl Context {
13 pub fn env() -> Self {
14 let env = std::env::vars_os()
15 .filter_map(|(key, value)| {
16 let key = key.into_string().ok()?;
18 let value = value.into_string();
20 Some((key, value))
21 })
22 .collect::<std::collections::HashMap<_, _>>();
23
24 Self {
25 prefix: vec![],
26 env,
27 }
28 }
29
30 pub fn empty() -> Self {
31 Self {
32 prefix: vec![],
33 env: Default::default(),
34 }
35 }
36
37 #[doc(hidden)]
38 pub fn with_prefix<T: FromEnv>(&mut self, prefix: &'static str) -> Result<T> {
39 self.prefix.push(prefix);
40
41 let out = T::from_ctx(self);
42
43 let old = self.prefix.pop();
44
45 assert!(old.is_some(), "Any operation on the prefix should be self-contained. [Something being flattened removed an extra segment]");
46
47 out
48 }
49
50 #[doc(hidden)]
51 pub fn resolve(&self, key: &'static str) -> Result<Result<&str, String>> {
52 let mut key_alloc;
53
54 let key = if self.prefix.is_empty() {
55 Cow::Borrowed(key)
56 } else {
57 let len = self.prefix.iter()
58 .map(|item| item.len())
59 .sum::<usize>()
60 + key.len();
61 key_alloc = String::with_capacity(len);
62 for prefix in self.prefix.iter() {
63 key_alloc.push_str(*prefix);
64 }
65 key_alloc.push_str(key);
66 Cow::Owned(key_alloc)
67 };
68
69 match self.env.get(key.as_ref()) {
70 Some(t) => match t {
71 Ok(va) => Ok(Ok(va.as_ref())),
72 Err(o) => Err(Error::NotUnicode(key.into_owned(), o.clone())),
73 },
74 None => Ok(Err(key.into_owned())),
75 }
76 }
77}
78
79pub type Result<T, E = Error> = std::result::Result<T, E>;
80
81#[derive(Debug, thiserror::Error)]
82pub enum Error {
83 #[error("Unable to locate '{0}' within environment")]
84 Missing(String),
85 #[error("Unable to convert '{0}' into UTF-8")]
86 NotUnicode(String, std::ffi::OsString),
87 #[error("Parsing '{input}' as '{ty}' failed: {err}")]
88 Parse {
89 err: String,
90 input: String,
91 ty: &'static str,
92 },
93}
94
95pub trait FromEnv: Sized {
96 fn from_env() -> Result<Self> {
97 Self::from_ctx(&mut Context::env())
98 }
99
100 fn from_ctx(ctx: &mut Context) -> Result<Self>;
101}
102
103pub trait Parse: Sized {
104 fn parse(input: &str) -> Result<Self>;
105}
106
107impl<T, E> Parse for T
108 where
109 T: std::str::FromStr<Err = E>,
110 E: std::error::Error,
111{
112 fn parse(value: &str) -> Result<Self> {
113 std::str::FromStr::from_str(value).map_err(|err: E| Error::Parse {
114 err: err.to_string(),
115 input: value.to_string(),
116 ty: std::any::type_name::<Self>(),
117 })
118 }
119}
120
121#[doc(hidden)]
122pub fn transpose_err<T, E, U>(result: Result<Result<T, U>, E>) -> Result<Result<T, E>, U> {
123 match result {
124 Ok(result) => match result {
125 Ok(value) => Ok(Ok(value)),
126 Err(err) => Err(err),
127 },
128 Err(err) => Ok(Err(err)),
129 }
130}
131
132#[doc(hidden)]
134pub fn flatten_err<T, E>(result: Result<T, Result<T, E>>) -> Result<T, E> {
135 match result {
136 Ok(value) => Ok(value),
137 Err(result) => match result {
138 Ok(value) => Ok(value),
139 Err(err) => Err(err),
140 },
141 }
142}