Skip to main content

larust_core/
config.rs

1use crate::AppError;
2use serde::Deserialize;
3use std::sync::OnceLock;
4
5static CONFIG: OnceLock<Config> = OnceLock::new();
6
7#[derive(Debug, Clone, Deserialize)]
8pub struct Config {
9    #[serde(default = "default_app_name")]
10    pub app_name: String,
11    #[serde(default = "default_app_env")]
12    pub app_env: String,
13    #[serde(default = "default_app_port")]
14    pub app_port: u16,
15    /// Whether the session cookie carries the `Secure` attribute. Defaults
16    /// to `true` (safe over any real deployment). Browsers only treat
17    /// loopback addresses and the literal name `localhost` as secure
18    /// contexts over plain HTTP - a custom local dev hostname (e.g. a
19    /// `.test` domain resolved via `/etc/hosts`, even one that points at
20    /// 127.0.0.1) is not on that list, so the `Secure` cookie is silently
21    /// dropped by the browser and sessions/CSRF stop working with no error
22    /// surfaced anywhere. Set `SESSION_SECURE_COOKIE=false` for that case.
23    #[serde(default = "default_session_secure_cookie")]
24    pub session_secure_cookie: bool,
25    /// Gates descriptive error pages (the full error message and source
26    /// chain, rendered as HTML) and panic details. Defaults to `false` -
27    /// safe if unset, so a deployment missing both `.env` and its own
28    /// `config/app.rs`'s `APP_DEBUG` handling never leaks internals by
29    /// accident. Scaffolded apps ship `APP_DEBUG=true` in their own
30    /// `.env` for local dev, mirroring Laravel's own scaffold convention.
31    #[serde(default = "default_app_debug")]
32    pub app_debug: bool,
33    /// The app's own base URL, for `larust_support::url()`/`asset()` to
34    /// build absolute URLs from a relative path. Defaults to
35    /// `"http://localhost"` - matching Laravel's own scaffolded default
36    /// exactly (no port; most local dev never needs `url()` to be
37    /// port-precise). Set `APP_URL` for anything that does.
38    #[serde(default = "default_app_url")]
39    pub app_url: String,
40    /// `larust_lang::current_locale`'s own default when no per-request
41    /// override is set (see that crate's own doc comment) - Laravel's
42    /// `config('app.locale')`/`APP_LOCALE`. Defaults to `"en"`.
43    #[serde(default = "default_app_locale")]
44    pub app_locale: String,
45    /// `larust_lang::t`/`t_with`'s fallback when a key is missing from the
46    /// current locale's own translation file - Laravel's
47    /// `config('app.fallback_locale')`/`APP_FALLBACK_LOCALE`. Defaults to
48    /// `"en"`, same as [`app_locale`](Self::app_locale) - most apps only
49    /// ever have one locale until they add a second, at which point this
50    /// is usually still the original one.
51    #[serde(default = "default_app_fallback_locale")]
52    pub app_fallback_locale: String,
53    /// Where `routes/api.rs` gets mounted (`main.rs`'s
54    /// `.group(&config.api_prefix, ...)` call) - Laravel's own
55    /// `routes/api.php` is likewise served under a configurable prefix
56    /// (`RouteServiceProvider`'s `apiPrefix`), not a fixed one. Defaults to
57    /// `"/api"`.
58    #[serde(default = "default_api_prefix")]
59    pub api_prefix: String,
60    /// `"log"` (default) writes a mail's rendered subject/body to
61    /// `tracing::info!` instead of sending it - no network touched, no
62    /// SMTP server needed for local dev or `cargo test`, matching
63    /// Laravel's own `MAIL_MAILER=log` scaffold default exactly. `"smtp"`
64    /// sends for real, using the fields below.
65    #[serde(default = "default_mail_driver")]
66    pub mail_driver: String,
67    #[serde(default = "default_mail_host")]
68    pub mail_host: String,
69    #[serde(default = "default_mail_port")]
70    pub mail_port: u16,
71    /// Empty string means "unset" - `Config` has no `Option<T>` field
72    /// precedent elsewhere, and the `log` driver (the default) never
73    /// reads these anyway.
74    #[serde(default = "default_mail_username")]
75    pub mail_username: String,
76    #[serde(default = "default_mail_password")]
77    pub mail_password: String,
78    #[serde(default = "default_mail_encryption")]
79    pub mail_encryption: String,
80    #[serde(default = "default_mail_from_address")]
81    pub mail_from_address: String,
82    /// Falls back to `app_name` if unset, matching Laravel's own
83    /// `MAIL_FROM_NAME="${APP_NAME}"` scaffold default.
84    #[serde(default)]
85    pub mail_from_name: String,
86    /// `"database"` (default) stores `larust-cache`'s entries in
87    /// `cache_items`, the same SQL-family table it has always used.
88    /// `"redis"` stores them in Redis instead - see `larust-cache::store`'s
89    /// own doc comment for the split. Same `mail_driver`-shaped "a plain
90    /// string picks a runtime code path" convention, not a typed enum:
91    /// `larust-core` has no `sqlx`/`redis` dependency and shouldn't need
92    /// one just to name a driver.
93    #[serde(default = "default_cache_driver")]
94    pub cache_driver: String,
95    /// `larust-queue`'s own driver toggle - see [`cache_driver`](Self::cache_driver)'s
96    /// own doc comment for the shape and reasoning; independent of it
97    /// (an app can mix a database-backed cache with a Redis-backed queue,
98    /// or vice versa).
99    #[serde(default = "default_queue_driver")]
100    pub queue_driver: String,
101    /// `"stdout"` (default) - everything goes to the terminal via
102    /// `tracing_subscriber::fmt`, exactly as before this field existed.
103    /// `"file"` writes to `storage/logs/larust.log` instead (rotated - see
104    /// [`log_max_size`](Self::log_max_size)); `"stack"` writes to both, the
105    /// same "combine channels" meaning Laravel's own `LOG_CHANNEL=stack`
106    /// has. Laravel additionally supports `single`/`daily`/`slack`/etc.
107    /// channels this doesn't attempt - see `crate::logging`'s own module
108    /// doc comment for why size-based rotation was the one strategy
109    /// actually built.
110    #[serde(default = "default_log_channel")]
111    pub log_channel: String,
112    /// Empty string means "unset" (same convention as
113    /// [`mail_username`](Self::mail_username)) - falls back to this
114    /// crate's own historical behavior: `debug,sqlx=warn,tower_sessions=warn`
115    /// in `app_env == "local"`, `info` otherwise. Set to a plain level name
116    /// (`trace`/`debug`/`info`/`warn`/`error`) to override that for every
117    /// crate at once; `sqlx`/`tower_sessions` are still individually capped
118    /// at `warn` even then, for the same reason `init_logging`'s own
119    /// hardcoded default already did - see `crate::logging`'s doc comment.
120    /// `RUST_LOG`, if set, wins over this unconditionally, same as it
121    /// already won over the old hardcoded default.
122    #[serde(default)]
123    pub log_level: String,
124    /// Bytes - once `storage/logs/larust.log` reaches this size, it's
125    /// rotated to `larust.log.1` (shifting any existing `.1`/`.2`/... down
126    /// one) and a fresh, empty file is started. Only consulted when
127    /// [`log_channel`](Self::log_channel) is `"file"`/`"stack"`. Defaults
128    /// to 10 MiB.
129    #[serde(default = "default_log_max_size")]
130    pub log_max_size: u64,
131    /// How many rotated backups (`larust.log.1` through `larust.log.{N}`)
132    /// to keep before the oldest is deleted outright. `0` means "delete
133    /// immediately on rotation, keep only the current file." Defaults to
134    /// `5`.
135    #[serde(default = "default_log_keep_files")]
136    pub log_keep_files: u32,
137    /// `"web"` (default) - an ordinary server, deployed via `xr deploy`'s
138    /// build-and-restart-handoff path. `"app"` - a Tauri desktop build,
139    /// where the app's own `Application`/router is spawned in-process and
140    /// a native webview points at it locally instead of a browser
141    /// connecting over the network. Read by `xr deploy`/`xr new` (from
142    /// `.env`, not through this struct - see those crates' own doc
143    /// comments for why), not by this crate itself; kept on `Config`
144    /// mainly so app code (e.g. a template wanting to render different
145    /// chrome for a desktop build) can read `app.config().deploy_type`
146    /// like any other setting.
147    #[serde(default = "default_deploy_type")]
148    pub deploy_type: String,
149    /// `"database"` (default) - sessions live in whatever `DB_CONNECTION`
150    /// points at, via `larust_http::session::AnySessionStore`. `"file"` -
151    /// one file per session under `storage/sessions/`
152    /// (`larust_http::session::FileSessionStore`), Laravel's own
153    /// `SESSION_DRIVER=file` equivalent: real persistence across a
154    /// restart/crash (unlike an in-memory store, which this framework
155    /// deliberately never offers - see `larust_http::session`'s own module
156    /// doc comment), just not through the app's database. A genuinely
157    /// reasonable choice for a single-server app that would rather not put
158    /// session churn through its own database at all - see
159    /// `docs/the-basics/middleware-sessions-and-csrf.md`. Any other value
160    /// is treated the same as an unset `LOG_CHANNEL` (see that field's own
161    /// doc comment on the identical "unrecognized degrades to the safe
162    /// default" precedent) - falls back to `"database"`, but (unlike
163    /// `LOG_CHANNEL`) `Application::new()` warns about it first, since a
164    /// silently-ignored `SESSION_DRIVER` value was a real, reported
165    /// production confusion before that warning existed.
166    #[serde(default = "default_session_driver")]
167    pub session_driver: String,
168}
169
170fn default_session_driver() -> String {
171    "database".to_string()
172}
173
174fn default_app_name() -> String {
175    "Larust".to_string()
176}
177
178fn default_app_env() -> String {
179    "local".to_string()
180}
181
182// 34187 - a small "WALBY" easter egg (a nod to Wallaby Designs), and
183// the practical reason: far less likely to already be taken by something
184// else on a dev machine than the extremely common 8000/3000/5000 range.
185fn default_app_port() -> u16 {
186    34187
187}
188
189fn default_session_secure_cookie() -> bool {
190    true
191}
192
193fn default_app_debug() -> bool {
194    false
195}
196
197fn default_app_url() -> String {
198    "http://localhost".to_string()
199}
200
201fn default_app_locale() -> String {
202    "en".to_string()
203}
204
205fn default_app_fallback_locale() -> String {
206    "en".to_string()
207}
208
209fn default_api_prefix() -> String {
210    "/api".to_string()
211}
212
213fn default_mail_driver() -> String {
214    "log".to_string()
215}
216
217fn default_mail_host() -> String {
218    "127.0.0.1".to_string()
219}
220
221fn default_mail_port() -> u16 {
222    587
223}
224
225fn default_mail_username() -> String {
226    String::new()
227}
228
229fn default_mail_password() -> String {
230    String::new()
231}
232
233fn default_mail_encryption() -> String {
234    "tls".to_string()
235}
236
237fn default_mail_from_address() -> String {
238    "hello@example.com".to_string()
239}
240
241fn default_cache_driver() -> String {
242    "database".to_string()
243}
244
245fn default_queue_driver() -> String {
246    "database".to_string()
247}
248
249fn default_deploy_type() -> String {
250    "web".to_string()
251}
252
253fn default_log_channel() -> String {
254    "stdout".to_string()
255}
256
257fn default_log_max_size() -> u64 {
258    10 * 1024 * 1024
259}
260
261fn default_log_keep_files() -> u32 {
262    5
263}
264
265impl Config {
266    /// Builds `Config` from `value` - the `serde_json::Value` an app's own
267    /// generated `config/app.rs` (`pub fn config() -> Value`) produces. A
268    /// single `serde_json::from_value` call: `Config` already derives
269    /// `Deserialize` with a `#[serde(default = ...)]` per field, which
270    /// works identically regardless of the source `Deserializer` (this
271    /// used to be TOML, read from `config/app.toml` - see this crate's
272    /// git history), so no manual field-by-field extraction is needed
273    /// here. Env-var override capability (Laravel's own "config file sets
274    /// a default, `.env` can override it" behavior) lives entirely in the
275    /// generated `config/app.rs`'s own `env_or`/`env_bool` calls now -
276    /// this function has no knowledge of environment variables at all,
277    /// unlike the TOML-era `load_from` it replaced.
278    pub fn from_value(value: &serde_json::Value) -> Result<Self, AppError> {
279        serde_json::from_value(value.clone()).map_err(|source| AppError::Config(Box::new(source)))
280    }
281
282    /// Stores `self` as the process-wide config (`config()` below reads it
283    /// back) - called once, from `Application::new()`, right after
284    /// `load()` succeeds. A second call (e.g. `Application::new()` running
285    /// more than once in the same process, such as a test suite exercising
286    /// several `APP_URL`/`APP_ENV` values) doesn't panic or overwrite -
287    /// `OnceLock` can only be set once - but every `url()`/`asset()`/
288    /// `larust_support::config()` call afterward keeps resolving against
289    /// the *first* call's values, silently wrong rather than reflecting
290    /// what the second `Application::new()` actually loaded. Worth
291    /// surfacing rather than swallowing outright, matching
292    /// `larust_http::route::publish_route_names`'s identical
293    /// first-writer-wins tradeoff.
294    pub(crate) fn publish(self) {
295        if CONFIG.set(self).is_err() {
296            tracing::warn!(
297                "Application::new() called more than once in this process; \
298                 config(), url(), and asset() still use the first call's values"
299            );
300        }
301    }
302}
303
304/// Returns the process-wide config `Application::new()` already loaded -
305/// the same `OnceLock`-backed idiom `larust_orm::pool()` uses for the
306/// connection pool. Unlike `pool()`, this panics rather than returning a
307/// `Result` if called before `Application::new()`: every Larust
308/// entry point calls `Application::new()` as its first line (there's no
309/// analogue to `pool()`'s "forgot to call `connect()` later" scenario -
310/// nothing before `Application::new()` could plausibly need config at
311/// all), so treating this as a real caller-contract violation (like
312/// `abort()`'s own documented panic for an invalid status code) rather
313/// than a `Result` every call site would need to unwrap anyway is the
314/// better fit here.
315///
316/// Shares its name with the unrelated, one-argument
317/// `larust_support::config(key)` (Laravel's stringly-typed
318/// `config('app.name')`) - a `use larust_core::config;` alongside
319/// `use larust_support::config;` in the same file is a duplicate-import
320/// error. Call this one by its full path (`larust_core::config()`, as
321/// every call site in this codebase already does) rather than importing
322/// it bare if a file needs both.
323pub fn config() -> &'static Config {
324    CONFIG
325        .get()
326        .expect("larust_core::config() called before Application::new()")
327}
328
329/// `config()`'s non-panicking twin, for the rare caller that has a
330/// sensible fallback behavior for "no `Application::new()` has run yet"
331/// rather than treating it as a contract violation - e.g.
332/// `larust_http::session`'s cookie-name derivation, which needs to behave
333/// identically whether or not the specific test harness building a router
334/// happened to construct an `Application` first.
335pub fn try_config() -> Option<&'static Config> {
336    CONFIG.get()
337}