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}