1use 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#[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#[derive(Debug, Default)]
24struct CliInputs {
25 host: Option<String>,
26 port: Option<String>,
27 snapshot_path: Option<String>,
28 restore_from: Option<String>,
29 wal_dir: Option<String>,
30 wal_sync_mode: Option<String>,
31}
32
33impl CliInputs {
34 fn set(&mut self, field: CliField, value: String) {
35 match field {
36 CliField::Host => self.host = Some(value),
37 CliField::Port => self.port = Some(value),
38 CliField::SnapshotPath => self.snapshot_path = Some(value),
39 CliField::RestoreFrom => self.restore_from = Some(value),
40 CliField::WalDir => self.wal_dir = Some(value),
41 CliField::WalSyncMode => self.wal_sync_mode = Some(value),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy)]
47enum CliField {
48 Host,
49 Port,
50 SnapshotPath,
51 RestoreFrom,
52 WalDir,
53 WalSyncMode,
54}
55
56enum ParsedCli {
57 Run(CliInputs),
58 Immediate(ConfigOutcome),
59}
60
61pub fn resolve<I>(args: I, env: EnvInputs) -> Result<ConfigOutcome, ConfigError>
66where
67 I: IntoIterator<Item = String>,
68{
69 let cli = match parse_cli_args(args)? {
70 ParsedCli::Run(cli) => cli,
71 ParsedCli::Immediate(outcome) => return Ok(outcome),
72 };
73
74 let host = cli
75 .host
76 .or(env.host)
77 .unwrap_or_else(|| DEFAULT_HOST.to_string());
78 if host.trim().is_empty() {
79 return Err(ConfigError::EmptyValue("--host"));
80 }
81
82 let port = match cli.port.or(env.port) {
83 Some(raw) => parse_port(&raw)?,
84 None => DEFAULT_PORT,
85 };
86
87 let snapshot_path = cli
88 .snapshot_path
89 .or(env.snapshot_path)
90 .and_then(non_empty_path);
91 let restore_from = cli.restore_from.and_then(non_empty_path);
92 let wal_dir = cli.wal_dir.or(env.wal_dir).and_then(non_empty_path);
93 let wal_sync_mode = match cli.wal_sync_mode.or(env.wal_sync_mode) {
94 Some(raw) => parse_sync_mode(&raw)?,
95 None => SyncMode::default(),
96 };
97
98 Ok(ConfigOutcome::Run(ServerConfig {
99 host,
100 port,
101 snapshot_path,
102 restore_from,
103 wal_dir,
104 wal_sync_mode,
105 }))
106}
107
108fn parse_cli_args<I>(args: I) -> Result<ParsedCli, ConfigError>
109where
110 I: IntoIterator<Item = String>,
111{
112 let mut iter = args.into_iter();
113 let _program = iter.next();
114
115 let mut cli = CliInputs::default();
116
117 while let Some(arg) = iter.next() {
118 if arg == "--help" {
119 return Ok(ParsedCli::Immediate(ConfigOutcome::Help(help_text())));
120 }
121 if arg == "--version" {
122 return Ok(ParsedCli::Immediate(ConfigOutcome::Version(version_text())));
123 }
124
125 if let Some((field, flag)) = cli_field_for_flag(&arg) {
126 let value = iter.next().ok_or(ConfigError::MissingValue(flag))?;
127 cli.set(field, value);
128 continue;
129 }
130
131 if let Some((field, value)) = cli_field_for_equals(&arg) {
132 cli.set(field, value.to_string());
133 continue;
134 }
135
136 if arg.starts_with("--") {
137 return Err(ConfigError::UnknownArg(arg));
138 }
139 return Err(ConfigError::UnexpectedPositional(arg));
140 }
141
142 Ok(ParsedCli::Run(cli))
143}
144
145fn cli_field_for_flag(arg: &str) -> Option<(CliField, &'static str)> {
146 match arg {
147 "--host" => Some((CliField::Host, "--host")),
148 "--port" => Some((CliField::Port, "--port")),
149 "--snapshot-path" => Some((CliField::SnapshotPath, "--snapshot-path")),
150 "--restore-from" => Some((CliField::RestoreFrom, "--restore-from")),
151 "--wal-dir" => Some((CliField::WalDir, "--wal-dir")),
152 "--wal-sync-mode" => Some((CliField::WalSyncMode, "--wal-sync-mode")),
153 _ => None,
154 }
155}
156
157fn cli_field_for_equals(arg: &str) -> Option<(CliField, &str)> {
158 let (flag, value) = arg.split_once('=')?;
159 let (field, _) = cli_field_for_flag(flag)?;
160 Some((field, value))
161}
162
163pub fn resolve_from_process() -> Result<ConfigOutcome, ConfigError> {
165 resolve(
166 std::env::args(),
167 EnvInputs {
168 host: std::env::var(HOST_ENV).ok(),
169 port: std::env::var(PORT_ENV).ok(),
170 snapshot_path: std::env::var(SNAPSHOT_PATH_ENV).ok(),
171 wal_dir: std::env::var(WAL_DIR_ENV).ok(),
172 wal_sync_mode: std::env::var(WAL_SYNC_MODE_ENV).ok(),
173 },
174 )
175}
176
177fn non_empty_path(p: String) -> Option<std::path::PathBuf> {
178 if p.trim().is_empty() {
179 None
180 } else {
181 Some(std::path::PathBuf::from(p))
182 }
183}
184
185fn parse_port(raw: &str) -> Result<u16, ConfigError> {
186 let trimmed = raw.trim();
187 if trimmed.is_empty() {
188 return Err(ConfigError::EmptyValue("--port"));
189 }
190 trimmed
191 .parse::<u16>()
192 .map_err(|e| ConfigError::InvalidPort {
193 value: raw.to_string(),
194 reason: e.to_string(),
195 })
196}
197
198fn parse_sync_mode(raw: &str) -> Result<SyncMode, ConfigError> {
199 match raw.trim().to_ascii_lowercase().as_str() {
200 "group-sync" | "group_sync" | "groupsync" => Ok(SyncMode::GroupSync {
201 interval_ms: 50,
206 }),
207 other => Err(ConfigError::InvalidSyncMode(other.to_string())),
208 }
209}