Skip to main content

webserver_base/
environment.rs

1//! Which deployment this process is: a laptop, or the droplet. There is no dev
2//! or staging tier.
3//!
4//! [`Environment::Production`] is the strict one — error monitoring is required
5//! there, and analytics only fire there. Required everywhere rather than
6//! defaulted, because a default means a misconfigured deploy behaves like a
7//! laptop.
8
9use std::fmt::{self, Display, Formatter};
10use std::str::FromStr;
11
12use serde::{Deserialize, Serialize};
13
14use crate::env::{self, EnvError};
15
16/// The environment variable [`Environment::from_env`] reads.
17pub const ENV_ENVIRONMENT: &str = "WSB_ENVIRONMENT";
18
19/// Which deployment this process is.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Environment {
23    /// Running on a development machine.
24    Local,
25    /// Running in production.
26    Production,
27}
28
29impl Environment {
30    /// The lowercase name, as written in `WSB_ENVIRONMENT`.
31    #[must_use]
32    pub const fn as_str(&self) -> &'static str {
33        match self {
34            Self::Local => "local",
35            Self::Production => "production",
36        }
37    }
38
39    /// Whether this is production.
40    #[must_use]
41    pub const fn is_production(&self) -> bool {
42        matches!(self, Self::Production)
43    }
44
45    /// Whether this is a development machine.
46    #[must_use]
47    pub const fn is_local(&self) -> bool {
48        matches!(self, Self::Local)
49    }
50
51    /// Reads [`ENV_ENVIRONMENT`]. Deliberately has no default.
52    ///
53    /// # Errors
54    ///
55    /// [`EnvError::Missing`] if unset, [`EnvError::Empty`] if blank, or
56    /// [`EnvError::Invalid`] if it is neither `local` nor `production`.
57    pub fn from_env() -> Result<Self, EnvError> {
58        let value: String = env::required(ENV_ENVIRONMENT)?;
59        value.parse::<Self>().map_err(|_| EnvError::Invalid {
60            key: ENV_ENVIRONMENT.to_string(),
61            expected: "environment (`local` or `production`)",
62            value,
63        })
64    }
65}
66
67/// The value was neither `local` nor `production`.
68#[derive(Debug, thiserror::Error)]
69#[error("`{value}` is not a supported environment; use `local` or `production`")]
70pub struct EnvironmentParseError {
71    /// What was supplied.
72    pub value: String,
73}
74
75impl FromStr for Environment {
76    type Err = EnvironmentParseError;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s.trim().to_lowercase().as_str() {
80            "local" => Ok(Self::Local),
81            "production" => Ok(Self::Production),
82            _ => Err(EnvironmentParseError {
83                value: s.to_string(),
84            }),
85        }
86    }
87}
88
89impl Display for Environment {
90    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91        f.write_str(self.as_str())
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{Environment, EnvironmentParseError};
98
99    #[test]
100    fn both_environments_round_trip_through_their_string_form() {
101        for expected in [Environment::Local, Environment::Production] {
102            let actual: Environment = expected
103                .as_str()
104                .parse::<Environment>()
105                .expect("its own as_str is always parseable");
106            assert_eq!(expected, actual);
107        }
108    }
109
110    #[test]
111    fn parsing_is_case_and_whitespace_insensitive() {
112        let expected: Environment = Environment::Production;
113        let actual: Environment = "  PRODUCTION  ".parse().expect("trimmed and lowercased");
114        assert_eq!(expected, actual);
115    }
116
117    #[test]
118    fn a_tier_that_does_not_exist_is_rejected_by_name() {
119        let error: EnvironmentParseError = "staging"
120            .parse::<Environment>()
121            .expect_err("there is no staging");
122
123        let expected: String =
124            String::from("`staging` is not a supported environment; use `local` or `production`");
125        let actual: String = error.to_string();
126        assert_eq!(expected, actual);
127    }
128
129    #[test]
130    fn the_predicates_agree_with_the_variant() {
131        assert!(Environment::Production.is_production());
132        assert!(!Environment::Production.is_local());
133        assert!(Environment::Local.is_local());
134        assert!(!Environment::Local.is_production());
135    }
136
137    #[test]
138    fn it_serializes_lowercase_for_template_data() {
139        let expected: String = String::from("\"production\"");
140        let actual: String =
141            serde_json::to_string(&Environment::Production).expect("plain enum serializes");
142        assert_eq!(expected, actual);
143    }
144}