1pub mod parser;
10
11use crate::config::Schema;
12
13pub trait FdlArgsTrait: Sized {
20 fn parse() -> Self {
22 let args: Vec<String> = std::env::args().collect();
23 match Self::try_parse_from(&args) {
24 Ok(t) => t,
25 Err(msg) => {
26 eprintln!("{msg}");
27 std::process::exit(2);
28 }
29 }
30 }
31
32 fn try_parse_from(args: &[String]) -> Result<Self, String>;
35
36 fn schema() -> Schema;
38
39 fn render_help() -> String;
41}
42
43pub fn parse_or_schema<T: FdlArgsTrait>() -> T {
54 let argv: Vec<String> = std::env::args().collect();
55 parse_or_schema_from::<T>(&argv)
56}
57
58pub fn parse_or_schema_from<T: FdlArgsTrait>(argv: &[String]) -> T {
64 if argv.iter().any(|a| a == "--fdl-schema") {
65 let schema = T::schema();
66 let json = serde_json::to_string_pretty(&schema)
67 .expect("Schema serializes cleanly by construction");
68 println!("{json}");
69 std::process::exit(0);
70 }
71 if argv.iter().any(|a| a == "--help" || a == "-h") {
72 println!("{}", T::render_help());
73 std::process::exit(0);
74 }
75 match T::try_parse_from(argv) {
76 Ok(t) => t,
77 Err(msg) => {
78 eprintln!("{msg}");
79 eprintln!();
80 eprintln!("{}", T::render_help());
81 std::process::exit(2);
82 }
83 }
84}
85
86#[cfg(test)]
87mod env_tests {
88 use std::sync::{Mutex, MutexGuard};
96
97 use crate::args::FdlArgsTrait;
98 use crate::FdlArgs;
99
100 static ENV_LOCK: Mutex<()> = Mutex::new(());
104
105 fn env_lock() -> MutexGuard<'static, ()> {
106 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
107 }
108
109 fn mk_args(xs: &[&str]) -> Vec<String> {
110 xs.iter().map(|s| s.to_string()).collect()
111 }
112
113 struct EnvGuard(&'static str);
116 impl EnvGuard {
117 fn set(name: &'static str, value: &str) -> Self {
118 unsafe { std::env::set_var(name, value); }
121 EnvGuard(name)
122 }
123 }
124 impl Drop for EnvGuard {
125 fn drop(&mut self) {
126 unsafe { std::env::remove_var(self.0); }
127 }
128 }
129
130 #[derive(FdlArgs, Debug)]
132 struct OptArgs {
133 #[option(env = "FDL_TEST_PORT")]
135 port: Option<u16>,
136 }
137
138 #[test]
139 fn env_fills_absent_option() {
140 let _lock = env_lock();
141 let _g = EnvGuard::set("FDL_TEST_PORT", "8080");
142 let cli: OptArgs = OptArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
143 assert_eq!(cli.port, Some(8080));
144 }
145
146 #[test]
147 fn argv_flag_beats_env() {
148 let _lock = env_lock();
149 let _g = EnvGuard::set("FDL_TEST_PORT", "8080");
150 let cli: OptArgs =
151 OptArgs::try_parse_from(&mk_args(&["prog", "--port", "9999"])).unwrap();
152 assert_eq!(cli.port, Some(9999));
153 }
154
155 #[test]
156 fn equals_form_beats_env() {
157 let _lock = env_lock();
158 let _g = EnvGuard::set("FDL_TEST_PORT", "8080");
159 let cli: OptArgs =
160 OptArgs::try_parse_from(&mk_args(&["prog", "--port=9999"])).unwrap();
161 assert_eq!(cli.port, Some(9999));
162 }
163
164 #[test]
165 fn empty_env_falls_through() {
166 let _lock = env_lock();
167 let _g = EnvGuard::set("FDL_TEST_PORT", "");
168 let cli: OptArgs = OptArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
169 assert_eq!(cli.port, None);
170 }
171
172 #[derive(FdlArgs, Debug)]
174 struct ScalarArgs {
175 #[option(default = "3", env = "FDL_TEST_RETRIES")]
177 retries: u32,
178 }
179
180 #[test]
181 fn env_overrides_default_on_scalar() {
182 let _lock = env_lock();
183 let _g = EnvGuard::set("FDL_TEST_RETRIES", "7");
184 let cli: ScalarArgs = ScalarArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
185 assert_eq!(cli.retries, 7);
186 }
187
188 #[test]
189 fn argv_beats_env_beats_default_on_scalar() {
190 let _lock = env_lock();
191 let _g = EnvGuard::set("FDL_TEST_RETRIES", "7");
192 let cli: ScalarArgs =
193 ScalarArgs::try_parse_from(&mk_args(&["prog", "--retries", "42"])).unwrap();
194 assert_eq!(cli.retries, 42);
195 }
196
197 #[derive(FdlArgs, Debug)]
199 struct ChoiceArgs {
200 #[option(choices = &["a", "b"], env = "FDL_TEST_CHOICE")]
202 pick: Option<String>,
203 }
204
205 #[test]
206 fn env_value_is_validated_against_choices() {
207 let _lock = env_lock();
208 let _g = EnvGuard::set("FDL_TEST_CHOICE", "z"); let err = ChoiceArgs::try_parse_from(&mk_args(&["prog"])).unwrap_err();
210 assert!(
211 err.contains("invalid value") && err.contains("z") && err.contains("allowed:"),
212 "env-sourced invalid choice should error like an argv one; got: {err}"
213 );
214 }
215
216 #[test]
217 fn env_valid_choice_accepted() {
218 let _lock = env_lock();
219 let _g = EnvGuard::set("FDL_TEST_CHOICE", "a");
220 let cli: ChoiceArgs = ChoiceArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
221 assert_eq!(cli.pick.as_deref(), Some("a"));
222 }
223
224 #[derive(FdlArgs, Debug)]
226 struct ShortArgs {
227 #[option(short = 'p', env = "FDL_TEST_SHORT")]
229 port: Option<u16>,
230 }
231
232 #[test]
233 fn short_form_suppresses_env_fallback() {
234 let _lock = env_lock();
235 let _g = EnvGuard::set("FDL_TEST_SHORT", "8080");
236 let cli: ShortArgs =
237 ShortArgs::try_parse_from(&mk_args(&["prog", "-p", "9999"])).unwrap();
238 assert_eq!(cli.port, Some(9999));
239 }
240}
241