Skip to main content

lora_server/config/
env.rs

1//! CLI / env resolution: turns argv + env into a [`ConfigOutcome`].
2
3use lora_database::SyncMode;
4
5use super::errors::ConfigError;
6use super::help::{help_text, version_text};
7use super::{
8    ConfigOutcome, ServerConfig, DEFAULT_HOST, DEFAULT_PORT, HOST_ENV, PORT_ENV, SNAPSHOT_PATH_ENV,
9    WAL_DIR_ENV, WAL_SYNC_MODE_ENV,
10};
11
12/// Inputs to [`resolve`]. Wrapping the env values in a struct keeps the
13/// caller-visible signature stable as new env-driven options are added.
14#[derive(Debug, Default, Clone)]
15pub struct EnvInputs {
16    pub host: Option<String>,
17    pub port: Option<String>,
18    pub snapshot_path: Option<String>,
19    pub wal_dir: Option<String>,
20    pub wal_sync_mode: Option<String>,
21}
22
23/// Resolve a [`ConfigOutcome`] from CLI args and env values.
24///
25/// `args` includes the program name at position 0 (as produced by
26/// [`std::env::args`]); it is skipped internally.
27pub fn resolve<I>(args: I, env: EnvInputs) -> Result<ConfigOutcome, ConfigError>
28where
29    I: IntoIterator<Item = String>,
30{
31    let mut iter = args.into_iter();
32    let _program = iter.next();
33
34    let mut cli_host: Option<String> = None;
35    let mut cli_port: Option<String> = None;
36    let mut cli_snapshot_path: Option<String> = None;
37    let mut cli_restore_from: Option<String> = None;
38    let mut cli_wal_dir: Option<String> = None;
39    let mut cli_wal_sync_mode: Option<String> = None;
40
41    while let Some(arg) = iter.next() {
42        match arg.as_str() {
43            "--help" => return Ok(ConfigOutcome::Help(help_text())),
44            "--version" => return Ok(ConfigOutcome::Version(version_text())),
45            "--host" => {
46                let v = iter.next().ok_or(ConfigError::MissingValue("--host"))?;
47                cli_host = Some(v);
48            }
49            "--port" => {
50                let v = iter.next().ok_or(ConfigError::MissingValue("--port"))?;
51                cli_port = Some(v);
52            }
53            "--snapshot-path" => {
54                let v = iter
55                    .next()
56                    .ok_or(ConfigError::MissingValue("--snapshot-path"))?;
57                cli_snapshot_path = Some(v);
58            }
59            "--restore-from" => {
60                let v = iter
61                    .next()
62                    .ok_or(ConfigError::MissingValue("--restore-from"))?;
63                cli_restore_from = Some(v);
64            }
65            "--wal-dir" => {
66                let v = iter.next().ok_or(ConfigError::MissingValue("--wal-dir"))?;
67                cli_wal_dir = Some(v);
68            }
69            "--wal-sync-mode" => {
70                let v = iter
71                    .next()
72                    .ok_or(ConfigError::MissingValue("--wal-sync-mode"))?;
73                cli_wal_sync_mode = Some(v);
74            }
75            s if s.starts_with("--host=") => {
76                cli_host = Some(s["--host=".len()..].to_string());
77            }
78            s if s.starts_with("--port=") => {
79                cli_port = Some(s["--port=".len()..].to_string());
80            }
81            s if s.starts_with("--snapshot-path=") => {
82                cli_snapshot_path = Some(s["--snapshot-path=".len()..].to_string());
83            }
84            s if s.starts_with("--restore-from=") => {
85                cli_restore_from = Some(s["--restore-from=".len()..].to_string());
86            }
87            s if s.starts_with("--wal-dir=") => {
88                cli_wal_dir = Some(s["--wal-dir=".len()..].to_string());
89            }
90            s if s.starts_with("--wal-sync-mode=") => {
91                cli_wal_sync_mode = Some(s["--wal-sync-mode=".len()..].to_string());
92            }
93            s if s.starts_with("--") => return Err(ConfigError::UnknownArg(arg)),
94            _ => return Err(ConfigError::UnexpectedPositional(arg)),
95        }
96    }
97
98    let host = cli_host
99        .or(env.host)
100        .unwrap_or_else(|| DEFAULT_HOST.to_string());
101    if host.trim().is_empty() {
102        return Err(ConfigError::EmptyValue("--host"));
103    }
104
105    let port = match cli_port.or(env.port) {
106        Some(raw) => parse_port(&raw)?,
107        None => DEFAULT_PORT,
108    };
109
110    let snapshot_path = cli_snapshot_path
111        .or(env.snapshot_path)
112        .and_then(non_empty_path);
113    let restore_from = cli_restore_from.and_then(non_empty_path);
114    let wal_dir = cli_wal_dir.or(env.wal_dir).and_then(non_empty_path);
115    let wal_sync_mode = match cli_wal_sync_mode.or(env.wal_sync_mode) {
116        Some(raw) => parse_sync_mode(&raw)?,
117        None => SyncMode::default(),
118    };
119
120    Ok(ConfigOutcome::Run(ServerConfig {
121        host,
122        port,
123        snapshot_path,
124        restore_from,
125        wal_dir,
126        wal_sync_mode,
127    }))
128}
129
130/// Resolve using the process environment and `std::env::args`.
131pub fn resolve_from_process() -> Result<ConfigOutcome, ConfigError> {
132    resolve(
133        std::env::args(),
134        EnvInputs {
135            host: std::env::var(HOST_ENV).ok(),
136            port: std::env::var(PORT_ENV).ok(),
137            snapshot_path: std::env::var(SNAPSHOT_PATH_ENV).ok(),
138            wal_dir: std::env::var(WAL_DIR_ENV).ok(),
139            wal_sync_mode: std::env::var(WAL_SYNC_MODE_ENV).ok(),
140        },
141    )
142}
143
144fn non_empty_path(p: String) -> Option<std::path::PathBuf> {
145    if p.trim().is_empty() {
146        None
147    } else {
148        Some(std::path::PathBuf::from(p))
149    }
150}
151
152fn parse_port(raw: &str) -> Result<u16, ConfigError> {
153    let trimmed = raw.trim();
154    if trimmed.is_empty() {
155        return Err(ConfigError::EmptyValue("--port"));
156    }
157    trimmed
158        .parse::<u16>()
159        .map_err(|e| ConfigError::InvalidPort {
160            value: raw.to_string(),
161            reason: e.to_string(),
162        })
163}
164
165fn parse_sync_mode(raw: &str) -> Result<SyncMode, ConfigError> {
166    match raw.trim().to_ascii_lowercase().as_str() {
167        "group-sync" | "group_sync" | "groupsync" => Ok(SyncMode::GroupSync {
168            // 50 ms cadence is short enough that a crash window is
169            // bounded by the wallclock budget operators usually quote
170            // ("at most ~50 ms of writes lost") and long enough that
171            // the bg flusher does not tax disks under sustained load.
172            interval_ms: 50,
173        }),
174        other => Err(ConfigError::InvalidSyncMode(other.to_string())),
175    }
176}