wdl_engine/
inputs.rs

1//! Implementation of workflow and task inputs.
2
3use std::collections::HashMap;
4use std::collections::HashSet;
5use std::fs::File;
6use std::io::BufReader;
7use std::path::Path;
8
9use anyhow::Context;
10use anyhow::Result;
11use anyhow::bail;
12use indexmap::IndexMap;
13use serde::Serialize;
14use serde_json::Value as JsonValue;
15use serde_yaml_ng::Value as YamlValue;
16use wdl_analysis::Document;
17use wdl_analysis::document::Task;
18use wdl_analysis::document::Workflow;
19use wdl_analysis::types::CallKind;
20use wdl_analysis::types::Coercible as _;
21use wdl_analysis::types::Type;
22use wdl_analysis::types::display_types;
23use wdl_analysis::types::v1::task_hint_types;
24use wdl_analysis::types::v1::task_requirement_types;
25
26use crate::Coercible;
27use crate::Value;
28
29/// A type alias to a JSON map (object).
30pub type JsonMap = serde_json::Map<String, JsonValue>;
31
32/// Helper for replacing input paths with a path derived from joining the
33/// specified path with the input path.
34fn join_paths<'a>(
35    inputs: &mut IndexMap<String, Value>,
36    path: impl Fn(&str) -> Result<&'a Path>,
37    ty: impl Fn(&str) -> Option<Type>,
38) -> Result<()> {
39    for (name, value) in inputs.iter_mut() {
40        let ty = match ty(name) {
41            Some(ty) => ty,
42            _ => {
43                continue;
44            }
45        };
46
47        let path = path(name)?;
48
49        // Replace the value with `None` temporarily as we need to coerce the value
50        // This is useful when this value is the only reference to shared data as this
51        // would prevent internal cloning
52        let mut current = std::mem::replace(value, Value::None);
53        if let Ok(mut v) = current.coerce(&ty) {
54            drop(current);
55            v.visit_paths_mut(false, &mut |_, v| {
56                v.expand_path()?;
57                v.join_path_to(path);
58                v.ensure_path_exists(false)
59            })?;
60            current = v;
61        }
62
63        *value = current;
64    }
65
66    Ok(())
67}
68
69/// Represents inputs to a task.
70#[derive(Default, Debug, Clone)]
71pub struct TaskInputs {
72    /// The task input values.
73    inputs: IndexMap<String, Value>,
74    /// The overridden requirements section values.
75    requirements: HashMap<String, Value>,
76    /// The overridden hints section values.
77    hints: HashMap<String, Value>,
78}
79
80impl TaskInputs {
81    /// Iterates the inputs to the task.
82    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
83        self.inputs.iter().map(|(k, v)| (k.as_str(), v))
84    }
85
86    /// Gets an input by name.
87    pub fn get(&self, name: &str) -> Option<&Value> {
88        self.inputs.get(name)
89    }
90
91    /// Sets a task input.
92    ///
93    /// Returns the previous value, if any.
94    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
95        self.inputs.insert(name.into(), value.into())
96    }
97
98    /// Gets an overridden requirement by name.
99    pub fn requirement(&self, name: &str) -> Option<&Value> {
100        self.requirements.get(name)
101    }
102
103    /// Overrides a requirement by name.
104    pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
105        self.requirements.insert(name.into(), value.into());
106    }
107
108    /// Gets an overridden hint by name.
109    pub fn hint(&self, name: &str) -> Option<&Value> {
110        self.hints.get(name)
111    }
112
113    /// Overrides a hint by name.
114    pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
115        self.hints.insert(name.into(), value.into());
116    }
117
118    /// Replaces any `File` or `Directory` input values with joining the
119    /// specified path with the value.
120    ///
121    /// This method will attempt to coerce matching input values to their
122    /// expected types.
123    pub fn join_paths<'a>(
124        &mut self,
125        task: &Task,
126        path: impl Fn(&str) -> Result<&'a Path>,
127    ) -> Result<()> {
128        join_paths(&mut self.inputs, path, |name| {
129            task.inputs().get(name).map(|input| input.ty().clone())
130        })
131    }
132
133    /// Validates the inputs for the given task.
134    ///
135    /// The `specified` set of inputs are those that are present, but may not
136    /// have values available at validation.
137    pub fn validate(
138        &self,
139        document: &Document,
140        task: &Task,
141        specified: Option<&HashSet<String>>,
142    ) -> Result<()> {
143        let version = document.version().context("missing document version")?;
144
145        // Start by validating all the specified inputs and their types
146        for (name, value) in &self.inputs {
147            let input = task
148                .inputs()
149                .get(name)
150                .with_context(|| format!("unknown input `{name}`"))?;
151            let ty = value.ty();
152            if !ty.is_coercible_to(input.ty()) {
153                bail!(
154                    "expected type `{expected_ty}` for input `{name}`, but found `{ty}`",
155                    expected_ty = input.ty(),
156                );
157            }
158        }
159
160        // Next check for missing required inputs
161        for (name, input) in task.inputs() {
162            if input.required()
163                && !self.inputs.contains_key(name)
164                && specified.map(|s| !s.contains(name)).unwrap_or(true)
165            {
166                bail!(
167                    "missing required input `{name}` to task `{task}`",
168                    task = task.name()
169                );
170            }
171        }
172
173        // Check the types of the specified requirements
174        for (name, value) in &self.requirements {
175            let ty = value.ty();
176            if let Some(expected) = task_requirement_types(version, name.as_str()) {
177                if !expected.iter().any(|target| ty.is_coercible_to(target)) {
178                    bail!(
179                        "expected {expected} for requirement `{name}`, but found type `{ty}`",
180                        expected = display_types(expected),
181                    );
182                }
183
184                continue;
185            }
186
187            bail!("unsupported requirement `{name}`");
188        }
189
190        // Check the types of the specified hints
191        for (name, value) in &self.hints {
192            let ty = value.ty();
193            if let Some(expected) = task_hint_types(version, name.as_str(), false) {
194                if !expected.iter().any(|target| ty.is_coercible_to(target)) {
195                    bail!(
196                        "expected {expected} for hint `{name}`, but found type `{ty}`",
197                        expected = display_types(expected),
198                    );
199                }
200            }
201        }
202
203        Ok(())
204    }
205
206    /// Sets a value with dotted path notation.
207    fn set_path_value(
208        &mut self,
209        document: &Document,
210        task: &Task,
211        path: &str,
212        value: Value,
213    ) -> Result<()> {
214        let version = document.version().expect("document should have a version");
215
216        match path.split_once('.') {
217            // The path might contain a requirement or hint
218            Some((key, remainder)) => {
219                let (must_match, matched) = match key {
220                    "runtime" => (
221                        false,
222                        task_requirement_types(version, remainder)
223                            .map(|types| (true, types))
224                            .or_else(|| {
225                                task_hint_types(version, remainder, false)
226                                    .map(|types| (false, types))
227                            }),
228                    ),
229                    "requirements" => (
230                        true,
231                        task_requirement_types(version, remainder).map(|types| (true, types)),
232                    ),
233                    "hints" => (
234                        false,
235                        task_hint_types(version, remainder, false).map(|types| (false, types)),
236                    ),
237                    _ => {
238                        bail!(
239                            "task `{task}` does not have an input named `{path}`",
240                            task = task.name()
241                        );
242                    }
243                };
244
245                if let Some((requirement, expected)) = matched {
246                    for ty in expected {
247                        if value.ty().is_coercible_to(ty) {
248                            if requirement {
249                                self.requirements.insert(remainder.to_string(), value);
250                            } else {
251                                self.hints.insert(remainder.to_string(), value);
252                            }
253                            return Ok(());
254                        }
255                    }
256
257                    bail!(
258                        "expected {expected} for {key} key `{remainder}`, but found type `{ty}`",
259                        expected = display_types(expected),
260                        ty = value.ty()
261                    );
262                } else if must_match {
263                    bail!("unsupported {key} key `{remainder}`");
264                } else {
265                    Ok(())
266                }
267            }
268            // The path is to an input
269            None => {
270                let input = task.inputs().get(path).with_context(|| {
271                    format!(
272                        "task `{name}` does not have an input named `{path}`",
273                        name = task.name()
274                    )
275                })?;
276
277                let actual = value.ty();
278                if !actual.is_coercible_to(input.ty()) {
279                    bail!(
280                        "expected type `{expected}` for input `{path}`, but found type `{actual}`",
281                        expected = input.ty()
282                    );
283                }
284                self.inputs.insert(path.to_string(), value);
285                Ok(())
286            }
287        }
288    }
289}
290
291impl<S, V> FromIterator<(S, V)> for TaskInputs
292where
293    S: Into<String>,
294    V: Into<Value>,
295{
296    fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
297        Self {
298            inputs: iter
299                .into_iter()
300                .map(|(k, v)| (k.into(), v.into()))
301                .collect(),
302            requirements: Default::default(),
303            hints: Default::default(),
304        }
305    }
306}
307
308impl Serialize for TaskInputs {
309    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
310    where
311        S: serde::Serializer,
312    {
313        // Only serialize the input values
314        self.inputs.serialize(serializer)
315    }
316}
317
318/// Represents inputs to a workflow.
319#[derive(Default, Debug, Clone)]
320pub struct WorkflowInputs {
321    /// The workflow input values.
322    inputs: IndexMap<String, Value>,
323    /// The nested call inputs.
324    calls: HashMap<String, Inputs>,
325}
326
327impl WorkflowInputs {
328    /// Iterates the inputs to the workflow.
329    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
330        self.inputs.iter().map(|(k, v)| (k.as_str(), v))
331    }
332
333    /// Gets an input by name.
334    pub fn get(&self, name: &str) -> Option<&Value> {
335        self.inputs.get(name)
336    }
337
338    /// Gets the nested call inputs.
339    pub fn calls(&self) -> &HashMap<String, Inputs> {
340        &self.calls
341    }
342
343    /// Gets the nested call inputs.
344    pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
345        &mut self.calls
346    }
347
348    /// Sets a workflow input.
349    ///
350    /// Returns the previous value, if any.
351    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
352        self.inputs.insert(name.into(), value.into())
353    }
354
355    /// Checks if the inputs contain a value with the specified name.
356    ///
357    /// This does not check nested call inputs.
358    pub fn contains(&self, name: &str) -> bool {
359        self.inputs.contains_key(name)
360    }
361
362    /// Replaces any `File` or `Directory` input values with joining the
363    /// specified path with the value.
364    ///
365    /// This method will attempt to coerce matching input values to their
366    /// expected types.
367    pub fn join_paths<'a>(
368        &mut self,
369        workflow: &Workflow,
370        path: impl Fn(&str) -> Result<&'a Path>,
371    ) -> Result<()> {
372        join_paths(&mut self.inputs, path, |name| {
373            workflow.inputs().get(name).map(|input| input.ty().clone())
374        })
375    }
376
377    /// Validates the inputs for the given workflow.
378    ///
379    /// The `specified` set of inputs are those that are present, but may not
380    /// have values available at validation.
381    pub fn validate(
382        &self,
383        document: &Document,
384        workflow: &Workflow,
385        specified: Option<&HashSet<String>>,
386    ) -> Result<()> {
387        // Start by validating all the specified inputs and their types
388        for (name, value) in &self.inputs {
389            let input = workflow
390                .inputs()
391                .get(name)
392                .with_context(|| format!("unknown input `{name}`"))?;
393            let expected_ty = input.ty();
394            let ty = value.ty();
395            if !ty.is_coercible_to(expected_ty) {
396                bail!("expected type `{expected_ty}` for input `{name}`, but found type `{ty}`");
397            }
398        }
399
400        // Next check for missing required inputs
401        for (name, input) in workflow.inputs() {
402            if input.required()
403                && !self.inputs.contains_key(name)
404                && specified.map(|s| !s.contains(name)).unwrap_or(true)
405            {
406                bail!(
407                    "missing required input `{name}` to workflow `{workflow}`",
408                    workflow = workflow.name()
409                );
410            }
411        }
412
413        // Check that the workflow allows nested inputs
414        if !self.calls.is_empty() && !workflow.allows_nested_inputs() {
415            bail!(
416                "cannot specify a nested call input for workflow `{name}` as it does not allow \
417                 nested inputs",
418                name = workflow.name()
419            );
420        }
421
422        // Check the inputs to the specified calls
423        for (name, inputs) in &self.calls {
424            let call = workflow.calls().get(name).with_context(|| {
425                format!(
426                    "workflow `{workflow}` does not have a call named `{name}`",
427                    workflow = workflow.name()
428                )
429            })?;
430
431            // Resolve the target document; the namespace is guaranteed to be present in the
432            // document.
433            let document = call
434                .namespace()
435                .map(|ns| {
436                    document
437                        .namespace(ns)
438                        .expect("namespace should be present")
439                        .document()
440                })
441                .unwrap_or(document);
442
443            // Validate the call's inputs
444            let inputs = match call.kind() {
445                CallKind::Task => {
446                    let task = document
447                        .task_by_name(call.name())
448                        .expect("task should be present");
449
450                    let task_inputs = inputs.as_task_inputs().with_context(|| {
451                        format!("`{name}` is a call to a task, but workflow inputs were supplied")
452                    })?;
453
454                    task_inputs.validate(document, task, Some(call.specified()))?;
455                    &task_inputs.inputs
456                }
457                CallKind::Workflow => {
458                    let workflow = document.workflow().expect("should have a workflow");
459                    assert_eq!(
460                        workflow.name(),
461                        call.name(),
462                        "call name does not match workflow name"
463                    );
464                    let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
465                        format!("`{name}` is a call to a workflow, but task inputs were supplied")
466                    })?;
467
468                    workflow_inputs.validate(document, workflow, Some(call.specified()))?;
469                    &workflow_inputs.inputs
470                }
471            };
472
473            for input in inputs.keys() {
474                if call.specified().contains(input) {
475                    bail!(
476                        "cannot specify nested input `{input}` for call `{call}` as it was \
477                         explicitly specified in the call itself",
478                        call = call.name(),
479                    );
480                }
481            }
482        }
483
484        // Finally, check for missing call arguments
485        if workflow.allows_nested_inputs() {
486            for (call, ty) in workflow.calls() {
487                let inputs = self.calls.get(call);
488
489                for (input, _) in ty
490                    .inputs()
491                    .iter()
492                    .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
493                {
494                    if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
495                        bail!("missing required input `{input}` for call `{call}`");
496                    }
497                }
498            }
499        }
500
501        Ok(())
502    }
503
504    /// Sets a value with dotted path notation.
505    fn set_path_value(
506        &mut self,
507        document: &Document,
508        workflow: &Workflow,
509        path: &str,
510        value: Value,
511    ) -> Result<()> {
512        match path.split_once('.') {
513            Some((name, remainder)) => {
514                // Check that the workflow allows nested inputs
515                if !workflow.allows_nested_inputs() {
516                    bail!(
517                        "cannot specify a nested call input for workflow `{workflow}` as it does \
518                         not allow nested inputs",
519                        workflow = workflow.name()
520                    );
521                }
522
523                // Resolve the call by name
524                let call = workflow.calls().get(name).with_context(|| {
525                    format!(
526                        "workflow `{workflow}` does not have a call named `{name}`",
527                        workflow = workflow.name()
528                    )
529                })?;
530
531                // Insert the inputs for the call
532                let inputs =
533                    self.calls
534                        .entry(name.to_string())
535                        .or_insert_with(|| match call.kind() {
536                            CallKind::Task => Inputs::Task(Default::default()),
537                            CallKind::Workflow => Inputs::Workflow(Default::default()),
538                        });
539
540                // Resolve the target document; the namespace is guaranteed to be present in the
541                // document.
542                let document = call
543                    .namespace()
544                    .map(|ns| {
545                        document
546                            .namespace(ns)
547                            .expect("namespace should be present")
548                            .document()
549                    })
550                    .unwrap_or(document);
551
552                let next = remainder
553                    .split_once('.')
554                    .map(|(n, _)| n)
555                    .unwrap_or(remainder);
556                if call.specified().contains(next) {
557                    bail!(
558                        "cannot specify nested input `{next}` for call `{name}` as it was \
559                         explicitly specified in the call itself",
560                    );
561                }
562
563                // Recurse on the call's inputs to set the value
564                match call.kind() {
565                    CallKind::Task => {
566                        let task = document
567                            .task_by_name(call.name())
568                            .expect("task should be present");
569                        inputs
570                            .as_task_inputs_mut()
571                            .expect("should be a task input")
572                            .set_path_value(document, task, remainder, value)
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                        inputs
582                            .as_workflow_inputs_mut()
583                            .expect("should be a task input")
584                            .set_path_value(document, workflow, remainder, value)
585                    }
586                }
587            }
588            None => {
589                let input = workflow.inputs().get(path).with_context(|| {
590                    format!(
591                        "workflow `{workflow}` does not have an input named `{path}`",
592                        workflow = workflow.name()
593                    )
594                })?;
595
596                let expected = input.ty();
597                let actual = value.ty();
598                if !actual.is_coercible_to(expected) {
599                    bail!(
600                        "expected type `{expected}` for input `{path}`, but found type `{actual}`"
601                    );
602                }
603                self.inputs.insert(path.to_string(), value);
604                Ok(())
605            }
606        }
607    }
608}
609
610impl<S, V> FromIterator<(S, V)> for WorkflowInputs
611where
612    S: Into<String>,
613    V: Into<Value>,
614{
615    fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
616        Self {
617            inputs: iter
618                .into_iter()
619                .map(|(k, v)| (k.into(), v.into()))
620                .collect(),
621            calls: Default::default(),
622        }
623    }
624}
625
626impl Serialize for WorkflowInputs {
627    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
628    where
629        S: serde::Serializer,
630    {
631        // Note: for serializing, only serialize the direct inputs, not the nested
632        // inputs
633        self.inputs.serialize(serializer)
634    }
635}
636
637/// Represents inputs to a WDL workflow or task.
638#[derive(Debug, Clone)]
639pub enum Inputs {
640    /// The inputs are to a task.
641    Task(TaskInputs),
642    /// The inputs are to a workflow.
643    Workflow(WorkflowInputs),
644}
645
646impl Inputs {
647    /// Parses an inputs file from the given file path.
648    ///
649    /// The format (JSON or YAML) is determined by the file extension:
650    ///
651    /// - `.json` for JSON format
652    /// - `.yml` or `.yaml` for YAML format
653    ///
654    /// The parse uses the provided document to validate the input keys within
655    /// the file.
656    ///
657    /// Returns `Ok(Some(_))` if the inputs are not empty.
658    ///
659    /// Returns `Ok(None)` if the inputs are empty.
660    pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
661        let path = path.as_ref();
662
663        match path.extension().and_then(|ext| ext.to_str()) {
664            Some("json") => Self::parse_json(document, path),
665            Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
666            ext => bail!(
667                "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
668                 and YAML (`.yaml` and `.yml`)",
669                ext = ext.unwrap_or("")
670            ),
671        }
672        .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
673    }
674
675    /// Parses a JSON inputs file from the given file path.
676    ///
677    /// The parse uses the provided document to validate the input keys within
678    /// the file.
679    ///
680    /// Returns `Ok(Some(_))` if the inputs are not empty.
681    ///
682    /// Returns `Ok(None)` if the inputs are empty.
683    pub fn parse_json(
684        document: &Document,
685        path: impl AsRef<Path>,
686    ) -> Result<Option<(String, Self)>> {
687        let path = path.as_ref();
688
689        let file = File::open(path).with_context(|| {
690            format!("failed to open input file `{path}`", path = path.display())
691        })?;
692
693        // Parse the JSON (should be an object)
694        let reader = BufReader::new(file);
695
696        let map = std::mem::take(
697            serde_json::from_reader::<_, JsonValue>(reader)?
698                .as_object_mut()
699                .with_context(|| {
700                    format!(
701                        "expected input file `{path}` to contain a JSON object",
702                        path = path.display()
703                    )
704                })?,
705        );
706
707        Self::parse_object(document, map)
708    }
709
710    /// Parses a YAML inputs file from the given file path.
711    ///
712    /// The parse uses the provided document to validate the input keys within
713    /// the file.
714    ///
715    /// Returns `Ok(Some(_))` if the inputs are not empty.
716    ///
717    /// Returns `Ok(None)` if the inputs are empty.
718    pub fn parse_yaml(
719        document: &Document,
720        path: impl AsRef<Path>,
721    ) -> Result<Option<(String, Self)>> {
722        let path = path.as_ref();
723
724        let file = File::open(path).with_context(|| {
725            format!("failed to open input file `{path}`", path = path.display())
726        })?;
727
728        // Parse the YAML
729        let reader = BufReader::new(file);
730        let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
731
732        // Convert YAML to JSON format
733        let mut json = serde_json::to_value(yaml).with_context(|| {
734            format!(
735                "failed to convert YAML to JSON for processing `{path}`",
736                path = path.display()
737            )
738        })?;
739
740        let object = std::mem::take(json.as_object_mut().with_context(|| {
741            format!(
742                "expected input file `{path}` to contain a YAML mapping",
743                path = path.display()
744            )
745        })?);
746
747        Self::parse_object(document, object)
748    }
749
750    /// Gets an input value.
751    pub fn get(&self, name: &str) -> Option<&Value> {
752        match self {
753            Self::Task(t) => t.inputs.get(name),
754            Self::Workflow(w) => w.inputs.get(name),
755        }
756    }
757
758    /// Sets an input value.
759    ///
760    /// Returns the previous value, if any.
761    pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
762        match self {
763            Self::Task(inputs) => inputs.set(name, value),
764            Self::Workflow(inputs) => inputs.set(name, value),
765        }
766    }
767
768    /// Gets the task inputs.
769    ///
770    /// Returns `None` if the inputs are for a workflow.
771    pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
772        match self {
773            Self::Task(inputs) => Some(inputs),
774            Self::Workflow(_) => None,
775        }
776    }
777
778    /// Gets a mutable reference to task inputs.
779    ///
780    /// Returns `None` if the inputs are for a workflow.
781    pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
782        match self {
783            Self::Task(inputs) => Some(inputs),
784            Self::Workflow(_) => None,
785        }
786    }
787
788    /// Unwraps the inputs as task inputs.
789    ///
790    /// # Panics
791    ///
792    /// Panics if the inputs are for a workflow.
793    pub fn unwrap_task_inputs(self) -> TaskInputs {
794        match self {
795            Self::Task(inputs) => inputs,
796            Self::Workflow(_) => panic!("inputs are for a workflow"),
797        }
798    }
799
800    /// Gets the workflow inputs.
801    ///
802    /// Returns `None` if the inputs are for a task.
803    pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
804        match self {
805            Self::Task(_) => None,
806            Self::Workflow(inputs) => Some(inputs),
807        }
808    }
809
810    /// Gets a mutable reference to workflow inputs.
811    ///
812    /// Returns `None` if the inputs are for a task.
813    pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
814        match self {
815            Self::Task(_) => None,
816            Self::Workflow(inputs) => Some(inputs),
817        }
818    }
819
820    /// Unwraps the inputs as workflow inputs.
821    ///
822    /// # Panics
823    ///
824    /// Panics if the inputs are for a task.
825    pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
826        match self {
827            Self::Task(_) => panic!("inputs are for a task"),
828            Self::Workflow(inputs) => inputs,
829        }
830    }
831
832    /// Parses the root object in an input file.
833    ///
834    /// Returns `Ok(Some(_))` if the inputs are not empty.
835    ///
836    /// Returns `Ok(None)` if the inputs are empty.
837    pub fn parse_object(document: &Document, object: JsonMap) -> Result<Option<(String, Self)>> {
838        // Determine the root workflow or task name
839        let (key, name) = match object.iter().next() {
840            Some((key, _)) => match key.split_once('.') {
841                Some((name, _)) => (key, name),
842                None => {
843                    bail!(
844                        "invalid input key `{key}`: expected the value to be prefixed with the \
845                         workflow or task name",
846                    )
847                }
848            },
849            // If the object is empty, treat it as a workflow evaluation without any inputs
850            None => {
851                return Ok(None);
852            }
853        };
854
855        match (document.task_by_name(name), document.workflow()) {
856            (Some(task), _) => Ok(Some(Self::parse_task_inputs(document, task, object)?)),
857            (None, Some(workflow)) if workflow.name() == name => Ok(Some(
858                Self::parse_workflow_inputs(document, workflow, object)?,
859            )),
860            _ => bail!(
861                "invalid input key `{key}`: a task or workflow named `{name}` does not exist in \
862                 the document"
863            ),
864        }
865    }
866
867    /// Parses the inputs for a task.
868    fn parse_task_inputs(
869        document: &Document,
870        task: &Task,
871        object: JsonMap,
872    ) -> Result<(String, Self)> {
873        let mut inputs = TaskInputs::default();
874        for (key, value) in object {
875            let value = serde_json::from_value(value)
876                .with_context(|| format!("invalid input key `{key}`"))?;
877
878            match key.split_once(".") {
879                Some((prefix, remainder)) if prefix == task.name() => {
880                    inputs
881                        .set_path_value(document, task, remainder, value)
882                        .with_context(|| format!("invalid input key `{key}`"))?;
883                }
884                _ => {
885                    bail!(
886                        "invalid input key `{key}`: expected key to be prefixed with `{task}`",
887                        task = task.name()
888                    );
889                }
890            }
891        }
892
893        Ok((task.name().to_string(), Inputs::Task(inputs)))
894    }
895
896    /// Parses the inputs for a workflow.
897    fn parse_workflow_inputs(
898        document: &Document,
899        workflow: &Workflow,
900        object: JsonMap,
901    ) -> Result<(String, Self)> {
902        let mut inputs = WorkflowInputs::default();
903        for (key, value) in object {
904            let value = serde_json::from_value(value)
905                .with_context(|| format!("invalid input key `{key}`"))?;
906
907            match key.split_once(".") {
908                Some((prefix, remainder)) if prefix == workflow.name() => {
909                    inputs
910                        .set_path_value(document, workflow, remainder, value)
911                        .with_context(|| format!("invalid input key `{key}`"))?;
912                }
913                _ => {
914                    bail!(
915                        "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
916                        workflow = workflow.name()
917                    );
918                }
919            }
920        }
921
922        Ok((workflow.name().to_string(), Inputs::Workflow(inputs)))
923    }
924}
925
926impl From<TaskInputs> for Inputs {
927    fn from(inputs: TaskInputs) -> Self {
928        Self::Task(inputs)
929    }
930}
931
932impl From<WorkflowInputs> for Inputs {
933    fn from(inputs: WorkflowInputs) -> Self {
934        Self::Workflow(inputs)
935    }
936}