webserver_base/
environment.rs1use std::fmt::{self, Display, Formatter};
10use std::str::FromStr;
11
12use serde::{Deserialize, Serialize};
13
14use crate::env::{self, EnvError};
15
16pub const ENV_ENVIRONMENT: &str = "WSB_ENVIRONMENT";
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Environment {
23 Local,
25 Production,
27}
28
29impl Environment {
30 #[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 #[must_use]
41 pub const fn is_production(&self) -> bool {
42 matches!(self, Self::Production)
43 }
44
45 #[must_use]
47 pub const fn is_local(&self) -> bool {
48 matches!(self, Self::Local)
49 }
50
51 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#[derive(Debug, thiserror::Error)]
69#[error("`{value}` is not a supported environment; use `local` or `production`")]
70pub struct EnvironmentParseError {
71 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}