Skip to main content

rainier_config/
key.rs

1//! Typed configuration keys — [`Key`] and [`config_keys!`](crate::config_keys).
2//!
3//! `config.set("cache.driver", "redis")` has two strings in it and the compiler
4//! checks neither. Misspell the path and the write lands somewhere nothing
5//! reads; misspell the value and it reads back as a driver that does not exist.
6//! Both failures surface far from the line that caused them.
7//!
8//! A [`Key<T>`] fixes both ends. It is a dotted path that knows what type lives
9//! there:
10//!
11//! ```
12//! use rainier_config::{config_keys, Config};
13//!
14//! config_keys! {
15//!     /// How many posts a listing shows.
16//!     pub POSTS_PER_PAGE: u64 = "posts.per_page";
17//! }
18//!
19//! let config = Config::new();
20//! config.set(POSTS_PER_PAGE, 15).unwrap();
21//!
22//! // No turbofish: the key says what comes back.
23//! let per_page = config.get(POSTS_PER_PAGE).unwrap();
24//! assert_eq!(per_page, 15_u64);
25//! ```
26//!
27//! And the mistakes stop compiling:
28//!
29//! ```compile_fail
30//! # use rainier_config::{config_keys, Config};
31//! # config_keys! { pub POSTS_PER_PAGE: u64 = "posts.per_page"; }
32//! # let config = Config::new();
33//! // error: expected `u64`, found `&str`
34//! config.set(POSTS_PER_PAGE, "fifteen").unwrap();
35//! ```
36//!
37//! ```compile_fail
38//! # use rainier_config::{config_keys, Config};
39//! # config_keys! { pub POSTS_PER_PAGE: u64 = "posts.per_page"; }
40//! # let config = Config::new();
41//! // error: the trait bound `Key<u64>: ConfigKey<String>` is not satisfied
42//! let name: Option<String> = config.get(POSTS_PER_PAGE);
43//! ```
44//!
45//! ## Plain strings still work
46//!
47//! [`ConfigKey<T>`] is implemented for `&str` and `String` for *every* `T`, so
48//! `config.get::<u16>("server.port")` reads exactly as it did. That is not
49//! grandfathering: a dotted path built at runtime — from a driver name, from a
50//! console argument — cannot be a `Key`, and pretending otherwise would just
51//! push callers into `format!` plus a cast.
52//!
53//! The typed form is for the keys an application names in its own source, which
54//! is nearly all of them.
55
56use std::marker::PhantomData;
57
58/// A dotted path, and the type stored at it.
59///
60/// Zero-sized beyond the `&'static str`, and `const`-constructible, so a module
61/// of these costs nothing at runtime.
62pub struct Key<T> {
63    path: &'static str,
64    /// `fn() -> T` rather than `T`, so a `Key<T>` is `Send + Sync + Copy`
65    /// whatever `T` is — a key is a name, and a name does not inherit the
66    /// thread-safety of the value it names.
67    marker: PhantomData<fn() -> T>,
68}
69
70impl<T> Key<T> {
71    /// A key at `path`.
72    ///
73    /// Prefer [`config_keys!`](crate::config_keys), which keeps the path, the type and the doc
74    /// comment on one line.
75    pub const fn new(path: &'static str) -> Self {
76        Self { path, marker: PhantomData }
77    }
78
79    /// The dotted path.
80    pub const fn path(&self) -> &'static str {
81        self.path
82    }
83
84    /// The same path, read as a different type.
85    ///
86    /// The escape hatch for the cases a single type cannot cover — reading a
87    /// key as raw [`Value`](serde_json::Value) to dump it, say. Rare enough
88    /// that it should look deliberate at the call site.
89    pub const fn as_type<U>(&self) -> Key<U> {
90        Key::new(self.path)
91    }
92}
93
94// Derived impls would demand `T: Clone`/`T: Copy`, which is wrong: a key holds
95// no `T`.
96impl<T> Clone for Key<T> {
97    fn clone(&self) -> Self {
98        *self
99    }
100}
101
102impl<T> Copy for Key<T> {}
103
104impl<T> std::fmt::Debug for Key<T> {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(f, "Key({})", self.path)
107    }
108}
109
110impl<T> std::fmt::Display for Key<T> {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.write_str(self.path)
113    }
114}
115
116impl<T> PartialEq for Key<T> {
117    fn eq(&self, other: &Self) -> bool {
118        self.path == other.path
119    }
120}
121
122impl<T> Eq for Key<T> {}
123
124/// Anything that names a place in the configuration tree.
125///
126/// The bound on the methods that do not care about the value's type —
127/// [`has`](crate::Config::has), [`forget`](crate::Config::forget),
128/// [`value`](crate::Config::value).
129pub trait ConfigPath {
130    /// The dotted path.
131    fn path(&self) -> &str;
132}
133
134impl ConfigPath for &str {
135    fn path(&self) -> &str {
136        self
137    }
138}
139
140impl ConfigPath for String {
141    fn path(&self) -> &str {
142        self
143    }
144}
145
146impl ConfigPath for &String {
147    fn path(&self) -> &str {
148        self
149    }
150}
151
152impl<T> ConfigPath for Key<T> {
153    fn path(&self) -> &str {
154        self.path
155    }
156}
157
158impl<T> ConfigPath for &Key<T> {
159    fn path(&self) -> &str {
160        self.path
161    }
162}
163
164/// A path that may hold a `T`.
165///
166/// The bound on [`get`](crate::Config::get), [`set`](crate::Config::set) and
167/// friends. A plain `&str` satisfies it for every `T` — it makes no claim about
168/// what is there — while a [`Key<T>`] satisfies it for exactly one, which is
169/// what turns a wrong type into a compile error.
170pub trait ConfigKey<T>: ConfigPath {}
171
172impl<T> ConfigKey<T> for &str {}
173impl<T> ConfigKey<T> for String {}
174impl<T> ConfigKey<T> for &String {}
175impl<T> ConfigKey<T> for Key<T> {}
176impl<T> ConfigKey<T> for &Key<T> {}
177
178/// Declare typed configuration keys.
179///
180/// One line per key: the name, the type stored there, and the dotted path.
181///
182/// ```
183/// use rainier_config::config_keys;
184///
185/// config_keys! {
186///     /// The application's display name.
187///     pub APP_NAME: String = "app.name";
188///     /// Whether to render a stack trace on a 500.
189///     pub APP_DEBUG: bool = "app.debug";
190/// }
191///
192/// assert_eq!(APP_NAME.path(), "app.name");
193/// ```
194///
195/// Grouping them in a `keys` module next to the section that writes them keeps
196/// the path and its one writer in the same file — which is the property that
197/// makes a rename a compile error everywhere instead of a silent no-op.
198#[macro_export]
199macro_rules! config_keys {
200    (
201        $(
202            $(#[$meta:meta])*
203            $vis:vis $name:ident: $type:ty = $path:literal;
204        )*
205    ) => {
206        $(
207            $(#[$meta])*
208            ///
209            #[doc = concat!("Reads and writes `", $path, "`.")]
210            $vis const $name: $crate::Key<$type> = $crate::Key::new($path);
211        )*
212    };
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    config_keys! {
220        /// A doc comment survives onto the constant.
221        pub CACHE_PREFIX: String = "cache.prefix";
222        pub POSTS_PER_PAGE: u64 = "posts.per_page";
223    }
224
225    #[test]
226    fn a_key_carries_its_path() {
227        assert_eq!(CACHE_PREFIX.path(), "cache.prefix");
228        assert_eq!(POSTS_PER_PAGE.to_string(), "posts.per_page");
229        assert_eq!(format!("{POSTS_PER_PAGE:?}"), "Key(posts.per_page)");
230    }
231
232    #[test]
233    fn a_key_is_copy_and_thread_safe_whatever_it_names() {
234        // `PhantomData<fn() -> T>` rather than `PhantomData<T>` is what buys
235        // this: a `Key<Rc<()>>` is still `Send`, because it holds no `Rc`.
236        fn assert_send_sync_copy<T: Send + Sync + Copy>(_: T) {}
237
238        assert_send_sync_copy(CACHE_PREFIX);
239        assert_send_sync_copy(Key::<std::rc::Rc<()>>::new("a.b"));
240    }
241
242    #[test]
243    fn two_keys_at_the_same_path_are_equal() {
244        assert_eq!(CACHE_PREFIX, Key::<String>::new("cache.prefix"));
245        assert_ne!(CACHE_PREFIX, Key::<String>::new("cache.other"));
246    }
247
248    #[test]
249    fn as_type_reuses_the_path() {
250        let raw: Key<serde_json::Value> = POSTS_PER_PAGE.as_type();
251        assert_eq!(raw.path(), "posts.per_page");
252    }
253
254    #[test]
255    fn every_spelling_of_a_path_is_accepted() {
256        let owned = String::from("a.b");
257        assert_eq!(ConfigPath::path(&"a.b"), "a.b");
258        assert_eq!(ConfigPath::path(&owned), "a.b");
259        assert_eq!(ConfigPath::path(&&owned), "a.b");
260        assert_eq!(ConfigPath::path(&CACHE_PREFIX), "cache.prefix");
261        assert_eq!(ConfigPath::path(&&CACHE_PREFIX), "cache.prefix");
262    }
263}