Skip to main content

sofie_core/
environment.rs

1use std::env::VarError;
2
3use crate::{Error, Result};
4
5/// `None` when unset; a value that is not Unicode is refused, naming the variable.
6pub fn environment_variable(name: &str) -> Result<Option<String>> {
7    unicode(name, std::env::var(name))
8}
9
10pub const BASE_URL: &str = "SOFIE_BASE_URL";
11
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct Variable<'name> {
14    pub name: &'name str,
15    pub value: String,
16}
17
18/// An empty value counts as unset, as `.env.example` lists keys empty.
19pub fn base_url(
20    own: &str,
21    lookup: impl Fn(&str) -> Result<Option<String>>,
22) -> Result<Option<Variable<'_>>> {
23    for name in [own, BASE_URL] {
24        if let Some(value) = lookup(name)?.filter(|value| !value.trim().is_empty()) {
25            return Ok(Some(Variable { name, value }));
26        }
27    }
28    Ok(None)
29}
30
31#[must_use]
32pub fn base_url_missing(own: &str, purpose: &str) -> Error {
33    Error::internal_message(format!("neither {own} nor {BASE_URL} is set; {purpose}"))
34}
35
36fn unicode(name: &str, value: std::result::Result<String, VarError>) -> Result<Option<String>> {
37    match value {
38        Ok(value) => Ok(Some(value)),
39        Err(VarError::NotPresent) => Ok(None),
40        Err(VarError::NotUnicode(_)) => {
41            Err(Error::internal_message(format!("{name} is not Unicode")))
42        }
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use std::ffi::OsString;
49
50    use super::*;
51
52    #[test]
53    fn a_value_that_is_not_unicode_is_refused() {
54        let error = unicode("DATABASE_URL", Err(VarError::NotUnicode(OsString::new())))
55            .err()
56            .unwrap();
57        assert_eq!(error.to_string(), "DATABASE_URL is not Unicode");
58    }
59
60    fn given(variables: &[(&str, &str)]) -> impl Fn(&str) -> Result<Option<String>> {
61        let variables: Vec<(String, String)> = variables
62            .iter()
63            .map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
64            .collect();
65        move |name| {
66            Ok(variables
67                .iter()
68                .find(|(variable, _)| variable == name)
69                .map(|(_, value)| value.clone()))
70        }
71    }
72
73    #[test]
74    fn a_package_s_own_base_url_overrides_the_shared_one() {
75        const OWN: &str = "SOFIE_MAIL_BASE_URL";
76        let shared = base_url(OWN, given(&[(BASE_URL, "https://a.test")])).unwrap();
77        assert_eq!(
78            shared,
79            Some(Variable {
80                name: BASE_URL,
81                value: "https://a.test".to_owned()
82            })
83        );
84
85        let both = [(BASE_URL, "https://a.test"), (OWN, "https://mail.test")];
86        let own = base_url(OWN, given(&both)).unwrap().unwrap();
87        assert_eq!((own.name, own.value.as_str()), (OWN, "https://mail.test"));
88
89        let empty_own = [(BASE_URL, "https://a.test"), (OWN, " ")];
90        let fallen_back = base_url(OWN, given(&empty_own)).unwrap().unwrap();
91        assert_eq!(fallen_back.name, BASE_URL);
92        assert_eq!(base_url(OWN, given(&[])).unwrap(), None);
93        assert_eq!(
94            base_url_missing(OWN, "mails link back to the app").to_string(),
95            "neither SOFIE_MAIL_BASE_URL nor SOFIE_BASE_URL is set; mails link back to the app"
96        );
97    }
98}