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