rainier_support/setting.rs
1//! Closed-set settings — [`Setting`] and [`setting_enum!`](crate::setting_enum).
2//!
3//! A driver name is not a string. It is one of a handful of values the code
4//! actually knows how to build, and every other string is a mistake. Spelling
5//! it as a `String` moves that mistake from the compiler to production:
6//!
7//! ```text
8//! CACHE_DRIVER=redys # boots fine, caches in-process, forever
9//! ```
10//!
11//! `setting_enum!` declares the closed set once and derives everything that
12//! follows from it — the wire spelling, `Display`, `FromStr`, serde, and a
13//! parse error that lists what was expected:
14//!
15//! ```
16//! use rainier_support::{setting_enum, Setting};
17//!
18//! setting_enum! {
19//! /// Where cached values live.
20//! pub enum CacheDriver: "cache driver" {
21//! /// This process only.
22//! #[default]
23//! Memory = "memory",
24//! /// One Redis server.
25//! Redis = "redis",
26//! /// A sharded Redis Cluster.
27//! RedisCluster = "redis-cluster",
28//! }
29//! }
30//!
31//! assert_eq!(CacheDriver::parse("redis-cluster").unwrap(), CacheDriver::RedisCluster);
32//! assert_eq!(CacheDriver::default(), CacheDriver::Memory);
33//!
34//! let err = CacheDriver::parse("redys").unwrap_err();
35//! assert_eq!(
36//! err.message(),
37//! "`redys` is not a valid cache driver; expected one of `memory`, `redis`, `redis-cluster`",
38//! );
39//! ```
40//!
41//! ## Why parsing fails rather than falling back
42//!
43//! A default is for a value nobody set. It is not for a value somebody set
44//! *wrong* — silently substituting one there means the deployment that typed
45//! `redys` runs on an in-process cache, and the first symptom is a rate limiter
46//! that lets through `N ×` its limit across `N` instances.
47//!
48//! Failing at boot puts the error where the mistake was made, with the list of
49//! valid values in the message.
50//!
51//! ## Every setting has a default
52//!
53//! `setting_enum!` always derives [`Default`], so exactly one variant must
54//! carry `#[default]` and the macro will not compile otherwise. That is
55//! deliberate: a setting with no default is a setting that has to be named in
56//! every environment, and if that is genuinely what you want, it belongs in
57//! `Env::require` rather than here.
58
59use crate::{Error, Result};
60
61/// A setting whose values are a closed set of wire spellings.
62///
63/// Implemented by [`setting_enum!`](crate::setting_enum). Hand-implementing is possible but there is
64/// no reason to: everything here follows mechanically from the variant list.
65pub trait Setting: Copy + Eq + Sized + 'static {
66 /// What this setting is called in an error message — `"cache driver"`.
67 ///
68 /// Lower case and unqualified, because it is interpolated mid-sentence.
69 const SETTING: &'static str;
70
71 /// Every variant, in the order they should be listed to a human.
72 const ALL: &'static [Self];
73
74 /// The wire spelling — what appears in `.env`, in the config tree, and in
75 /// serialised output.
76 fn as_str(&self) -> &'static str;
77
78 /// Parse a wire spelling.
79 ///
80 /// Tolerant in the ways that are unambiguous and no further: surrounding
81 /// whitespace, letter case, and `_` where the canonical spelling uses `-`.
82 /// `Redis_Cluster` is what someone means by `redis-cluster`; `redys` is
83 /// not, and no amount of guessing makes it so.
84 fn parse(raw: &str) -> Result<Self> {
85 let wanted = raw.trim().replace('_', "-");
86 Self::ALL
87 .iter()
88 .copied()
89 .find(|variant| variant.as_str().eq_ignore_ascii_case(&wanted))
90 .ok_or_else(|| {
91 Error::internal(format!(
92 "`{}` is not a valid {}; expected one of {}",
93 raw.trim(),
94 Self::SETTING,
95 Self::options()
96 ))
97 })
98 }
99
100 /// The valid spellings, backtick-quoted and comma-separated.
101 ///
102 /// For an error message, a `--help` line, or a config-dump command.
103 fn options() -> String {
104 Self::ALL
105 .iter()
106 .map(|variant| format!("`{}`", variant.as_str()))
107 .collect::<Vec<_>>()
108 .join(", ")
109 }
110}
111
112/// Declare a closed-set setting: an enum plus everything that follows from it.
113///
114/// See the [module docs](self) for the shape and the reasoning. The expansion
115/// derives `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`, `Default`,
116/// `Serialize` and `Deserialize`, and implements [`Setting`], [`Display`],
117/// [`FromStr`] and `AsRef<str>`.
118///
119/// [`Display`]: std::fmt::Display
120/// [`FromStr`]: std::str::FromStr
121///
122/// The serde impls are written by hand rather than derived, for two reasons.
123/// The caller does not need `serde` in its own dependencies — the macro reaches
124/// it through this crate — and, more usefully, deserialising goes through
125/// [`Setting::parse`], so **the wire format cannot drift from the parser** and
126/// a bad value in a config file gets the same message as a bad value in `.env`.
127#[macro_export]
128macro_rules! setting_enum {
129 (
130 $(#[$enum_meta:meta])*
131 $vis:vis enum $name:ident: $setting:literal {
132 $(
133 $(#[$variant_meta:meta])*
134 $variant:ident = $wire:literal
135 ),+ $(,)?
136 }
137 ) => {
138 $(#[$enum_meta])*
139 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
140 $vis enum $name {
141 $(
142 $(#[$variant_meta])*
143 $variant,
144 )+
145 }
146
147 impl $crate::__private::serde::Serialize for $name {
148 fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
149 where
150 S: $crate::__private::serde::Serializer,
151 {
152 serializer.serialize_str(<Self as $crate::Setting>::as_str(self))
153 }
154 }
155
156 impl<'de> $crate::__private::serde::Deserialize<'de> for $name {
157 fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
158 where
159 D: $crate::__private::serde::Deserializer<'de>,
160 {
161 use $crate::__private::serde::de::Error as _;
162
163 let raw = <::std::string::String as $crate::__private::serde::Deserialize>::deserialize(
164 deserializer,
165 )?;
166 <Self as $crate::Setting>::parse(&raw)
167 .map_err(|e| D::Error::custom(e.message()))
168 }
169 }
170
171 impl $crate::Setting for $name {
172 const SETTING: &'static str = $setting;
173 const ALL: &'static [Self] = &[$(Self::$variant),+];
174
175 fn as_str(&self) -> &'static str {
176 match self {
177 $(Self::$variant => $wire,)+
178 }
179 }
180 }
181
182 impl $name {
183 /// The wire spelling — what appears in `.env` and in the config
184 /// tree.
185 ///
186 /// Inherent as well as on the trait, so callers do not have to
187 /// import `Setting` to print one.
188 $vis fn as_str(&self) -> &'static str {
189 <Self as $crate::Setting>::as_str(self)
190 }
191 }
192
193 impl ::std::fmt::Display for $name {
194 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
195 f.write_str(<Self as $crate::Setting>::as_str(self))
196 }
197 }
198
199 impl ::std::str::FromStr for $name {
200 type Err = $crate::Error;
201
202 fn from_str(raw: &str) -> ::std::result::Result<Self, Self::Err> {
203 <Self as $crate::Setting>::parse(raw)
204 }
205 }
206
207 impl ::std::convert::AsRef<str> for $name {
208 fn as_ref(&self) -> &str {
209 <Self as $crate::Setting>::as_str(self)
210 }
211 }
212 };
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 setting_enum! {
220 /// A stand-in for the real driver enums, which live in the crates that
221 /// own the concept.
222 pub enum Driver: "cache driver" {
223 #[default]
224 Memory = "memory",
225 Redis = "redis",
226 RedisCluster = "redis-cluster",
227 }
228 }
229
230 #[test]
231 fn round_trips_through_its_wire_spelling() {
232 for driver in Driver::ALL {
233 assert_eq!(Driver::parse(driver.as_str()).unwrap(), *driver);
234 assert_eq!(driver.to_string(), driver.as_str());
235 }
236 }
237
238 #[test]
239 fn parsing_is_forgiving_about_case_whitespace_and_underscores() {
240 // The three ways a human writes the same value.
241 assert_eq!(Driver::parse(" redis-cluster \n").unwrap(), Driver::RedisCluster);
242 assert_eq!(Driver::parse("Redis-Cluster").unwrap(), Driver::RedisCluster);
243 assert_eq!(Driver::parse("REDIS_CLUSTER").unwrap(), Driver::RedisCluster);
244 }
245
246 #[test]
247 fn an_unknown_value_names_itself_and_lists_the_alternatives() {
248 // The whole point: this is a boot failure, not a silent fallback.
249 let err = Driver::parse("redys").unwrap_err();
250
251 assert_eq!(
252 err.message(),
253 "`redys` is not a valid cache driver; expected one of `memory`, `redis`, `redis-cluster`"
254 );
255 }
256
257 #[test]
258 fn the_reported_value_is_what_was_written_not_what_was_normalised() {
259 // Echoing back `REDYS` when the operator typed ` REDYS ` is fine;
260 // echoing back `redys` would send them looking for the wrong line.
261 let err = Driver::parse(" REDYS ").unwrap_err();
262 assert!(err.message().starts_with("`REDYS` is not"), "{}", err.message());
263 }
264
265 #[test]
266 fn an_empty_value_is_an_error_rather_than_the_default() {
267 // `CACHE_DRIVER=` in a `.env` is a half-finished edit. Treating it as
268 // "unset" would hide it; the caller supplies the default by not calling
269 // this at all.
270 assert!(Driver::parse("").is_err());
271 }
272
273 #[test]
274 fn serde_uses_the_same_spelling_as_the_parser() {
275 // If these ever diverged, a value written by `Config::set` would not
276 // read back through `Config::get`.
277 let json = serde_json::to_string(&Driver::RedisCluster).unwrap();
278 assert_eq!(json, "\"redis-cluster\"");
279
280 let back: Driver = serde_json::from_str(&json).unwrap();
281 assert_eq!(back, Driver::RedisCluster);
282 }
283
284 #[test]
285 fn the_default_is_the_marked_variant() {
286 assert_eq!(Driver::default(), Driver::Memory);
287 }
288
289 #[test]
290 fn from_str_is_the_same_parser() {
291 assert_eq!("redis".parse::<Driver>().unwrap(), Driver::Redis);
292 assert!("nope".parse::<Driver>().is_err());
293 }
294
295 #[test]
296 fn options_lists_every_variant_in_declaration_order() {
297 assert_eq!(Driver::options(), "`memory`, `redis`, `redis-cluster`");
298 }
299}