1use std::ops::Deref;
4use std::ops::DerefMut;
5use std::str::FromStr;
6use std::sync::LazyLock;
7
8use anyhow::bail;
9use indexmap::IndexMap;
10use regex::Regex;
11use serde_json::Value;
12use thiserror::Error;
13use wdl_analysis::Document;
14use wdl_engine::Inputs as EngineInputs;
15use wdl_engine::path::EvaluationPath;
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"^[^\[\]{}]*$").unwrap()
39});
40
41#[derive(Error, Debug)]
43pub enum Error {
44 #[error("failed to determine the current working directory")]
46 NoCurrentWorkingDirectory,
47
48 #[error(transparent)]
50 File(#[from] file::Error),
51
52 #[error("invalid key-value pair `{pair}`: {reason}")]
54 InvalidPair {
55 pair: String,
57
58 reason: String,
60 },
61
62 #[error("invalid entrypoint `{0}`")]
64 InvalidEntrypoint(String),
65
66 #[error("unable to deserialize `{0}` as a valid WDL value")]
68 Deserialize(String),
69}
70
71pub type Result<T> = std::result::Result<T, Error>;
73
74#[derive(Clone, Debug)]
76pub enum Input {
77 File(
79 EvaluationPath,
84 ),
85 Pair {
87 key: String,
89
90 value: Value,
92 },
93}
94
95impl Input {
96 pub fn as_file(&self) -> Option<&EvaluationPath> {
102 match self {
103 Input::File(p) => Some(p),
104 _ => None,
105 }
106 }
107
108 pub fn into_file(self) -> Option<EvaluationPath> {
114 match self {
115 Input::File(p) => Some(p),
116 _ => None,
117 }
118 }
119
120 pub fn unwrap_file(self) -> EvaluationPath {
126 match self {
127 Input::File(p) => p,
128 v => panic!("{v:?} is not an `Input::File`"),
129 }
130 }
131
132 pub fn as_pair(&self) -> Option<(&str, &Value)> {
138 match self {
139 Input::Pair { key, value } => Some((key.as_str(), value)),
140 _ => None,
141 }
142 }
143
144 pub fn into_pair(self) -> Option<(String, Value)> {
150 match self {
151 Input::Pair { key, value } => Some((key, value)),
152 _ => None,
153 }
154 }
155
156 pub fn unwrap_pair(self) -> (String, Value) {
162 match self {
163 Input::Pair { key, value } => (key, value),
164 v => panic!("{v:?} is not an `Input::Pair`"),
165 }
166 }
167}
168
169impl FromStr for Input {
170 type Err = Error;
171
172 fn from_str(s: &str) -> std::result::Result<Self, Error> {
173 match s.split_once("=") {
174 Some((key, value)) => {
175 if !IDENTIFIER_REGEX.is_match(key) {
176 return Err(Error::InvalidPair {
177 pair: s.to_string(),
178 reason: format!(
179 "key `{}` did not match the identifier regex (`{}`)",
180 key,
181 IDENTIFIER_REGEX.as_str()
182 ),
183 });
184 }
185
186 let value = serde_json::from_str(value).or_else(|_| {
187 if ASSUME_STRING_REGEX.is_match(value) {
188 Ok(Value::String(value.to_owned()))
189 } else {
190 Err(Error::Deserialize(value.to_owned()))
191 }
192 })?;
193
194 Ok(Input::Pair {
195 key: key.to_owned(),
196 value,
197 })
198 }
199 None => {
200 let path: EvaluationPath = s.parse().map_err(|e| file::Error::Path {
201 path: s.to_string(),
202 error: e,
203 })?;
204 if let Some(path) = path.as_local()
205 && !path.exists()
206 {
207 return Err(file::Error::NotFound(path.to_path_buf()).into());
208 }
209
210 Ok(Input::File(path))
211 }
212 }
213 }
214}
215
216type InputsInner = IndexMap<String, (EvaluationPath, Value)>;
218
219#[derive(Clone, Debug, Default)]
222pub struct Inputs {
223 inputs: InputsInner,
225 entrypoint: Option<String>,
227}
228
229impl Inputs {
230 async fn add_input(&mut self, input: &str) -> Result<()> {
232 match input.parse::<Input>()? {
233 Input::File(path) => {
234 let inputs = InputFile::read(&path).await.map_err(Error::File)?;
235 self.extend(inputs.into_inner());
236 }
237 Input::Pair { key, value } => {
238 let cwd = std::env::current_dir().map_err(|_| Error::NoCurrentWorkingDirectory)?;
239
240 let key = if let Some(prefix) = &self.entrypoint {
241 format!("{prefix}.{key}")
242 } else {
243 key
244 };
245 self.insert(key, (EvaluationPath::Local(cwd), value));
246 }
247 };
248
249 Ok(())
250 }
251
252 pub async fn coalesce<T, V>(iter: T, entrypoint: Option<String>) -> Result<Self>
260 where
261 T: IntoIterator<Item = V>,
262 V: AsRef<str>,
263 {
264 if let Some(ep) = &entrypoint
265 && ep.contains('.')
266 {
267 return Err(Error::InvalidEntrypoint(ep.into()));
268 }
269
270 let mut inputs = Inputs {
271 entrypoint,
272 ..Default::default()
273 };
274
275 for input in iter {
276 inputs.add_input(input.as_ref()).await?;
277 }
278
279 Ok(inputs)
280 }
281
282 pub fn into_inner(self) -> InputsInner {
284 self.inputs
285 }
286
287 pub fn into_engine_inputs(
300 self,
301 document: &Document,
302 ) -> anyhow::Result<Option<(String, EngineInputs, OriginPaths)>> {
303 let (origins, values) = self.inputs.into_iter().fold(
304 (IndexMap::new(), serde_json::Map::new()),
305 |(mut origins, mut values), (key, (origin, value))| {
306 origins.insert(key.clone(), origin);
307 values.insert(key, value);
308 (origins, values)
309 },
310 );
311
312 let result = EngineInputs::parse_object(document, values)?;
313
314 if let Some((derived, _)) = &result
315 && let Some(ep) = &self.entrypoint
316 && derived != ep
317 {
318 bail!(format!(
319 "supplied entrypoint `{ep}` does not match derived entrypoint `{derived}`"
320 ))
321 }
322
323 Ok(result.map(|(callee_name, inputs)| {
324 let callee_prefix = format!("{callee_name}.");
325
326 let origins = origins
327 .into_iter()
328 .map(|(key, path)| {
329 if let Some(key) = key.strip_prefix(&callee_prefix) {
330 (key.to_owned(), path)
331 } else {
332 (key, path)
333 }
334 })
335 .collect::<IndexMap<_, _>>();
336
337 (callee_name, inputs, OriginPaths::Map(origins))
338 }))
339 }
340}
341
342impl Deref for Inputs {
343 type Target = InputsInner;
344
345 fn deref(&self) -> &Self::Target {
346 &self.inputs
347 }
348}
349
350impl DerefMut for Inputs {
351 fn deref_mut(&mut self) -> &mut Self::Target {
352 &mut self.inputs
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use pretty_assertions::assert_eq;
359
360 use super::*;
361
362 #[test]
363 fn identifier_regex() {
364 assert!(IDENTIFIER_REGEX.is_match("here_is_an.identifier"));
365 assert!(!IDENTIFIER_REGEX.is_match("here is not an identifier"));
366 }
367
368 #[test]
369 fn assume_string_regex() {
370 assert!(ASSUME_STRING_REGEX.is_match(""));
372 assert!(ASSUME_STRING_REGEX.is_match("fooBAR082"));
373 assert!(ASSUME_STRING_REGEX.is_match("foo bar baz"));
374
375 assert!(!ASSUME_STRING_REGEX.is_match("[1, a]"));
377 }
378
379 #[test]
380 fn file_parsing() {
381 let input = "./tests/fixtures/inputs_one.json".parse::<Input>().unwrap();
383 assert!(matches!(
384 input,
385 Input::File(path) if path.to_str().unwrap().replace("\\", "/") == "tests/fixtures/inputs_one.json"
386 ));
387
388 let input = "tests/fixtures/inputs_three.yml".parse::<Input>().unwrap();
390 assert!(matches!(
391 input,
392 Input::File(path) if path.to_str().unwrap().replace("\\", "/") == "tests/fixtures/inputs_three.yml"
393 ));
394
395 let err = "./tests/fixtures/missing.json"
397 .parse::<Input>()
398 .unwrap_err();
399 assert_eq!(
400 err.to_string().replace("\\", "/"),
401 "input file `tests/fixtures/missing.json` was not found"
402 );
403 }
404
405 #[test]
406 fn key_value_pair_parsing() {
407 let input = r#"foo="bar""#.parse::<Input>().unwrap();
409 let (key, value) = input.unwrap_pair();
410 assert_eq!(key, "foo");
411 assert_eq!(value.as_str().unwrap(), "bar");
412
413 let input = r#"foo.bar_baz_quux="qil""#.parse::<Input>().unwrap();
415 let (key, value) = input.unwrap_pair();
416 assert_eq!(key, "foo.bar_baz_quux");
417 assert_eq!(value.as_str().unwrap(), "qil");
418
419 let err = r#"foo$="bar""#.parse::<Input>().unwrap_err();
421 assert_eq!(
422 err.to_string(),
423 r#"invalid key-value pair `foo$="bar"`: key `foo$` did not match the identifier regex (`^([a-zA-Z][a-zA-Z0-9_.]*)$`)"#
424 );
425
426 let input = r#"foo="bar$""#.parse::<Input>().unwrap();
428 let (key, value) = input.unwrap_pair();
429 assert_eq!(key, "foo");
430 assert_eq!(value.as_str().unwrap(), "bar$");
431 }
432
433 #[tokio::test]
434 async fn coalesce() {
435 fn check_string_value(inputs: &Inputs, key: &str, value: &str) {
437 let (_, input) = inputs.get(key).unwrap();
438 assert_eq!(input.as_str().unwrap(), value);
439 }
440
441 fn check_float_value(inputs: &Inputs, key: &str, value: f64) {
442 let (_, input) = inputs.get(key).unwrap();
443 assert_eq!(input.as_f64().unwrap(), value);
444 }
445
446 fn check_boolean_value(inputs: &Inputs, key: &str, value: bool) {
447 let (_, input) = inputs.get(key).unwrap();
448 assert_eq!(input.as_bool().unwrap(), value);
449 }
450
451 fn check_integer_value(inputs: &Inputs, key: &str, value: i64) {
452 let (_, input) = inputs.get(key).unwrap();
453 assert_eq!(input.as_i64().unwrap(), value);
454 }
455
456 let inputs = Inputs::coalesce(
458 [
459 "./tests/fixtures/inputs_one.json",
460 "./tests/fixtures/inputs_two.json",
461 "./tests/fixtures/inputs_three.yml",
462 ],
463 Some("foo".to_string()),
464 )
465 .await
466 .unwrap();
467
468 assert_eq!(inputs.len(), 5);
469 check_string_value(&inputs, "foo", "bar");
470 check_float_value(&inputs, "baz", 128.0);
471 check_string_value(&inputs, "quux", "qil");
472 check_string_value(&inputs, "new.key", "foobarbaz");
473 check_string_value(&inputs, "new_two.key", "bazbarfoo");
474
475 let inputs = Inputs::coalesce(
477 [
478 "./tests/fixtures/inputs_three.yml",
479 "./tests/fixtures/inputs_two.json",
480 "./tests/fixtures/inputs_one.json",
481 ],
482 Some("name_ex".to_string()),
483 )
484 .await
485 .unwrap();
486
487 assert_eq!(inputs.len(), 5);
488 check_string_value(&inputs, "foo", "bar");
489 check_float_value(&inputs, "baz", 42.0);
490 check_string_value(&inputs, "quux", "qil");
491 check_string_value(&inputs, "new.key", "foobarbaz");
492 check_string_value(&inputs, "new_two.key", "bazbarfoo");
493
494 let inputs = Inputs::coalesce(
496 [
497 r#"sandwich=-100"#,
498 "./tests/fixtures/inputs_one.json",
499 "./tests/fixtures/inputs_two.json",
500 r#"quux="jacks""#,
501 "./tests/fixtures/inputs_three.yml",
502 r#"baz=false"#,
503 ],
504 None,
505 )
506 .await
507 .unwrap();
508
509 assert_eq!(inputs.len(), 6);
510 check_string_value(&inputs, "foo", "bar");
511 check_boolean_value(&inputs, "baz", false);
512 check_string_value(&inputs, "quux", "jacks");
513 check_string_value(&inputs, "new.key", "foobarbaz");
514 check_string_value(&inputs, "new_two.key", "bazbarfoo");
515 check_integer_value(&inputs, "sandwich", -100);
516
517 let error = Inputs::coalesce(["./tests/fixtures/inputs_one.json", "foo=baz[bar"], None)
519 .await
520 .unwrap_err();
521 assert_eq!(
522 error.to_string(),
523 "unable to deserialize `baz[bar` as a valid WDL value"
524 );
525
526 let error = Inputs::coalesce(
528 [
529 "./tests/fixtures/inputs_one.json",
530 "./tests/fixtures/inputs_two.json",
531 "./tests/fixtures/inputs_three.yml",
532 "./tests/fixtures/missing.json",
533 ],
534 None,
535 )
536 .await
537 .unwrap_err();
538 assert_eq!(
539 error.to_string().replace("\\", "/"),
540 "input file `tests/fixtures/missing.json` was not found"
541 );
542 }
543
544 #[tokio::test]
545 async fn coalesce_special_characters() {
546 async fn check_can_coalesce_string(value: &str) {
547 let inputs = Inputs::coalesce([format!("input={}", value)], None)
548 .await
549 .unwrap();
550 let (_, input) = inputs.get("input").unwrap();
551 assert_eq!(input.as_str().unwrap(), value);
552 }
553 async fn check_cannot_coalesce_string(value: &str) {
554 let error = Inputs::coalesce([format!("input={}", value)], None)
555 .await
556 .unwrap_err();
557 assert!(matches!(
558 error,
559 Error::Deserialize(output) if output == value
560 ));
561 }
562
563 check_can_coalesce_string("can-coalesce-dashes").await;
564 check_can_coalesce_string("can\"coalesce\"quotes").await;
565 check_can_coalesce_string("can'coalesce'apostrophes").await;
566 check_can_coalesce_string("can;coalesce;semicolons").await;
567 check_can_coalesce_string("can:coalesce:colons").await;
568 check_can_coalesce_string("can*coalesce*stars").await;
569 check_can_coalesce_string("can,coalesce,commas").await;
570 check_can_coalesce_string("can?coalesce?question?mark").await;
571 check_can_coalesce_string("can|coalesce|pipe").await;
572 check_can_coalesce_string("can<coalesce>less<than>or>greater<than").await;
573 check_can_coalesce_string("can^coalesce^carrot").await;
574 check_can_coalesce_string("can#coalesce#pound#sign").await;
575 check_can_coalesce_string("can%coalesce%percent").await;
576 check_can_coalesce_string("can!coalesce!exclamation!marks").await;
577 check_can_coalesce_string("can\\coalesce\\backslashes").await;
578 check_can_coalesce_string("can@coalesce@at@sign").await;
579 check_can_coalesce_string("can(coalesce(parenthesis))").await;
580 check_can_coalesce_string("can coalesce السلام عليكم").await;
581 check_can_coalesce_string("can coalesce 你").await;
582 check_can_coalesce_string("can coalesce Dobrý den").await;
583 check_can_coalesce_string("can coalesce Hello").await;
584 check_can_coalesce_string("can coalesce שלום").await;
585 check_can_coalesce_string("can coalesce नमस्ते").await;
586 check_can_coalesce_string("can coalesce こんにちは").await;
587 check_can_coalesce_string("can coalesce 안녕하세요").await;
588 check_can_coalesce_string("can coalesce 你好").await;
589 check_can_coalesce_string("can coalesce Olá").await;
590 check_can_coalesce_string("can coalesce Здравствуйте").await;
591 check_can_coalesce_string("can coalesce Hola").await;
592 check_cannot_coalesce_string("cannot coalesce string with [").await;
593 check_cannot_coalesce_string("cannot coalesce string with ]").await;
594 check_cannot_coalesce_string("cannot coalesce string with {").await;
595 check_cannot_coalesce_string("cannot coalesce string with }").await;
596 }
597
598 #[test]
599 fn multiple_equal_signs() {
600 let (key, value) = r#"foo="bar=baz""#.parse::<Input>().unwrap().unwrap_pair();
601 assert_eq!(key, "foo");
602 assert_eq!(value.as_str().unwrap(), "bar=baz");
603 }
604}