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 {
49 let argv: Vec<String> = std::env::args().collect();
50 parse_or_schema_from::<T>(&argv)
51}
52
53pub fn parse_or_schema_from<T: FdlArgsTrait>(argv: &[String]) -> T {
59 if argv.iter().any(|a| a == "--fdl-schema") {
60 let schema = T::schema();
61 let json = serde_json::to_string_pretty(&schema)
62 .expect("Schema serializes cleanly by construction");
63 println!("{json}");
64 std::process::exit(0);
65 }
66 if argv.iter().any(|a| a == "--help" || a == "-h") {
67 println!("{}", T::render_help());
68 std::process::exit(0);
69 }
70 match T::try_parse_from(argv) {
71 Ok(t) => t,
72 Err(msg) => {
73 eprintln!("{msg}");
74 std::process::exit(2);
75 }
76 }
77}
78
79#[cfg(test)]
80mod env_tests {
81 use std::sync::{Mutex, MutexGuard};
89
90 use crate::args::FdlArgsTrait;
91 use crate::FdlArgs;
92
93 static ENV_LOCK: Mutex<()> = Mutex::new(());
97
98 fn env_lock() -> MutexGuard<'static, ()> {
99 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
100 }
101
102 fn mk_args(xs: &[&str]) -> Vec<String> {
103 xs.iter().map(|s| s.to_string()).collect()
104 }
105
106 struct EnvGuard(&'static str);
109 impl EnvGuard {
110 fn set(name: &'static str, value: &str) -> Self {
111 unsafe { std::env::set_var(name, value); }
114 EnvGuard(name)
115 }
116 }
117 impl Drop for EnvGuard {
118 fn drop(&mut self) {
119 unsafe { std::env::remove_var(self.0); }
120 }
121 }
122
123 #[derive(FdlArgs, Debug)]
125 struct OptArgs {
126 #[option(env = "FDL_TEST_PORT")]
128 port: Option<u16>,
129 }
130
131 #[test]
132 fn env_fills_absent_option() {
133 let _lock = env_lock();
134 let _g = EnvGuard::set("FDL_TEST_PORT", "8080");
135 let cli: OptArgs = OptArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
136 assert_eq!(cli.port, Some(8080));
137 }
138
139 #[test]
140 fn argv_flag_beats_env() {
141 let _lock = env_lock();
142 let _g = EnvGuard::set("FDL_TEST_PORT", "8080");
143 let cli: OptArgs =
144 OptArgs::try_parse_from(&mk_args(&["prog", "--port", "9999"])).unwrap();
145 assert_eq!(cli.port, Some(9999));
146 }
147
148 #[test]
149 fn equals_form_beats_env() {
150 let _lock = env_lock();
151 let _g = EnvGuard::set("FDL_TEST_PORT", "8080");
152 let cli: OptArgs =
153 OptArgs::try_parse_from(&mk_args(&["prog", "--port=9999"])).unwrap();
154 assert_eq!(cli.port, Some(9999));
155 }
156
157 #[test]
158 fn empty_env_falls_through() {
159 let _lock = env_lock();
160 let _g = EnvGuard::set("FDL_TEST_PORT", "");
161 let cli: OptArgs = OptArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
162 assert_eq!(cli.port, None);
163 }
164
165 #[derive(FdlArgs, Debug)]
167 struct ScalarArgs {
168 #[option(default = "3", env = "FDL_TEST_RETRIES")]
170 retries: u32,
171 }
172
173 #[test]
174 fn env_overrides_default_on_scalar() {
175 let _lock = env_lock();
176 let _g = EnvGuard::set("FDL_TEST_RETRIES", "7");
177 let cli: ScalarArgs = ScalarArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
178 assert_eq!(cli.retries, 7);
179 }
180
181 #[test]
182 fn argv_beats_env_beats_default_on_scalar() {
183 let _lock = env_lock();
184 let _g = EnvGuard::set("FDL_TEST_RETRIES", "7");
185 let cli: ScalarArgs =
186 ScalarArgs::try_parse_from(&mk_args(&["prog", "--retries", "42"])).unwrap();
187 assert_eq!(cli.retries, 42);
188 }
189
190 #[derive(FdlArgs, Debug)]
192 struct ChoiceArgs {
193 #[option(choices = &["a", "b"], env = "FDL_TEST_CHOICE")]
195 pick: Option<String>,
196 }
197
198 #[test]
199 fn env_value_is_validated_against_choices() {
200 let _lock = env_lock();
201 let _g = EnvGuard::set("FDL_TEST_CHOICE", "z"); let err = ChoiceArgs::try_parse_from(&mk_args(&["prog"])).unwrap_err();
203 assert!(
204 err.contains("invalid value") && err.contains("z") && err.contains("allowed:"),
205 "env-sourced invalid choice should error like an argv one; got: {err}"
206 );
207 }
208
209 #[test]
210 fn env_valid_choice_accepted() {
211 let _lock = env_lock();
212 let _g = EnvGuard::set("FDL_TEST_CHOICE", "a");
213 let cli: ChoiceArgs = ChoiceArgs::try_parse_from(&mk_args(&["prog"])).unwrap();
214 assert_eq!(cli.pick.as_deref(), Some("a"));
215 }
216
217 #[derive(FdlArgs, Debug)]
219 struct ShortArgs {
220 #[option(short = 'p', env = "FDL_TEST_SHORT")]
222 port: Option<u16>,
223 }
224
225 #[test]
226 fn short_form_suppresses_env_fallback() {
227 let _lock = env_lock();
228 let _g = EnvGuard::set("FDL_TEST_SHORT", "8080");
229 let cli: ShortArgs =
230 ShortArgs::try_parse_from(&mk_args(&["prog", "-p", "9999"])).unwrap();
231 assert_eq!(cli.port, Some(9999));
232 }
233}
234