1use std::ops::Deref;
4use std::ops::DerefMut;
5use std::path::Path;
6use std::path::PathBuf;
7use std::str::FromStr;
8use std::sync::LazyLock;
9
10use indexmap::IndexMap;
11use regex::Regex;
12use serde_json::Value;
13use thiserror::Error;
14use wdl_analysis::Document;
15use wdl_engine::Inputs as EngineInputs;
16
17pub mod file;
18pub mod origin_paths;
19
20pub use file::InputFile;
21pub use origin_paths::OriginPaths;
22
23static IDENTIFIER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
28 Regex::new(r"^([a-zA-Z][a-zA-Z0-9_.]*)$").unwrap()
30});
31
32static ASSUME_STRING_REGEX: LazyLock<Regex> = LazyLock::new(|| {
37 Regex::new(r"^[\w /~.]*$").unwrap()
39});
40
41#[derive(Error, Debug)]
43pub enum Error {
44 #[error(transparent)]
46 File(#[from] file::Error),
47
48 #[error("file `{0}` was not found")]
50 FileNotFound(PathBuf),
51
52 #[error("invalid key-value pair `{pair}`: {reason}")]
54 InvalidPair {
55 pair: String,
57
58 reason: String,
60 },
61
62 #[error("unable to deserialize `{0}` as a valid WDL value")]
64 Deserialize(String),
65}
66
67pub type Result<T> = std::result::Result<T, Error>;
69
70#[derive(Clone, Debug)]
72pub enum Input {
73 File(
75 PathBuf,
80 ),
81
82 Pair {
84 key: String,
86
87 value: Value,
89 },
90}
91
92impl Input {
93 pub fn as_file(&self) -> Option<&Path> {
99 match self {
100 Input::File(p) => Some(p.as_path()),
101 _ => None,
102 }
103 }
104
105 pub fn into_file(self) -> Option<PathBuf> {
111 match self {
112 Input::File(p) => Some(p),
113 _ => None,
114 }
115 }
116
117 pub fn unwrap_file(self) -> PathBuf {
123 match self {
124 Input::File(p) => p,
125 v => panic!("{v:?} is not an `Input::File`"),
126 }
127 }
128
129 pub fn as_pair(&self) -> Option<(&str, &Value)> {
135 match self {
136 Input::Pair { key, value } => Some((key.as_str(), value)),
137 _ => None,
138 }
139 }
140
141 pub fn into_pair(self) -> Option<(String, Value)> {
147 match self {
148 Input::Pair { key, value } => Some((key, value)),
149 _ => None,
150 }
151 }
152
153 pub fn unwrap_pair(self) -> (String, Value) {
159 match self {
160 Input::Pair { key, value } => (key, value),
161 v => panic!("{v:?} is not an `Input::Pair`"),
162 }
163 }
164}
165
166impl FromStr for Input {
167 type Err = Error;
168
169 fn from_str(s: &str) -> std::result::Result<Self, Error> {
170 match s.split_once("=") {
171 Some((key, value)) => {
172 if !IDENTIFIER_REGEX.is_match(key) {
173 return Err(Error::InvalidPair {
174 pair: s.to_string(),
175 reason: format!(
176 "key `{}` did not match the identifier regex (`{}`)",
177 key,
178 IDENTIFIER_REGEX.as_str()
179 ),
180 });
181 }
182
183 let value = serde_json::from_str(value).or_else(|_| {
184 if ASSUME_STRING_REGEX.is_match(value) {
185 Ok(Value::String(value.to_owned()))
186 } else {
187 Err(Error::Deserialize(value.to_owned()))
188 }
189 })?;
190
191 Ok(Input::Pair {
192 key: key.to_owned(),
193 value,
194 })
195 }
196 None => {
197 let path = PathBuf::from(s);
198
199 if !path.exists() {
200 return Err(Error::FileNotFound(path));
201 }
202
203 Ok(Input::File(path))
204 }
205 }
206 }
207}
208
209type InputsInner = IndexMap<String, (PathBuf, Value)>;
211
212#[derive(Clone, Debug, Default)]
215pub struct Inputs(InputsInner);
216
217impl Inputs {
218 fn add_input(&mut self, input: &str) -> Result<()> {
220 match input.parse::<Input>()? {
221 Input::File(path) => {
222 let inputs = InputFile::read(&path).map_err(Error::File)?;
223 self.extend(inputs.into_inner());
224 }
225 Input::Pair { key, value } => {
226 let cwd = std::env::current_dir().unwrap();
230 self.insert(key, (cwd, value));
231 }
232 };
233
234 Ok(())
235 }
236
237 pub fn coalesce<T, V>(iter: T) -> Result<Self>
239 where
240 T: IntoIterator<Item = V>,
241 V: AsRef<str>,
242 {
243 let mut inputs = Inputs::default();
244
245 for input in iter {
246 inputs.add_input(input.as_ref())?;
247 }
248
249 Ok(inputs)
250 }
251
252 pub fn into_inner(self) -> InputsInner {
254 self.0
255 }
256
257 pub fn into_engine_inputs(
270 self,
271 document: &Document,
272 ) -> anyhow::Result<Option<(String, EngineInputs, OriginPaths)>> {
273 let (origins, values) = self.0.into_iter().fold(
274 (IndexMap::new(), serde_json::Map::new()),
275 |(mut origins, mut values), (key, (origin, value))| {
276 origins.insert(key.clone(), origin);
277 values.insert(key, value);
278 (origins, values)
279 },
280 );
281
282 let result = EngineInputs::parse_object(document, values)?;
283
284 Ok(result.map(|(callee_name, inputs)| {
285 let callee_prefix = format!("{}.", callee_name);
286
287 let origins = origins
288 .into_iter()
289 .map(|(key, path)| {
290 if let Some(key) = key.strip_prefix(&callee_prefix) {
291 (key.to_owned(), path)
292 } else {
293 (key, path)
294 }
295 })
296 .collect::<IndexMap<String, PathBuf>>();
297
298 (callee_name, inputs, OriginPaths::from(origins))
299 }))
300 }
301}
302
303impl Deref for Inputs {
304 type Target = InputsInner;
305
306 fn deref(&self) -> &Self::Target {
307 &self.0
308 }
309}
310
311impl DerefMut for Inputs {
312 fn deref_mut(&mut self) -> &mut Self::Target {
313 &mut self.0
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn identifier_regex() {
323 assert!(IDENTIFIER_REGEX.is_match("here_is_an.identifier"));
324 assert!(!IDENTIFIER_REGEX.is_match("here is not an identifier"));
325 }
326
327 #[test]
328 fn assume_string_regex() {
329 assert!(ASSUME_STRING_REGEX.is_match(""));
331 assert!(ASSUME_STRING_REGEX.is_match("fooBAR082"));
332 assert!(ASSUME_STRING_REGEX.is_match("foo bar baz"));
333
334 assert!(!ASSUME_STRING_REGEX.is_match("[1, a]"));
336 }
337
338 #[test]
339 fn file_parsing() {
340 let input = "./tests/fixtures/inputs_one.json".parse::<Input>().unwrap();
342 assert!(matches!(
343 input,
344 Input::File(path) if path.to_str().unwrap() == "./tests/fixtures/inputs_one.json"
345 ));
346
347 let input = "./tests/fixtures/inputs_three.yml"
349 .parse::<Input>()
350 .unwrap();
351 assert!(matches!(
352 input,
353 Input::File(path) if path.to_str().unwrap() == "./tests/fixtures/inputs_three.yml"
354 ));
355
356 let err = "./tests/fixtures/missing.json"
358 .parse::<Input>()
359 .unwrap_err();
360 assert!(matches!(
361 err,
362 Error::FileNotFound(path) if path.to_str().unwrap() == "./tests/fixtures/missing.json"
363 ));
364 }
365
366 #[test]
367 fn key_value_pair_parsing() {
368 let input = r#"foo="bar""#.parse::<Input>().unwrap();
370 let (key, value) = input.unwrap_pair();
371 assert_eq!(key, "foo");
372 assert_eq!(value.as_str().unwrap(), "bar");
373
374 let input = r#"foo.bar_baz_quux="qil""#.parse::<Input>().unwrap();
376 let (key, value) = input.unwrap_pair();
377 assert_eq!(key, "foo.bar_baz_quux");
378 assert_eq!(value.as_str().unwrap(), "qil");
379
380 let err = r#"foo$="bar""#.parse::<Input>().unwrap_err();
382 assert!(matches!(
383 err,
384 Error::InvalidPair {
385 pair,
386 reason
387 } if pair == r#"foo$="bar""# &&
388 reason == r"key `foo$` did not match the identifier regex (`^([a-zA-Z][a-zA-Z0-9_.]*)$`)"));
389
390 let input = r#"foo="bar$""#.parse::<Input>().unwrap();
392 let (key, value) = input.unwrap_pair();
393 assert_eq!(key, "foo");
394 assert_eq!(value.as_str().unwrap(), "bar$");
395 }
396
397 #[test]
398 fn coalesce() {
399 fn check_string_value(inputs: &Inputs, key: &str, value: &str) {
401 let (_, input) = inputs.get(key).unwrap();
402 assert_eq!(input.as_str().unwrap(), value);
403 }
404
405 fn check_float_value(inputs: &Inputs, key: &str, value: f64) {
406 let (_, input) = inputs.get(key).unwrap();
407 assert_eq!(input.as_f64().unwrap(), value);
408 }
409
410 fn check_boolean_value(inputs: &Inputs, key: &str, value: bool) {
411 let (_, input) = inputs.get(key).unwrap();
412 assert_eq!(input.as_bool().unwrap(), value);
413 }
414
415 fn check_integer_value(inputs: &Inputs, key: &str, value: i64) {
416 let (_, input) = inputs.get(key).unwrap();
417 assert_eq!(input.as_i64().unwrap(), value);
418 }
419
420 let inputs = Inputs::coalesce([
422 "./tests/fixtures/inputs_one.json",
423 "./tests/fixtures/inputs_two.json",
424 "./tests/fixtures/inputs_three.yml",
425 ])
426 .unwrap();
427
428 assert_eq!(inputs.len(), 5);
429 check_string_value(&inputs, "foo", "bar");
430 check_float_value(&inputs, "baz", 128.0);
431 check_string_value(&inputs, "quux", "qil");
432 check_string_value(&inputs, "new.key", "foobarbaz");
433 check_string_value(&inputs, "new_two.key", "bazbarfoo");
434
435 let inputs = Inputs::coalesce([
437 "./tests/fixtures/inputs_three.yml",
438 "./tests/fixtures/inputs_two.json",
439 "./tests/fixtures/inputs_one.json",
440 ])
441 .unwrap();
442
443 assert_eq!(inputs.len(), 5);
444 check_string_value(&inputs, "foo", "bar");
445 check_float_value(&inputs, "baz", 42.0);
446 check_string_value(&inputs, "quux", "qil");
447 check_string_value(&inputs, "new.key", "foobarbaz");
448 check_string_value(&inputs, "new_two.key", "bazbarfoo");
449
450 let inputs = Inputs::coalesce([
452 r#"sandwich=-100"#,
453 "./tests/fixtures/inputs_one.json",
454 "./tests/fixtures/inputs_two.json",
455 r#"quux="jacks""#,
456 "./tests/fixtures/inputs_three.yml",
457 r#"baz=false"#,
458 ])
459 .unwrap();
460
461 assert_eq!(inputs.len(), 6);
462 check_string_value(&inputs, "foo", "bar");
463 check_boolean_value(&inputs, "baz", false);
464 check_string_value(&inputs, "quux", "jacks");
465 check_string_value(&inputs, "new.key", "foobarbaz");
466 check_string_value(&inputs, "new_two.key", "bazbarfoo");
467 check_integer_value(&inputs, "sandwich", -100);
468
469 let error =
471 Inputs::coalesce(["./tests/fixtures/inputs_one.json", "foo=baz#bar"]).unwrap_err();
472 assert!(matches!(
473 error,
474 Error::Deserialize(value) if value == "baz#bar"
475 ));
476
477 let error = Inputs::coalesce([
479 "./tests/fixtures/inputs_one.json",
480 "./tests/fixtures/inputs_two.json",
481 "./tests/fixtures/inputs_three.yml",
482 "./tests/fixtures/missing.json",
483 ])
484 .unwrap_err();
485 assert!(matches!(
486 error,
487 Error::FileNotFound(path) if path.to_str().unwrap() == "./tests/fixtures/missing.json"));
488 }
489
490 #[test]
491 fn multiple_equal_signs() {
492 let (key, value) = r#"foo="bar=baz""#.parse::<Input>().unwrap().unwrap_pair();
493 assert_eq!(key, "foo");
494 assert_eq!(value.as_str().unwrap(), "bar=baz");
495 }
496}