Skip to main content

wdl_engine/
inputs.rs

1//! Implementation of workflow and task inputs.
2
3use std::collections::BTreeSet;
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::fs::File;
7use std::io::BufReader;
8use std::path::Path;
9
10use anyhow::Context;
11use anyhow::Result;
12use anyhow::bail;
13use indexmap::IndexMap;
14use serde::Serialize;
15use serde::ser::SerializeMap;
16use serde_json::Value as JsonValue;
17use serde_yaml_ng::Value as YamlValue;
18use wdl_analysis::Document;
19use wdl_analysis::document::Input;
20use wdl_analysis::document::Task;
21use wdl_analysis::document::Workflow;
22use wdl_analysis::types::CallKind;
23use wdl_analysis::types::Coercible as _;
24use wdl_analysis::types::Optional;
25use wdl_analysis::types::PrimitiveType;
26use wdl_analysis::types::display_types;
27use wdl_analysis::types::v1::task_hint_types;
28use wdl_analysis::types::v1::task_requirement_types;
29
30use crate::Array;
31use crate::Coercible;
32use crate::CompoundValue;
33use crate::EvaluationPath;
34use crate::Value;
35
36/// A type alias to a JSON map (object).
37pub type JsonMap = serde_json::Map<String, JsonValue>;
38
39/// Checks that an input value matches the type of the input.
40fn check_input_type(_document: &Document, name: &str, input: &Input, value: &Value) -> Result<()> {
41    // We accept optional values for the input even if the input's type is
42    // non-optional; if the runtime value is `None` for a non-optional input,
43    // the default expression will be evaluated instead.
44    let expected_ty = if !input.required() {
45        input.ty().optional()
46    } else {
47        input.ty().clone()
48    };
49
50    let ty = value.ty();
51    if !ty.is_coercible_to(&expected_ty) {
52        bail!("expected {expected_ty:#} for input `{name}`, but found {ty:#}");
53    }
54
55    Ok(())
56}
57
58/// Resolves paths in a value using per-element origins.
59///
60/// When `origins` contains multiple entries and the value is an array, each
61/// element is resolved against its corresponding origin. Otherwise, all paths
62/// are resolved against the first (and only) origin.
63async fn resolve_with_origins(
64    value: Value,
65    ty: &wdl_analysis::types::Type,
66    origins: &[EvaluationPath],
67) -> Result<Value> {
68    if origins.len() > 1
69        && let Value::Compound(CompoundValue::Array(ref array)) = value
70    {
71        let arr_ty = ty.as_array().expect("should be an array type");
72        assert_eq!(
73            origins.len(),
74            array.as_slice().len(),
75            "the number of origins should match the number of array elements"
76        );
77        let optional = arr_ty.element_type().is_optional();
78        let mut resolved = Vec::with_capacity(array.as_slice().len());
79        for (elem, base_dir) in array.as_slice().iter().zip(origins) {
80            resolved.push(
81                elem.resolve_paths(optional, None, None, &|p| p.expand(base_dir))
82                    .await?,
83            );
84        }
85        return Ok(Value::Compound(CompoundValue::Array(Array::new_unchecked(
86            arr_ty.clone(),
87            resolved,
88        ))));
89    }
90
91    let base_dir = &origins[0];
92    value
93        .resolve_paths(ty.is_optional(), None, None, &|p| p.expand(base_dir))
94        .await
95}
96
97/// Represents inputs to a task.
98#[derive(Default, Debug, Clone)]
99pub struct TaskInputs {
100    /// The task input values.
101    inputs: IndexMap<String, Value>,
102    /// The overridden requirements section values.
103    requirements: HashMap<String, Value>,
104    /// The overridden hints section values.
105    hints: HashMap<String, Value>,
106}
107
108impl TaskInputs {
109    /// Iterates the inputs to the task.
110    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
111        self.inputs.iter().map(|(k, v)| (k.as_str(), v))
112    }
113
114    /// Determines if the inputs are empty.
115    pub fn is_empty(&self) -> bool {
116        self.len() == 0
117    }
118
119    /// Gets the length of the inputs.
120    ///
121    /// This includes the count of inputs, requirements, and hints.
122    pub fn len(&self) -> usize {
123        self.inputs.len() + self.requirements.len() + self.hints.len()
124    }
125
126    /// Gets an input by name.
127    pub fn get(&self, name: &str) -> Option<&Value> {
128        self.inputs.get(name)
129    }
130
131    /// Sets a task input.
132    ///
133    /// Returns the previous value, if any.
134    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
135        self.inputs.insert(name.into(), value.into())
136    }
137
138    /// Gets an overridden requirement by name.
139    pub fn requirement(&self, name: &str) -> Option<&Value> {
140        self.requirements.get(name)
141    }
142
143    /// Overrides a requirement by name.
144    pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
145        self.requirements.insert(name.into(), value.into());
146    }
147
148    /// Gets an overridden hint by name.
149    pub fn hint(&self, name: &str) -> Option<&Value> {
150        self.hints.get(name)
151    }
152
153    /// Overrides a hint by name.
154    pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
155        self.hints.insert(name.into(), value.into());
156    }
157
158    /// Replaces any `File` or `Directory` input values with joining the
159    /// specified path with the value.
160    ///
161    /// This method will attempt to coerce matching input values to their
162    /// expected types.
163    pub async fn join_paths<'a>(
164        &mut self,
165        task: &Task,
166        path: impl Fn(&str) -> Result<&'a [EvaluationPath]>,
167    ) -> Result<()> {
168        for (name, value) in self.inputs.iter_mut() {
169            let Some(ty) = task.inputs().get(name).map(|input| input.ty().clone()) else {
170                bail!("could not find an expected type for input {name}");
171            };
172
173            let origins = path(name)?;
174
175            if let Ok(v) = value.coerce(None, &ty) {
176                *value = resolve_with_origins(v, &ty, origins).await?;
177            }
178        }
179        Ok(())
180    }
181
182    /// Validates the inputs for the given task.
183    ///
184    /// The `specified` set of inputs are those that are present, but may not
185    /// have values available at validation.
186    pub fn validate(
187        &self,
188        document: &Document,
189        task: &Task,
190        specified: Option<&HashSet<String>>,
191    ) -> Result<()> {
192        let version = document.version().context("missing document version")?;
193
194        // Start by validating all the specified inputs and their types
195        for (name, value) in &self.inputs {
196            let input = task
197                .inputs()
198                .get(name)
199                .with_context(|| format!("unknown input `{name}`"))?;
200
201            check_input_type(document, name, input, value)?;
202        }
203
204        // Next check for missing required inputs
205        for (name, input) in task.inputs() {
206            if input.required()
207                && !self.inputs.contains_key(name)
208                && specified.map(|s| !s.contains(name)).unwrap_or(true)
209            {
210                bail!(
211                    "missing required input `{name}` to task `{task}`",
212                    task = task.name()
213                );
214            }
215        }
216
217        // Check the types of the specified requirements
218        for (name, value) in &self.requirements {
219            let ty = value.ty();
220            if let Some(expected) = task_requirement_types(version, name.as_str()) {
221                if !expected.iter().any(|target| ty.is_coercible_to(target)) {
222                    bail!(
223                        "expected {expected:#} for requirement `{name}`, but found {ty:#}",
224                        expected = display_types(expected),
225                    );
226                }
227
228                continue;
229            }
230
231            bail!("unsupported requirement `{name}`");
232        }
233
234        // Check the types of the specified hints
235        for (name, value) in &self.hints {
236            let ty = value.ty();
237            if let Some(expected) = task_hint_types(version, name.as_str(), false)
238                && !expected.iter().any(|target| ty.is_coercible_to(target))
239            {
240                bail!(
241                    "expected {expected:#} for hint `{name}`, but found {ty:#}",
242                    expected = display_types(expected),
243                );
244            }
245        }
246
247        Ok(())
248    }
249
250    /// Sets a value with dotted path notation.
251    ///
252    /// If the provided `value` is a [`PrimitiveType`] other than
253    /// [`PrimitiveType::String`] and the `path` is to an input which is of
254    /// type [`PrimitiveType::String`], `value` will be converted to a string
255    /// and accepted as valid.
256    ///
257    /// Returns `true` if the given path was for an input or `false` if the
258    /// given path was for a requirement or hint.
259    fn set_path_value(
260        &mut self,
261        document: &Document,
262        task: &Task,
263        path: &str,
264        value: Value,
265    ) -> Result<bool> {
266        let version = document.version().expect("document should have a version");
267
268        match path.split_once('.') {
269            // The path might contain a requirement or hint
270            Some((key, remainder)) => {
271                let (must_match, matched) = match key {
272                    "runtime" => (
273                        false,
274                        task_requirement_types(version, remainder)
275                            .map(|types| (true, types))
276                            .or_else(|| {
277                                task_hint_types(version, remainder, false)
278                                    .map(|types| (false, types))
279                            }),
280                    ),
281                    "requirements" => (
282                        true,
283                        task_requirement_types(version, remainder).map(|types| (true, types)),
284                    ),
285                    "hints" => (
286                        false,
287                        task_hint_types(version, remainder, false).map(|types| (false, types)),
288                    ),
289                    _ => {
290                        bail!(
291                            "task `{task}` does not have an input named `{path}`",
292                            task = task.name()
293                        );
294                    }
295                };
296
297                if let Some((requirement, expected)) = matched {
298                    for ty in expected {
299                        if value.ty().is_coercible_to(ty) {
300                            if requirement {
301                                self.requirements.insert(remainder.to_string(), value);
302                            } else {
303                                self.hints.insert(remainder.to_string(), value);
304                            }
305                            return Ok(false);
306                        }
307                    }
308
309                    bail!(
310                        "expected {expected:#} for {key} key `{remainder}`, but found {ty:#}",
311                        expected = display_types(expected),
312                        ty = value.ty()
313                    );
314                } else if must_match {
315                    bail!("unsupported {key} key `{remainder}`");
316                } else {
317                    Ok(false)
318                }
319            }
320            // The path is to an input
321            None => {
322                let input = task.inputs().get(path).with_context(|| {
323                    format!(
324                        "task `{name}` does not have an input named `{path}`",
325                        name = task.name()
326                    )
327                })?;
328
329                // Allow primitive values to implicitly convert to string
330                let actual = value.ty();
331                let expected = input.ty();
332                if let Some(PrimitiveType::String) = expected.as_primitive()
333                    && let Some(actual) = actual.as_primitive()
334                    && actual != PrimitiveType::String
335                {
336                    self.inputs
337                        .insert(path.to_string(), value.to_string().into());
338                    return Ok(true);
339                }
340
341                // Auto-wrap a non-array value in a single-element array when the
342                // expected type is an array and the value is coercible to the
343                // element type.
344                let value = if let Some(arr_ty) = expected.as_array()
345                    && !matches!(&value, Value::Compound(CompoundValue::Array(_)))
346                    && value.ty().is_coercible_to(arr_ty.element_type())
347                {
348                    Value::Compound(CompoundValue::Array(Array::new_unchecked(
349                        expected.clone(),
350                        vec![value],
351                    )))
352                } else {
353                    value
354                };
355
356                check_input_type(document, path, input, &value)?;
357                self.inputs.insert(path.to_string(), value);
358                Ok(true)
359            }
360        }
361    }
362}
363
364impl<S, V> FromIterator<(S, V)> for TaskInputs
365where
366    S: Into<String>,
367    V: Into<Value>,
368{
369    fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
370        Self {
371            inputs: iter
372                .into_iter()
373                .map(|(k, v)| (k.into(), v.into()))
374                .collect(),
375            requirements: Default::default(),
376            hints: Default::default(),
377        }
378    }
379}
380
381impl Serialize for TaskInputs {
382    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
383    where
384        S: serde::Serializer,
385    {
386        let mut map = serializer.serialize_map(Some(self.len()))?;
387
388        for (k, v) in &self.inputs {
389            let v = crate::ValueSerializer::new(None, v, true);
390            map.serialize_entry(k, &v)?;
391        }
392
393        for (k, v) in &self.requirements {
394            let v = crate::ValueSerializer::new(None, v, true);
395            map.serialize_entry(&format!("requirements.{k}"), &v)?;
396        }
397
398        for (k, v) in &self.hints {
399            let v = crate::ValueSerializer::new(None, v, true);
400            map.serialize_entry(&format!("hints.{k}"), &v)?;
401        }
402
403        map.end()
404    }
405}
406
407/// Represents inputs to a workflow.
408#[derive(Default, Debug, Clone)]
409pub struct WorkflowInputs {
410    /// The workflow input values.
411    inputs: IndexMap<String, Value>,
412    /// The nested call inputs.
413    calls: HashMap<String, Inputs>,
414}
415
416impl WorkflowInputs {
417    /// Determines if there are any nested inputs in the workflow inputs.
418    ///
419    /// Returns `true` if the inputs contains nested inputs or `false` if it
420    /// does not.
421    pub fn has_nested_inputs(&self) -> bool {
422        self.calls.values().any(|inputs| match inputs {
423            Inputs::Task(task) => !task.inputs.is_empty(),
424            Inputs::Workflow(workflow) => workflow.has_nested_inputs(),
425        })
426    }
427
428    /// Iterates the inputs to the workflow.
429    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
430        self.inputs.iter().map(|(k, v)| (k.as_str(), v))
431    }
432
433    /// Determines if the inputs are empty.
434    pub fn is_empty(&self) -> bool {
435        self.len() == 0
436    }
437
438    /// Gets the length of the workflow inputs.
439    ///
440    /// This includes the workflow inputs plus the lengths of all nested inputs.
441    pub fn len(&self) -> usize {
442        self.inputs.len() + self.calls.values().map(Inputs::len).sum::<usize>()
443    }
444
445    /// Gets an input by name.
446    pub fn get(&self, name: &str) -> Option<&Value> {
447        self.inputs.get(name)
448    }
449
450    /// Gets the nested call inputs.
451    pub fn calls(&self) -> &HashMap<String, Inputs> {
452        &self.calls
453    }
454
455    /// Gets the nested call inputs.
456    pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
457        &mut self.calls
458    }
459
460    /// Sets a workflow input.
461    ///
462    /// Returns the previous value, if any.
463    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
464        self.inputs.insert(name.into(), value.into())
465    }
466
467    /// Checks if the inputs contain a value with the specified name.
468    ///
469    /// This does not check nested call inputs.
470    pub fn contains(&self, name: &str) -> bool {
471        self.inputs.contains_key(name)
472    }
473
474    /// Replaces any `File` or `Directory` input values with joining the
475    /// specified path with the value.
476    ///
477    /// This method will attempt to coerce matching input values to their
478    /// expected types.
479    pub async fn join_paths<'a>(
480        &mut self,
481        workflow: &Workflow,
482        path: impl Fn(&str) -> Result<&'a [EvaluationPath]>,
483    ) -> Result<()> {
484        for (name, value) in self.inputs.iter_mut() {
485            let Some(ty) = workflow.inputs().get(name).map(|input| input.ty().clone()) else {
486                bail!("could not find an expected type for input {name}");
487            };
488
489            let origins = path(name)?;
490
491            if let Ok(v) = value.coerce(None, &ty) {
492                *value = resolve_with_origins(v, &ty, origins).await?;
493            }
494        }
495        Ok(())
496    }
497
498    /// Validates the inputs for the given workflow.
499    ///
500    /// The `specified` set of inputs are those that are present, but may not
501    /// have values available at validation.
502    pub fn validate(
503        &self,
504        document: &Document,
505        workflow: &Workflow,
506        specified: Option<&HashSet<String>>,
507    ) -> Result<()> {
508        // Start by validating all the specified inputs and their types
509        for (name, value) in &self.inputs {
510            let input = workflow
511                .inputs()
512                .get(name)
513                .with_context(|| format!("unknown input `{name}`"))?;
514            check_input_type(document, name, input, value)?;
515        }
516
517        // Next check for missing required inputs
518        for (name, input) in workflow.inputs() {
519            if input.required()
520                && !self.inputs.contains_key(name)
521                && specified.map(|s| !s.contains(name)).unwrap_or(true)
522            {
523                bail!(
524                    "missing required input `{name}` to workflow `{workflow}`",
525                    workflow = workflow.name()
526                );
527            }
528        }
529
530        // Check that the workflow allows nested inputs
531        if !workflow.allows_nested_inputs() && self.has_nested_inputs() {
532            bail!(
533                "cannot specify a nested call input for workflow `{name}` as it does not allow \
534                 nested inputs",
535                name = workflow.name()
536            );
537        }
538
539        // Check the inputs to the specified calls
540        for (name, inputs) in &self.calls {
541            let call = workflow.calls().get(name).with_context(|| {
542                format!(
543                    "workflow `{workflow}` does not have a call named `{name}`",
544                    workflow = workflow.name()
545                )
546            })?;
547
548            // Resolve the target document; the namespace is guaranteed to be present in the
549            // document.
550            let document = call
551                .namespace()
552                .map(|ns| {
553                    document
554                        .namespace(ns)
555                        .expect("namespace should be present")
556                        .document()
557                })
558                .unwrap_or(document);
559
560            // Validate the call's inputs
561            let inputs = match call.kind() {
562                CallKind::Task => {
563                    let task = document
564                        .task_by_name(call.name())
565                        .expect("task should be present");
566
567                    let task_inputs = inputs.as_task_inputs().with_context(|| {
568                        format!("`{name}` is a call to a task, but workflow inputs were supplied")
569                    })?;
570
571                    task_inputs.validate(document, task, Some(call.specified()))?;
572                    &task_inputs.inputs
573                }
574                CallKind::Workflow => {
575                    let workflow = document.workflow().expect("should have a workflow");
576                    assert_eq!(
577                        workflow.name(),
578                        call.name(),
579                        "call name does not match workflow name"
580                    );
581                    let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
582                        format!("`{name}` is a call to a workflow, but task inputs were supplied")
583                    })?;
584
585                    workflow_inputs.validate(document, workflow, Some(call.specified()))?;
586                    &workflow_inputs.inputs
587                }
588            };
589
590            for input in inputs.keys() {
591                if call.specified().contains(input) {
592                    bail!(
593                        "cannot specify nested input `{input}` for call `{call}` as it was \
594                         explicitly specified in the call itself",
595                        call = call.name(),
596                    );
597                }
598            }
599        }
600
601        // Finally, check for missing call arguments
602        if workflow.allows_nested_inputs() {
603            for (call, ty) in workflow.calls() {
604                let inputs = self.calls.get(call);
605
606                for (input, _) in ty
607                    .inputs()
608                    .iter()
609                    .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
610                {
611                    if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
612                        bail!("missing required input `{input}` for call `{call}`");
613                    }
614                }
615            }
616        }
617
618        Ok(())
619    }
620
621    /// Sets a value with dotted path notation.
622    ///
623    /// If the provided `value` is a [`PrimitiveType`] other than
624    /// [`PrimitiveType::String`] and the `path` is to an input which is of
625    /// type [`PrimitiveType::String`], `value` will be converted to a string
626    /// and accepted as valid.
627    ///
628    /// Returns `true` if the path was to an input or `false` if it was not.
629    fn set_path_value(
630        &mut self,
631        document: &Document,
632        workflow: &Workflow,
633        path: &str,
634        value: Value,
635    ) -> Result<bool> {
636        match path.split_once('.') {
637            Some((name, remainder)) => {
638                // Resolve the call by name
639                let call = workflow.calls().get(name).with_context(|| {
640                    format!(
641                        "workflow `{workflow}` does not have a call named `{name}`",
642                        workflow = workflow.name()
643                    )
644                })?;
645
646                // Insert the inputs for the call
647                let inputs =
648                    self.calls
649                        .entry(name.to_string())
650                        .or_insert_with(|| match call.kind() {
651                            CallKind::Task => Inputs::Task(Default::default()),
652                            CallKind::Workflow => Inputs::Workflow(Default::default()),
653                        });
654
655                // Resolve the target document; the namespace is guaranteed to be present in the
656                // document.
657                let document = call
658                    .namespace()
659                    .map(|ns| {
660                        document
661                            .namespace(ns)
662                            .expect("namespace should be present")
663                            .document()
664                    })
665                    .unwrap_or(document);
666
667                let next = remainder
668                    .split_once('.')
669                    .map(|(n, _)| n)
670                    .unwrap_or(remainder);
671                if call.specified().contains(next) {
672                    bail!(
673                        "cannot specify nested input `{next}` for call `{name}` as it was \
674                         explicitly specified in the call itself",
675                    );
676                }
677
678                // Recurse on the call's inputs to set the value
679                let input = match call.kind() {
680                    CallKind::Task => {
681                        let task = document
682                            .task_by_name(call.name())
683                            .expect("task should be present");
684                        inputs
685                            .as_task_inputs_mut()
686                            .expect("should be a task input")
687                            .set_path_value(document, task, remainder, value)?
688                    }
689                    CallKind::Workflow => {
690                        let workflow = document.workflow().expect("should have a workflow");
691                        assert_eq!(
692                            workflow.name(),
693                            call.name(),
694                            "call name does not match workflow name"
695                        );
696                        inputs
697                            .as_workflow_inputs_mut()
698                            .expect("should be a task input")
699                            .set_path_value(document, workflow, remainder, value)?
700                    }
701                };
702
703                if input && !workflow.allows_nested_inputs() {
704                    bail!(
705                        "cannot specify a nested call input for workflow `{workflow}` as it does \
706                         not allow nested inputs",
707                        workflow = workflow.name()
708                    );
709                }
710
711                Ok(input)
712            }
713            None => {
714                let input = workflow.inputs().get(path).with_context(|| {
715                    format!(
716                        "workflow `{workflow}` does not have an input named `{path}`",
717                        workflow = workflow.name()
718                    )
719                })?;
720
721                // Allow primitive values to implicitly convert to string
722                let actual = value.ty();
723                let expected = input.ty();
724                if let Some(PrimitiveType::String) = expected.as_primitive()
725                    && let Some(actual) = actual.as_primitive()
726                    && actual != PrimitiveType::String
727                {
728                    self.inputs
729                        .insert(path.to_string(), value.to_string().into());
730                    return Ok(true);
731                }
732
733                // Auto-wrap a non-array value in a single-element array when
734                // the expected type is an array and the value is coercible to
735                // the element type.
736                let value = if let Some(arr_ty) = expected.as_array()
737                    && !matches!(&value, Value::Compound(CompoundValue::Array(_)))
738                    && value.ty().is_coercible_to(arr_ty.element_type())
739                {
740                    Value::Compound(CompoundValue::Array(Array::new_unchecked(
741                        expected.clone(),
742                        vec![value],
743                    )))
744                } else {
745                    value
746                };
747
748                check_input_type(document, path, input, &value)?;
749                self.inputs.insert(path.to_string(), value);
750                Ok(true)
751            }
752        }
753    }
754}
755
756impl<S, V> FromIterator<(S, V)> for WorkflowInputs
757where
758    S: Into<String>,
759    V: Into<Value>,
760{
761    fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
762        Self {
763            inputs: iter
764                .into_iter()
765                .map(|(k, v)| (k.into(), v.into()))
766                .collect(),
767            calls: Default::default(),
768        }
769    }
770}
771
772impl Serialize for WorkflowInputs {
773    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
774    where
775        S: serde::Serializer,
776    {
777        let mut map = serializer.serialize_map(Some(self.len()))?;
778        for (k, v) in &self.inputs {
779            let serialized_value = crate::ValueSerializer::new(None, v, true);
780            map.serialize_entry(k, &serialized_value)?;
781        }
782
783        for (k, v) in &self.calls {
784            let serialized = serde_json::to_value(v).map_err(|_| {
785                serde::ser::Error::custom(format!("failed to serialize inputs for call `{k}`"))
786            })?;
787            let mut map = serde_json::Map::new();
788            if let JsonValue::Object(obj) = serialized {
789                for (inner, value) in obj {
790                    map.insert(format!("{k}.{inner}"), value);
791                }
792            }
793        }
794
795        map.end()
796    }
797}
798
799/// Represents inputs to a WDL workflow or task.
800#[derive(Debug, Clone)]
801pub enum Inputs {
802    /// The inputs are to a task.
803    Task(TaskInputs),
804    /// The inputs are to a workflow.
805    Workflow(WorkflowInputs),
806}
807
808impl Inputs {
809    /// Parses an inputs file from the given file path.
810    ///
811    /// The format (JSON or YAML) is determined by the file extension:
812    ///
813    /// - `.json` for JSON format
814    /// - `.yml` or `.yaml` for YAML format
815    ///
816    /// The parse uses the provided document to validate the input keys within
817    /// the file.
818    ///
819    /// Returns `Ok(Some(_))` if the inputs are not empty.
820    ///
821    /// Returns `Ok(None)` if the inputs are empty.
822    pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
823        let path = path.as_ref();
824
825        match path.extension().and_then(|ext| ext.to_str()) {
826            Some("json") => Self::parse_json(document, path),
827            Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
828            ext => bail!(
829                "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
830                 and YAML (`.yaml` and `.yml`)",
831                ext = ext.unwrap_or("")
832            ),
833        }
834        .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
835    }
836
837    /// Parses a JSON inputs file from the given file path.
838    ///
839    /// The parse uses the provided document to validate the input keys within
840    /// the file.
841    ///
842    /// Returns `Ok(Some(_))` if the inputs are not empty.
843    ///
844    /// Returns `Ok(None)` if the inputs are empty.
845    pub fn parse_json(
846        document: &Document,
847        path: impl AsRef<Path>,
848    ) -> Result<Option<(String, Self)>> {
849        let path = path.as_ref();
850
851        let file = File::open(path).with_context(|| {
852            format!("failed to open input file `{path}`", path = path.display())
853        })?;
854
855        // Parse the JSON (should be an object)
856        let reader = BufReader::new(file);
857
858        let map = std::mem::take(
859            serde_json::from_reader::<_, JsonValue>(reader)?
860                .as_object_mut()
861                .with_context(|| {
862                    format!(
863                        "expected input file `{path}` to contain a JSON object",
864                        path = path.display()
865                    )
866                })?,
867        );
868
869        Self::parse_json_object(document, map)
870    }
871
872    /// Parses a YAML inputs file from the given file path.
873    ///
874    /// The parse uses the provided document to validate the input keys within
875    /// the file.
876    ///
877    /// Returns `Ok(Some(_))` if the inputs are not empty.
878    ///
879    /// Returns `Ok(None)` if the inputs are empty.
880    pub fn parse_yaml(
881        document: &Document,
882        path: impl AsRef<Path>,
883    ) -> Result<Option<(String, Self)>> {
884        let path = path.as_ref();
885
886        let file = File::open(path).with_context(|| {
887            format!("failed to open input file `{path}`", path = path.display())
888        })?;
889
890        // Parse the YAML
891        let reader = BufReader::new(file);
892        let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
893
894        // Convert YAML to JSON format
895        let mut json = serde_json::to_value(yaml).with_context(|| {
896            format!(
897                "failed to convert YAML to JSON for processing `{path}`",
898                path = path.display()
899            )
900        })?;
901
902        let object = std::mem::take(json.as_object_mut().with_context(|| {
903            format!(
904                "expected input file `{path}` to contain a YAML mapping",
905                path = path.display()
906            )
907        })?);
908
909        Self::parse_json_object(document, object)
910    }
911
912    /// Determines if the inputs are empty.
913    pub fn is_empty(&self) -> bool {
914        self.len() == 0
915    }
916
917    /// Gets the length of all inputs.
918    ///
919    /// For task inputs, this include the inputs, requirements, and hints.
920    ///
921    /// For workflow inputs, this includes the inputs and nested inputs.
922    pub fn len(&self) -> usize {
923        match self {
924            Self::Task(inputs) => inputs.len(),
925            Self::Workflow(inputs) => inputs.len(),
926        }
927    }
928
929    /// Gets an input value.
930    pub fn get(&self, name: &str) -> Option<&Value> {
931        match self {
932            Self::Task(t) => t.inputs.get(name),
933            Self::Workflow(w) => w.inputs.get(name),
934        }
935    }
936
937    /// Sets an input value.
938    ///
939    /// Returns the previous value, if any.
940    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
941        match self {
942            Self::Task(inputs) => inputs.set(name, value),
943            Self::Workflow(inputs) => inputs.set(name, value),
944        }
945    }
946
947    /// Gets the task inputs.
948    ///
949    /// Returns `None` if the inputs are for a workflow.
950    pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
951        match self {
952            Self::Task(inputs) => Some(inputs),
953            Self::Workflow(_) => None,
954        }
955    }
956
957    /// Gets a mutable reference to task inputs.
958    ///
959    /// Returns `None` if the inputs are for a workflow.
960    pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
961        match self {
962            Self::Task(inputs) => Some(inputs),
963            Self::Workflow(_) => None,
964        }
965    }
966
967    /// Unwraps the inputs as task inputs.
968    ///
969    /// # Panics
970    ///
971    /// Panics if the inputs are for a workflow.
972    pub fn unwrap_task_inputs(self) -> TaskInputs {
973        match self {
974            Self::Task(inputs) => inputs,
975            Self::Workflow(_) => panic!("inputs are for a workflow"),
976        }
977    }
978
979    /// Gets the workflow inputs.
980    ///
981    /// Returns `None` if the inputs are for a task.
982    pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
983        match self {
984            Self::Task(_) => None,
985            Self::Workflow(inputs) => Some(inputs),
986        }
987    }
988
989    /// Gets a mutable reference to workflow inputs.
990    ///
991    /// Returns `None` if the inputs are for a task.
992    pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
993        match self {
994            Self::Task(_) => None,
995            Self::Workflow(inputs) => Some(inputs),
996        }
997    }
998
999    /// Unwraps the inputs as workflow inputs.
1000    ///
1001    /// # Panics
1002    ///
1003    /// Panics if the inputs are for a task.
1004    pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
1005        match self {
1006            Self::Task(_) => panic!("inputs are for a task"),
1007            Self::Workflow(inputs) => inputs,
1008        }
1009    }
1010
1011    /// Parses the root object in a [`JsonMap`].
1012    ///
1013    /// Returns `Ok(Some(_))` if the inputs are not empty.
1014    ///
1015    /// Returns `Ok(None)` if the inputs are empty.
1016    pub fn parse_json_object(
1017        document: &Document,
1018        object: JsonMap,
1019    ) -> Result<Option<(String, Self)>> {
1020        // If the object is empty, treat it as an invocation without any inputs.
1021        if object.is_empty() {
1022            return Ok(None);
1023        }
1024
1025        // Otherwise, build a set of candidate targets from the prefixes of each input
1026        // key.
1027        let mut target_candidates = BTreeSet::new();
1028        for key in object.keys() {
1029            let Some((prefix, _)) = key.split_once('.') else {
1030                bail!(
1031                    "invalid input key `{key}`: expected the key to be prefixed with the workflow \
1032                     or task name",
1033                )
1034            };
1035            target_candidates.insert(prefix);
1036        }
1037
1038        // If every prefix is the same, there will be only one candidate. If not, report
1039        // an error.
1040        let target_name = match target_candidates
1041            .iter()
1042            .take(2)
1043            .collect::<Vec<_>>()
1044            .as_slice()
1045        {
1046            [] => panic!("no target candidates for inputs; report this as a bug"),
1047            [target_name] => target_name.to_string(),
1048            _ => bail!(
1049                "invalid inputs: expected each input key to be prefixed with the same workflow or \
1050                 task name, but found multiple prefixes: {target_candidates:?}",
1051            ),
1052        };
1053
1054        let inputs = match (document.task_by_name(&target_name), document.workflow()) {
1055            (Some(task), _) => Self::parse_task_inputs(document, task, object)?,
1056            (None, Some(workflow)) if workflow.name() == target_name => {
1057                Self::parse_workflow_inputs(document, workflow, object)?
1058            }
1059            _ => bail!(
1060                "invalid inputs: a task or workflow named `{target_name}` does not exist in the \
1061                 document"
1062            ),
1063        };
1064        Ok(Some((target_name, inputs)))
1065    }
1066
1067    /// Parses the inputs for a task.
1068    fn parse_task_inputs(document: &Document, task: &Task, object: JsonMap) -> Result<Self> {
1069        let mut inputs = TaskInputs::default();
1070        for (key, value) in object {
1071            // Convert from serde_json::Value to crate::Value
1072            let value = serde_json::from_value(value)
1073                .with_context(|| format!("invalid input value for key `{key}`"))?;
1074
1075            match key.split_once(".") {
1076                Some((prefix, remainder)) if prefix == task.name() => {
1077                    inputs
1078                        .set_path_value(document, task, remainder, value)
1079                        .with_context(|| format!("invalid input key `{key}`"))?;
1080                }
1081                _ => {
1082                    // This should be caught by the initial check of the prefixes in
1083                    // `parse_json_object()`, but we retain a friendly error message in case this
1084                    // function gets called from another context in the future.
1085                    bail!(
1086                        "invalid input key `{key}`: expected key to be prefixed with `{task}`",
1087                        task = task.name()
1088                    );
1089                }
1090            }
1091        }
1092
1093        Ok(Inputs::Task(inputs))
1094    }
1095
1096    /// Parses the inputs for a workflow.
1097    fn parse_workflow_inputs(
1098        document: &Document,
1099        workflow: &Workflow,
1100        object: JsonMap,
1101    ) -> Result<Self> {
1102        let mut inputs = WorkflowInputs::default();
1103        for (key, value) in object {
1104            // Convert from serde_json::Value to crate::Value
1105            let value = serde_json::from_value(value)
1106                .with_context(|| format!("invalid input value for key `{key}`"))?;
1107
1108            match key.split_once(".") {
1109                Some((prefix, remainder)) if prefix == workflow.name() => {
1110                    inputs
1111                        .set_path_value(document, workflow, remainder, value)
1112                        .with_context(|| format!("invalid input key `{key}`"))?;
1113                }
1114                _ => {
1115                    // This should be caught by the initial check of the prefixes in
1116                    // `parse_json_object()`, but we retain a friendly error message in case this
1117                    // function gets called from another context in the future.
1118                    bail!(
1119                        "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
1120                        workflow = workflow.name()
1121                    );
1122                }
1123            }
1124        }
1125
1126        Ok(Inputs::Workflow(inputs))
1127    }
1128}
1129
1130impl From<TaskInputs> for Inputs {
1131    fn from(inputs: TaskInputs) -> Self {
1132        Self::Task(inputs)
1133    }
1134}
1135
1136impl From<WorkflowInputs> for Inputs {
1137    fn from(inputs: WorkflowInputs) -> Self {
1138        Self::Workflow(inputs)
1139    }
1140}
1141
1142impl Serialize for Inputs {
1143    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1144    where
1145        S: serde::Serializer,
1146    {
1147        match self {
1148            Self::Task(inputs) => inputs.serialize(serializer),
1149            Self::Workflow(inputs) => inputs.serialize(serializer),
1150        }
1151    }
1152}