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