vane_core/config/mod.rs
1//! Config loading entry point.
2//!
3//! See [`spec/crates/core.md` § _Config layers_](../../../../spec/crates/core.md#config-layers).
4//! Module responsibilities:
5//!
6//! 1. Best-effort `dotenvy` load of `<config_dir>/.env`. **OS env wins**
7//! — `dotenvy::from_path` does not override pre-existing keys, which
8//! matches operator expectations (systemd / supervisor unit files
9//! are authoritative).
10//! 2. Scan `<config_dir>/rules/*.json` for [`RawRuleFile`]s.
11//! 3. Read every `VANE_*` deployment constant into a typed [`Env`]
12//! snapshot.
13//!
14// TODO(config-json): parse `<config_dir>/config.json` (global daemon
15// settings — listeners, management, wasm pool config). Today this file
16// is silently ignored; the daemon's startup wires those values directly
17// from the env layer instead.
18
19mod env;
20mod loader;
21
22pub use env::{Env, EnvReader, ProcessEnv};
23pub use loader::scan_rules_dir;
24
25use std::path::Path;
26
27use crate::compile::merge::RawRuleFile;
28use crate::error::Error;
29
30/// Result of [`load`]: rule files (unmerged) plus the typed `Env`
31/// snapshot. Downstream callers thread `files` into
32/// [`crate::compile::compile`] and read `env` for deployment constants.
33#[derive(Debug, Clone)]
34pub struct LoadedConfig {
35 pub files: Vec<RawRuleFile>,
36 pub env: Env,
37}
38
39/// Load a vane config directory.
40///
41/// Order of operations:
42///
43/// 1. If `<config_dir>/.env` exists, run `dotenvy::from_path` on it.
44/// **Pre-existing OS env keys win** — operators who set values via
45/// systemd / `EnvironmentFile=` / docker `-e` flag override what's
46/// in `.env`. A missing `.env` is not an error; many deployments
47/// rely entirely on OS-level env.
48/// 2. Scan `<config_dir>/rules/*.json` via [`scan_rules_dir`].
49/// 3. Read `VANE_*` deployment constants into [`Env`].
50///
51/// # Errors
52/// - `<config_dir>/rules/` does not exist or is not a directory
53/// (propagated from [`scan_rules_dir`]).
54/// - Any `.json` under `rules/` fails to parse as `RawRuleFile`.
55/// - Any `VANE_*` env var has an invalid value (non-integer, not
56/// `"0"`/`"1"` for booleans, malformed `SocketAddr`, etc.).
57///
58/// **Not** an error:
59/// - `.env` file is missing.
60/// - `<config_dir>/config.json` is missing or malformed (it is not
61/// parsed at this stage).
62pub fn load(config_dir: &Path) -> Result<LoadedConfig, Error> {
63 let env_path = config_dir.join(".env");
64 if env_path.is_file() {
65 // `.env` is an optional operator override — a missing file is
66 // normal and must not produce noise. The `.is_file()` guard
67 // handles the common case; the `NotFound` arm below covers the
68 // race where the file disappears between the guard and the open.
69 // Other failures (malformed syntax, permission denied) are real
70 // problems the operator should see.
71 match dotenvy::from_path(&env_path) {
72 Ok(()) => {}
73 Err(dotenvy::Error::Io(ref io_err)) if io_err.kind() == std::io::ErrorKind::NotFound => {}
74 Err(e) => {
75 tracing::warn!(
76 path = %env_path.display(),
77 error = %e,
78 ".env parse failed; using OS env only",
79 );
80 }
81 }
82 }
83
84 let rules_dir = config_dir.join("rules");
85 let files = scan_rules_dir(&rules_dir)?;
86 let env = Env::from_process_env(config_dir)?;
87
88 Ok(LoadedConfig { files, env })
89}