Skip to main content

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}