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}