rainier_framework/keys.rs
1//! The configuration keys Rainier itself reads — a typed index of the tree.
2//!
3//! Laravel's answer to "what can I put in `config/app.php`?" is to read the
4//! shipped file. Rainier's is this module: every key the framework writes or
5//! reads, with the type stored at it.
6//!
7//! ```
8//! use rainier_framework::config::Config;
9//! use rainier_framework::keys;
10//!
11//! let config = Config::new();
12//! config.set(keys::APP_NAME, "My App".to_string()).unwrap();
13//!
14//! assert_eq!(config.get(keys::APP_NAME).as_deref(), Some("My App"));
15//! ```
16//!
17//! Using them is optional — every `Config` method still takes a `&str` — but
18//! it is what turns two classes of silent mistake into compile errors:
19//!
20//! ```compile_fail
21//! # use rainier_framework::{config::Config, keys};
22//! # let config = Config::new();
23//! // The port is a u16, so this does not compile.
24//! config.set(keys::SERVER_PORT, "8000").unwrap();
25//! ```
26//!
27//! ```compile_fail
28//! # use rainier_framework::{config::Config, keys};
29//! # let config = Config::new();
30//! // Neither does a driver spelled as a string.
31//! config.set(keys::CACHE_DRIVER, "redis").unwrap();
32//! ```
33//!
34//! ## Reading a driver
35//!
36//! The driver keys hold [settings](rainier_support::Setting) — closed sets with
37//! their own parser. Read them with [`Config::setting`], which distinguishes
38//! *unset* (use the default) from *set to nonsense* (fail, and list what was
39//! expected):
40//!
41//! ```
42//! use rainier_framework::cache::CacheDriver;
43//! use rainier_framework::config::Config;
44//! use rainier_framework::keys;
45//!
46//! let config = Config::new();
47//! config.set(keys::CACHE_DRIVER, CacheDriver::Redis).unwrap();
48//!
49//! match config.setting(keys::CACHE_DRIVER).unwrap() {
50//! CacheDriver::Memory => { /* … */ }
51//! CacheDriver::Redis | CacheDriver::RedisCluster => { /* … */ }
52//! CacheDriver::Memcached => { /* … */ }
53//! CacheDriver::DynamoDb => { /* … */ }
54//! }
55//! ```
56//!
57//! [`Config::setting`]: rainier_config::Config::setting
58//!
59//! ## Adding your own
60//!
61//! An application declares its keys the same way, next to the section that
62//! writes them:
63//!
64//! ```
65//! use rainier_framework::config::config_keys;
66//!
67//! config_keys! {
68//! /// How many posts a listing shows.
69//! pub POSTS_PER_PAGE: u64 = "posts.per_page";
70//! }
71//! ```
72
73use rainier_cache::CacheDriver;
74use rainier_config::{config_keys, AppEnv};
75use rainier_mail::MailDriver;
76use rainier_queue::QueueDriver;
77use rainier_session::SessionDriver;
78use rainier_telemetry::LogFormat;
79
80config_keys! {
81 // --- app ---------------------------------------------------------------
82
83 /// The application's display name, in page titles and mail.
84 pub APP_NAME: String = "app.name";
85
86 /// Which deployment this is.
87 ///
88 /// Defaults to [`AppEnv::Production`] when `APP_ENV` is unset — the safe
89 /// direction to be wrong in.
90 pub APP_ENV: AppEnv = "app.env";
91
92 /// Whether a failure may show its internals to the client.
93 ///
94 /// Separate from [`APP_ENV`] because they drift the moment someone debugs
95 /// staging.
96 pub APP_DEBUG: bool = "app.debug";
97
98 /// The application's canonical base URL, used to generate absolute links.
99 pub APP_URL: String = "app.url";
100
101 /// The directory the application was started from. Set by the framework;
102 /// there is no environment variable for it.
103 pub APP_BASE_PATH: String = "app.base_path";
104
105 // --- server ------------------------------------------------------------
106
107 /// The interface the HTTP server binds to.
108 pub SERVER_HOST: String = "server.host";
109
110 /// The port the HTTP server binds to.
111 pub SERVER_PORT: u16 = "server.port";
112
113 /// The largest request body that will be buffered, in bytes.
114 pub SERVER_MAX_BODY_BYTES: u64 = "server.max_body_bytes";
115
116 /// How long a handler may take before the request is cancelled and
117 /// answered `408`, in seconds. `0` turns it off.
118 ///
119 /// Off by default, because the right ceiling is a fact about your
120 /// application and a wrong one cancels work that was going to succeed.
121 /// `30` is a reasonable first answer for an API; a route that legitimately
122 /// takes longer should carry its own
123 /// [`Timeout`](rainier_middleware::Timeout) rather than raising this for
124 /// everything.
125 ///
126 /// This bounds the **handler**, not the response body — a streaming or
127 /// server-sent-events route returns its response immediately and streams
128 /// afterwards, so it is unaffected.
129 pub SERVER_REQUEST_TIMEOUT_SECS: u64 = "server.request_timeout_secs";
130
131 /// Whether to gzip text responses on the way out.
132 ///
133 /// Off by default: a deployment behind nginx, a CDN or a load balancer
134 /// usually compresses there, and doing it twice is CPU spent to produce
135 /// the same bytes. Turn it on when Rainier is the thing clients talk to.
136 ///
137 /// See [`Compress`](rainier_middleware::Compress) for what it will and
138 /// will not compress.
139 pub SERVER_COMPRESSION: bool = "server.compression";
140
141 // --- observability ------------------------------------------------------
142 //
143 // All three are off by default. Each costs something a request pays for —
144 // a lock and a timer, a document rendered at boot, a span per request —
145 // and an application that has not asked for them should not pay it.
146
147 /// Whether to record Prometheus metrics.
148 pub METRICS_ENABLED: bool = "metrics.enabled";
149
150 /// The path the scrape endpoint is served at.
151 ///
152 /// Configurable because it is the one endpoint you may want somewhere
153 /// unguessable, on a deployment where it cannot be put behind auth.
154 pub METRICS_PATH: String = "metrics.path";
155
156 /// Whether to serve the OpenAPI document.
157 pub OPENAPI_ENABLED: bool = "openapi.enabled";
158
159 /// The path the document is served at.
160 pub OPENAPI_PATH: String = "openapi.path";
161
162 /// The `info.title` of the document.
163 pub OPENAPI_TITLE: String = "openapi.title";
164
165 /// The `info.version` of the document — your API's version, not the
166 /// framework's.
167 pub OPENAPI_VERSION: String = "openapi.version";
168
169 /// The base URL clients should use, if the document should name one.
170 pub OPENAPI_SERVER: String = "openapi.server";
171
172 /// Whether to join and propagate W3C trace context.
173 ///
174 /// Cheap: no exporter, no collector, just the `traceparent` header and a
175 /// trace id on every log line. Worth having on even without OTLP.
176 pub TELEMETRY_ENABLED: bool = "telemetry.enabled";
177
178 /// The OTLP collector's gRPC endpoint — `http://localhost:4317`.
179 ///
180 /// Absent unless set, and absent means spans are not exported. Needs the
181 /// `otlp` feature.
182 pub TELEMETRY_ENDPOINT: String = "telemetry.endpoint";
183
184 /// What this service calls itself in a trace.
185 pub TELEMETRY_SERVICE_NAME: String = "telemetry.service_name";
186
187 /// What fraction of traces this service *starts* to record, `0.0` to
188 /// `1.0`.
189 ///
190 /// A trace that arrives with a decision keeps it, whatever this says.
191 pub TELEMETRY_SAMPLE_RATIO: f64 = "telemetry.sample_ratio";
192
193 /// The shape of a log line — `auto`, `pretty`, `compact`, `json`.
194 ///
195 /// `auto` is JSON in production and staging and pretty everywhere else,
196 /// which is what you want without having to say so. Set it explicitly to
197 /// read production logs by eye for an afternoon.
198 pub LOG_FORMAT: LogFormat = "telemetry.log_format";
199
200 // --- database ----------------------------------------------------------
201
202 /// The database DSN — `sqlite::memory:`, `mysql://…`, `postgres://…`.
203 pub DATABASE_URL: String = "database.url";
204
205 // --- cache -------------------------------------------------------------
206
207 /// Which cache store to build.
208 pub CACHE_DRIVER: CacheDriver = "cache.driver";
209
210 /// The Redis DSN, or a comma-separated seed list for a cluster.
211 pub CACHE_REDIS_URL: String = "cache.redis_url";
212
213 /// `host:port` of the Memcached server.
214 pub CACHE_MEMCACHED_URL: String = "cache.memcached_url";
215
216 /// Prepended to every cache key, so two applications can share a server.
217 pub CACHE_PREFIX: String = "cache.prefix";
218
219 // --- session -----------------------------------------------------------
220
221 /// Where session state lives.
222 pub SESSION_DRIVER: SessionDriver = "session.driver";
223
224 /// How long a session survives without a request, in seconds.
225 pub SESSION_LIFETIME: i64 = "session.lifetime";
226
227 /// The name of the session cookie.
228 pub SESSION_COOKIE: String = "session.cookie";
229
230 /// Whether the session cookie is `Secure`. Must be true over HTTPS.
231 pub SESSION_SECURE: bool = "session.secure";
232
233 // --- queue -------------------------------------------------------------
234
235 /// Where queued jobs wait.
236 pub QUEUE_DRIVER: QueueDriver = "queue.driver";
237
238 /// The queue a job goes on when it does not name one.
239 pub QUEUE_DEFAULT: String = "queue.default";
240
241 // --- mail --------------------------------------------------------------
242
243 /// Where mail goes.
244 pub MAIL_DRIVER: MailDriver = "mail.driver";
245
246 /// The default `From` address.
247 pub MAIL_FROM_ADDRESS: String = "mail.from.address";
248
249 /// The default `From` display name.
250 pub MAIL_FROM_NAME: String = "mail.from.name";
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use rainier_config::Config;
257
258 #[test]
259 fn a_driver_key_round_trips_as_its_wire_spelling() {
260 let config = Config::new();
261 config.set(CACHE_DRIVER, CacheDriver::RedisCluster).unwrap();
262
263 // What a `.env` would hold, and what a config dump should show.
264 assert_eq!(config.string("cache.driver").as_deref(), Some("redis-cluster"));
265 assert_eq!(config.setting(CACHE_DRIVER).unwrap(), CacheDriver::RedisCluster);
266 }
267
268 #[test]
269 fn every_key_is_under_the_section_its_name_says() {
270 // A key filed under the wrong prefix reads fine and writes somewhere
271 // nothing looks. Cheap to assert, and it has caught a paste already.
272 let pairs: &[(&str, &str)] = &[
273 ("APP", APP_NAME.path()),
274 ("APP", APP_ENV.path()),
275 ("APP", APP_DEBUG.path()),
276 ("APP", APP_URL.path()),
277 ("APP", APP_BASE_PATH.path()),
278 ("SERVER", SERVER_HOST.path()),
279 ("SERVER", SERVER_PORT.path()),
280 ("SERVER", SERVER_MAX_BODY_BYTES.path()),
281 ("SERVER", SERVER_REQUEST_TIMEOUT_SECS.path()),
282 ("SERVER", SERVER_COMPRESSION.path()),
283 ("DATABASE", DATABASE_URL.path()),
284 ("CACHE", CACHE_DRIVER.path()),
285 ("CACHE", CACHE_REDIS_URL.path()),
286 ("CACHE", CACHE_MEMCACHED_URL.path()),
287 ("CACHE", CACHE_PREFIX.path()),
288 ("SESSION", SESSION_DRIVER.path()),
289 ("SESSION", SESSION_LIFETIME.path()),
290 ("SESSION", SESSION_COOKIE.path()),
291 ("SESSION", SESSION_SECURE.path()),
292 ("QUEUE", QUEUE_DRIVER.path()),
293 ("QUEUE", QUEUE_DEFAULT.path()),
294 ("MAIL", MAIL_DRIVER.path()),
295 ("MAIL", MAIL_FROM_ADDRESS.path()),
296 ("MAIL", MAIL_FROM_NAME.path()),
297 ];
298
299 for (section, path) in pairs {
300 assert!(
301 path.starts_with(&format!("{}.", section.to_lowercase())),
302 "`{path}` is filed under the wrong section"
303 );
304 }
305 }
306
307 #[test]
308 fn no_two_keys_name_the_same_path() {
309 let paths = [
310 APP_NAME.path(),
311 APP_ENV.path(),
312 APP_DEBUG.path(),
313 APP_URL.path(),
314 APP_BASE_PATH.path(),
315 SERVER_HOST.path(),
316 SERVER_PORT.path(),
317 SERVER_MAX_BODY_BYTES.path(),
318 SERVER_REQUEST_TIMEOUT_SECS.path(),
319 SERVER_COMPRESSION.path(),
320 DATABASE_URL.path(),
321 CACHE_DRIVER.path(),
322 CACHE_REDIS_URL.path(),
323 CACHE_MEMCACHED_URL.path(),
324 CACHE_PREFIX.path(),
325 SESSION_DRIVER.path(),
326 SESSION_LIFETIME.path(),
327 SESSION_COOKIE.path(),
328 SESSION_SECURE.path(),
329 QUEUE_DRIVER.path(),
330 QUEUE_DEFAULT.path(),
331 MAIL_DRIVER.path(),
332 MAIL_FROM_ADDRESS.path(),
333 MAIL_FROM_NAME.path(),
334 ];
335
336 let unique: std::collections::BTreeSet<_> = paths.iter().collect();
337 assert_eq!(unique.len(), paths.len(), "two keys share a path: {paths:?}");
338 }
339}