Skip to main content

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}