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