Skip to main content

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