wdl_engine/
inputs.rs

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