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