1use 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::EvaluationPath;
34use crate::Value;
35
36pub type JsonMap = serde_json::Map<String, JsonValue>;
38
39fn check_input_type(document: &Document, name: &str, input: &Input, value: &Value) -> Result<()> {
41 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#[derive(Default, Debug, Clone)]
65pub struct TaskInputs {
66 inputs: IndexMap<String, Value>,
68 requirements: HashMap<String, Value>,
70 hints: HashMap<String, Value>,
72}
73
74impl TaskInputs {
75 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
77 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
78 }
79
80 pub fn is_empty(&self) -> bool {
82 self.len() == 0
83 }
84
85 pub fn len(&self) -> usize {
89 self.inputs.len() + self.requirements.len() + self.hints.len()
90 }
91
92 pub fn get(&self, name: &str) -> Option<&Value> {
94 self.inputs.get(name)
95 }
96
97 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
101 self.inputs.insert(name.into(), value.into())
102 }
103
104 pub fn requirement(&self, name: &str) -> Option<&Value> {
106 self.requirements.get(name)
107 }
108
109 pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
111 self.requirements.insert(name.into(), value.into());
112 }
113
114 pub fn hint(&self, name: &str) -> Option<&Value> {
116 self.hints.get(name)
117 }
118
119 pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
121 self.hints.insert(name.into(), value.into());
122 }
123
124 pub async fn join_paths<'a>(
130 &mut self,
131 task: &Task,
132 path: impl Fn(&str) -> Result<&'a EvaluationPath>,
133 ) -> Result<()> {
134 for (name, value) in self.inputs.iter_mut() {
135 let Some(ty) = task.inputs().get(name).map(|input| input.ty().clone()) else {
136 bail!("could not find an expected type for input {name}");
137 };
138
139 let base_dir = path(name)?;
140
141 if let Ok(v) = value.coerce(None, &ty) {
142 *value = v
143 .resolve_paths(ty.is_optional(), None, None, &|path| path.expand(base_dir))
144 .await?;
145 }
146 }
147 Ok(())
148 }
149
150 pub fn validate(
155 &self,
156 document: &Document,
157 task: &Task,
158 specified: Option<&HashSet<String>>,
159 ) -> Result<()> {
160 let version = document.version().context("missing document version")?;
161
162 for (name, value) in &self.inputs {
164 let input = task
165 .inputs()
166 .get(name)
167 .with_context(|| format!("unknown input `{name}`"))?;
168
169 check_input_type(document, name, input, value)?;
170 }
171
172 for (name, input) in task.inputs() {
174 if input.required()
175 && !self.inputs.contains_key(name)
176 && specified.map(|s| !s.contains(name)).unwrap_or(true)
177 {
178 bail!(
179 "missing required input `{name}` to task `{task}`",
180 task = task.name()
181 );
182 }
183 }
184
185 for (name, value) in &self.requirements {
187 let ty = value.ty();
188 if let Some(expected) = task_requirement_types(version, name.as_str()) {
189 if !expected.iter().any(|target| ty.is_coercible_to(target)) {
190 bail!(
191 "expected {expected} for requirement `{name}`, but found type `{ty}`",
192 expected = display_types(expected),
193 );
194 }
195
196 continue;
197 }
198
199 bail!("unsupported requirement `{name}`");
200 }
201
202 for (name, value) in &self.hints {
204 let ty = value.ty();
205 if let Some(expected) = task_hint_types(version, name.as_str(), false)
206 && !expected.iter().any(|target| ty.is_coercible_to(target))
207 {
208 bail!(
209 "expected {expected} for hint `{name}`, but found type `{ty}`",
210 expected = display_types(expected),
211 );
212 }
213 }
214
215 Ok(())
216 }
217
218 fn set_path_value(
228 &mut self,
229 document: &Document,
230 task: &Task,
231 path: &str,
232 value: Value,
233 ) -> Result<bool> {
234 let version = document.version().expect("document should have a version");
235
236 match path.split_once('.') {
237 Some((key, remainder)) => {
239 let (must_match, matched) = match key {
240 "runtime" => (
241 false,
242 task_requirement_types(version, remainder)
243 .map(|types| (true, types))
244 .or_else(|| {
245 task_hint_types(version, remainder, false)
246 .map(|types| (false, types))
247 }),
248 ),
249 "requirements" => (
250 true,
251 task_requirement_types(version, remainder).map(|types| (true, types)),
252 ),
253 "hints" => (
254 false,
255 task_hint_types(version, remainder, false).map(|types| (false, types)),
256 ),
257 _ => {
258 bail!(
259 "task `{task}` does not have an input named `{path}`",
260 task = task.name()
261 );
262 }
263 };
264
265 if let Some((requirement, expected)) = matched {
266 for ty in expected {
267 if value.ty().is_coercible_to(ty) {
268 if requirement {
269 self.requirements.insert(remainder.to_string(), value);
270 } else {
271 self.hints.insert(remainder.to_string(), value);
272 }
273 return Ok(false);
274 }
275 }
276
277 bail!(
278 "expected {expected} for {key} key `{remainder}`, but found type `{ty}`",
279 expected = display_types(expected),
280 ty = value.ty()
281 );
282 } else if must_match {
283 bail!("unsupported {key} key `{remainder}`");
284 } else {
285 Ok(false)
286 }
287 }
288 None => {
290 let input = task.inputs().get(path).with_context(|| {
291 format!(
292 "task `{name}` does not have an input named `{path}`",
293 name = task.name()
294 )
295 })?;
296
297 let actual = value.ty();
299 let expected = input.ty();
300 if let Some(PrimitiveType::String) = expected.as_primitive()
301 && let Some(actual) = actual.as_primitive()
302 && actual != PrimitiveType::String
303 {
304 self.inputs
305 .insert(path.to_string(), value.to_string().into());
306 return Ok(true);
307 }
308
309 check_input_type(document, path, input, &value)?;
310 self.inputs.insert(path.to_string(), value);
311 Ok(true)
312 }
313 }
314 }
315}
316
317impl<S, V> FromIterator<(S, V)> for TaskInputs
318where
319 S: Into<String>,
320 V: Into<Value>,
321{
322 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
323 Self {
324 inputs: iter
325 .into_iter()
326 .map(|(k, v)| (k.into(), v.into()))
327 .collect(),
328 requirements: Default::default(),
329 hints: Default::default(),
330 }
331 }
332}
333
334impl Serialize for TaskInputs {
335 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
336 where
337 S: serde::Serializer,
338 {
339 let mut map = serializer.serialize_map(Some(self.len()))?;
340
341 for (k, v) in &self.inputs {
342 let v = crate::ValueSerializer::new(None, v, true);
343 map.serialize_entry(k, &v)?;
344 }
345
346 for (k, v) in &self.requirements {
347 let v = crate::ValueSerializer::new(None, v, true);
348 map.serialize_entry(&format!("requirements.{k}"), &v)?;
349 }
350
351 for (k, v) in &self.hints {
352 let v = crate::ValueSerializer::new(None, v, true);
353 map.serialize_entry(&format!("hints.{k}"), &v)?;
354 }
355
356 map.end()
357 }
358}
359
360#[derive(Default, Debug, Clone)]
362pub struct WorkflowInputs {
363 inputs: IndexMap<String, Value>,
365 calls: HashMap<String, Inputs>,
367}
368
369impl WorkflowInputs {
370 pub fn has_nested_inputs(&self) -> bool {
375 self.calls.values().any(|inputs| match inputs {
376 Inputs::Task(task) => !task.inputs.is_empty(),
377 Inputs::Workflow(workflow) => workflow.has_nested_inputs(),
378 })
379 }
380
381 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
383 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
384 }
385
386 pub fn is_empty(&self) -> bool {
388 self.len() == 0
389 }
390
391 pub fn len(&self) -> usize {
395 self.inputs.len() + self.calls.values().map(Inputs::len).sum::<usize>()
396 }
397
398 pub fn get(&self, name: &str) -> Option<&Value> {
400 self.inputs.get(name)
401 }
402
403 pub fn calls(&self) -> &HashMap<String, Inputs> {
405 &self.calls
406 }
407
408 pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
410 &mut self.calls
411 }
412
413 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
417 self.inputs.insert(name.into(), value.into())
418 }
419
420 pub fn contains(&self, name: &str) -> bool {
424 self.inputs.contains_key(name)
425 }
426
427 pub async fn join_paths<'a>(
433 &mut self,
434 workflow: &Workflow,
435 path: impl Fn(&str) -> Result<&'a EvaluationPath>,
436 ) -> Result<()> {
437 for (name, value) in self.inputs.iter_mut() {
438 let Some(ty) = workflow.inputs().get(name).map(|input| input.ty().clone()) else {
439 bail!("could not find an expected type for input {name}");
440 };
441
442 let base_dir = path(name)?;
443
444 if let Ok(v) = value.coerce(None, &ty) {
445 *value = v
446 .resolve_paths(ty.is_optional(), None, None, &|path| path.expand(base_dir))
447 .await?;
448 }
449 }
450 Ok(())
451 }
452
453 pub fn validate(
458 &self,
459 document: &Document,
460 workflow: &Workflow,
461 specified: Option<&HashSet<String>>,
462 ) -> Result<()> {
463 for (name, value) in &self.inputs {
465 let input = workflow
466 .inputs()
467 .get(name)
468 .with_context(|| format!("unknown input `{name}`"))?;
469 check_input_type(document, name, input, value)?;
470 }
471
472 for (name, input) in workflow.inputs() {
474 if input.required()
475 && !self.inputs.contains_key(name)
476 && specified.map(|s| !s.contains(name)).unwrap_or(true)
477 {
478 bail!(
479 "missing required input `{name}` to workflow `{workflow}`",
480 workflow = workflow.name()
481 );
482 }
483 }
484
485 if !workflow.allows_nested_inputs() && self.has_nested_inputs() {
487 bail!(
488 "cannot specify a nested call input for workflow `{name}` as it does not allow \
489 nested inputs",
490 name = workflow.name()
491 );
492 }
493
494 for (name, inputs) in &self.calls {
496 let call = workflow.calls().get(name).with_context(|| {
497 format!(
498 "workflow `{workflow}` does not have a call named `{name}`",
499 workflow = workflow.name()
500 )
501 })?;
502
503 let document = call
506 .namespace()
507 .map(|ns| {
508 document
509 .namespace(ns)
510 .expect("namespace should be present")
511 .document()
512 })
513 .unwrap_or(document);
514
515 let inputs = match call.kind() {
517 CallKind::Task => {
518 let task = document
519 .task_by_name(call.name())
520 .expect("task should be present");
521
522 let task_inputs = inputs.as_task_inputs().with_context(|| {
523 format!("`{name}` is a call to a task, but workflow inputs were supplied")
524 })?;
525
526 task_inputs.validate(document, task, Some(call.specified()))?;
527 &task_inputs.inputs
528 }
529 CallKind::Workflow => {
530 let workflow = document.workflow().expect("should have a workflow");
531 assert_eq!(
532 workflow.name(),
533 call.name(),
534 "call name does not match workflow name"
535 );
536 let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
537 format!("`{name}` is a call to a workflow, but task inputs were supplied")
538 })?;
539
540 workflow_inputs.validate(document, workflow, Some(call.specified()))?;
541 &workflow_inputs.inputs
542 }
543 };
544
545 for input in inputs.keys() {
546 if call.specified().contains(input) {
547 bail!(
548 "cannot specify nested input `{input}` for call `{call}` as it was \
549 explicitly specified in the call itself",
550 call = call.name(),
551 );
552 }
553 }
554 }
555
556 if workflow.allows_nested_inputs() {
558 for (call, ty) in workflow.calls() {
559 let inputs = self.calls.get(call);
560
561 for (input, _) in ty
562 .inputs()
563 .iter()
564 .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
565 {
566 if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
567 bail!("missing required input `{input}` for call `{call}`");
568 }
569 }
570 }
571 }
572
573 Ok(())
574 }
575
576 fn set_path_value(
585 &mut self,
586 document: &Document,
587 workflow: &Workflow,
588 path: &str,
589 value: Value,
590 ) -> Result<bool> {
591 match path.split_once('.') {
592 Some((name, remainder)) => {
593 let call = workflow.calls().get(name).with_context(|| {
595 format!(
596 "workflow `{workflow}` does not have a call named `{name}`",
597 workflow = workflow.name()
598 )
599 })?;
600
601 let inputs =
603 self.calls
604 .entry(name.to_string())
605 .or_insert_with(|| match call.kind() {
606 CallKind::Task => Inputs::Task(Default::default()),
607 CallKind::Workflow => Inputs::Workflow(Default::default()),
608 });
609
610 let document = call
613 .namespace()
614 .map(|ns| {
615 document
616 .namespace(ns)
617 .expect("namespace should be present")
618 .document()
619 })
620 .unwrap_or(document);
621
622 let next = remainder
623 .split_once('.')
624 .map(|(n, _)| n)
625 .unwrap_or(remainder);
626 if call.specified().contains(next) {
627 bail!(
628 "cannot specify nested input `{next}` for call `{name}` as it was \
629 explicitly specified in the call itself",
630 );
631 }
632
633 let input = match call.kind() {
635 CallKind::Task => {
636 let task = document
637 .task_by_name(call.name())
638 .expect("task should be present");
639 inputs
640 .as_task_inputs_mut()
641 .expect("should be a task input")
642 .set_path_value(document, task, remainder, value)?
643 }
644 CallKind::Workflow => {
645 let workflow = document.workflow().expect("should have a workflow");
646 assert_eq!(
647 workflow.name(),
648 call.name(),
649 "call name does not match workflow name"
650 );
651 inputs
652 .as_workflow_inputs_mut()
653 .expect("should be a task input")
654 .set_path_value(document, workflow, remainder, value)?
655 }
656 };
657
658 if input && !workflow.allows_nested_inputs() {
659 bail!(
660 "cannot specify a nested call input for workflow `{workflow}` as it does \
661 not allow nested inputs",
662 workflow = workflow.name()
663 );
664 }
665
666 Ok(input)
667 }
668 None => {
669 let input = workflow.inputs().get(path).with_context(|| {
670 format!(
671 "workflow `{workflow}` does not have an input named `{path}`",
672 workflow = workflow.name()
673 )
674 })?;
675
676 let actual = value.ty();
678 let expected = input.ty();
679 if let Some(PrimitiveType::String) = expected.as_primitive()
680 && let Some(actual) = actual.as_primitive()
681 && actual != PrimitiveType::String
682 {
683 self.inputs
684 .insert(path.to_string(), value.to_string().into());
685 return Ok(true);
686 }
687
688 check_input_type(document, path, input, &value)?;
689 self.inputs.insert(path.to_string(), value);
690 Ok(true)
691 }
692 }
693 }
694}
695
696impl<S, V> FromIterator<(S, V)> for WorkflowInputs
697where
698 S: Into<String>,
699 V: Into<Value>,
700{
701 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
702 Self {
703 inputs: iter
704 .into_iter()
705 .map(|(k, v)| (k.into(), v.into()))
706 .collect(),
707 calls: Default::default(),
708 }
709 }
710}
711
712impl Serialize for WorkflowInputs {
713 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
714 where
715 S: serde::Serializer,
716 {
717 let mut map = serializer.serialize_map(Some(self.len()))?;
718 for (k, v) in &self.inputs {
719 let serialized_value = crate::ValueSerializer::new(None, v, true);
720 map.serialize_entry(k, &serialized_value)?;
721 }
722
723 for (k, v) in &self.calls {
724 let serialized = serde_json::to_value(v).map_err(|_| {
725 serde::ser::Error::custom(format!("failed to serialize inputs for call `{k}`"))
726 })?;
727 let mut map = serde_json::Map::new();
728 if let JsonValue::Object(obj) = serialized {
729 for (inner, value) in obj {
730 map.insert(format!("{k}.{inner}"), value);
731 }
732 }
733 }
734
735 map.end()
736 }
737}
738
739#[derive(Debug, Clone)]
741pub enum Inputs {
742 Task(TaskInputs),
744 Workflow(WorkflowInputs),
746}
747
748impl Inputs {
749 pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
763 let path = path.as_ref();
764
765 match path.extension().and_then(|ext| ext.to_str()) {
766 Some("json") => Self::parse_json(document, path),
767 Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
768 ext => bail!(
769 "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
770 and YAML (`.yaml` and `.yml`)",
771 ext = ext.unwrap_or("")
772 ),
773 }
774 .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
775 }
776
777 pub fn parse_json(
786 document: &Document,
787 path: impl AsRef<Path>,
788 ) -> Result<Option<(String, Self)>> {
789 let path = path.as_ref();
790
791 let file = File::open(path).with_context(|| {
792 format!("failed to open input file `{path}`", path = path.display())
793 })?;
794
795 let reader = BufReader::new(file);
797
798 let map = std::mem::take(
799 serde_json::from_reader::<_, JsonValue>(reader)?
800 .as_object_mut()
801 .with_context(|| {
802 format!(
803 "expected input file `{path}` to contain a JSON object",
804 path = path.display()
805 )
806 })?,
807 );
808
809 Self::parse_json_object(document, map)
810 }
811
812 pub fn parse_yaml(
821 document: &Document,
822 path: impl AsRef<Path>,
823 ) -> Result<Option<(String, Self)>> {
824 let path = path.as_ref();
825
826 let file = File::open(path).with_context(|| {
827 format!("failed to open input file `{path}`", path = path.display())
828 })?;
829
830 let reader = BufReader::new(file);
832 let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
833
834 let mut json = serde_json::to_value(yaml).with_context(|| {
836 format!(
837 "failed to convert YAML to JSON for processing `{path}`",
838 path = path.display()
839 )
840 })?;
841
842 let object = std::mem::take(json.as_object_mut().with_context(|| {
843 format!(
844 "expected input file `{path}` to contain a YAML mapping",
845 path = path.display()
846 )
847 })?);
848
849 Self::parse_json_object(document, object)
850 }
851
852 pub fn is_empty(&self) -> bool {
854 self.len() == 0
855 }
856
857 pub fn len(&self) -> usize {
863 match self {
864 Self::Task(inputs) => inputs.len(),
865 Self::Workflow(inputs) => inputs.len(),
866 }
867 }
868
869 pub fn get(&self, name: &str) -> Option<&Value> {
871 match self {
872 Self::Task(t) => t.inputs.get(name),
873 Self::Workflow(w) => w.inputs.get(name),
874 }
875 }
876
877 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
881 match self {
882 Self::Task(inputs) => inputs.set(name, value),
883 Self::Workflow(inputs) => inputs.set(name, value),
884 }
885 }
886
887 pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
891 match self {
892 Self::Task(inputs) => Some(inputs),
893 Self::Workflow(_) => None,
894 }
895 }
896
897 pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
901 match self {
902 Self::Task(inputs) => Some(inputs),
903 Self::Workflow(_) => None,
904 }
905 }
906
907 pub fn unwrap_task_inputs(self) -> TaskInputs {
913 match self {
914 Self::Task(inputs) => inputs,
915 Self::Workflow(_) => panic!("inputs are for a workflow"),
916 }
917 }
918
919 pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
923 match self {
924 Self::Task(_) => None,
925 Self::Workflow(inputs) => Some(inputs),
926 }
927 }
928
929 pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
933 match self {
934 Self::Task(_) => None,
935 Self::Workflow(inputs) => Some(inputs),
936 }
937 }
938
939 pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
945 match self {
946 Self::Task(_) => panic!("inputs are for a task"),
947 Self::Workflow(inputs) => inputs,
948 }
949 }
950
951 pub fn parse_json_object(
957 document: &Document,
958 object: JsonMap,
959 ) -> Result<Option<(String, Self)>> {
960 if object.is_empty() {
962 return Ok(None);
963 }
964
965 let mut target_candidates = BTreeSet::new();
968 for key in object.keys() {
969 let Some((prefix, _)) = key.split_once('.') else {
970 bail!(
971 "invalid input key `{key}`: expected the key to be prefixed with the workflow \
972 or task name",
973 )
974 };
975 target_candidates.insert(prefix);
976 }
977
978 let target_name = match target_candidates
981 .iter()
982 .take(2)
983 .collect::<Vec<_>>()
984 .as_slice()
985 {
986 [] => panic!("no target candidates for inputs; report this as a bug"),
987 [target_name] => target_name.to_string(),
988 _ => bail!(
989 "invalid inputs: expected each input key to be prefixed with the same workflow or \
990 task name, but found multiple prefixes: {target_candidates:?}",
991 ),
992 };
993
994 let inputs = match (document.task_by_name(&target_name), document.workflow()) {
995 (Some(task), _) => Self::parse_task_inputs(document, task, object)?,
996 (None, Some(workflow)) if workflow.name() == target_name => {
997 Self::parse_workflow_inputs(document, workflow, object)?
998 }
999 _ => bail!(
1000 "invalid inputs: a task or workflow named `{target_name}` does not exist in the \
1001 document"
1002 ),
1003 };
1004 Ok(Some((target_name, inputs)))
1005 }
1006
1007 fn parse_task_inputs(document: &Document, task: &Task, object: JsonMap) -> Result<Self> {
1009 let mut inputs = TaskInputs::default();
1010 for (key, value) in object {
1011 let value = serde_json::from_value(value)
1013 .with_context(|| format!("invalid input value for key `{key}`"))?;
1014
1015 match key.split_once(".") {
1016 Some((prefix, remainder)) if prefix == task.name() => {
1017 inputs
1018 .set_path_value(document, task, remainder, value)
1019 .with_context(|| format!("invalid input key `{key}`"))?;
1020 }
1021 _ => {
1022 bail!(
1026 "invalid input key `{key}`: expected key to be prefixed with `{task}`",
1027 task = task.name()
1028 );
1029 }
1030 }
1031 }
1032
1033 Ok(Inputs::Task(inputs))
1034 }
1035
1036 fn parse_workflow_inputs(
1038 document: &Document,
1039 workflow: &Workflow,
1040 object: JsonMap,
1041 ) -> Result<Self> {
1042 let mut inputs = WorkflowInputs::default();
1043 for (key, value) in object {
1044 let value = serde_json::from_value(value)
1046 .with_context(|| format!("invalid input value for key `{key}`"))?;
1047
1048 match key.split_once(".") {
1049 Some((prefix, remainder)) if prefix == workflow.name() => {
1050 inputs
1051 .set_path_value(document, workflow, remainder, value)
1052 .with_context(|| format!("invalid input key `{key}`"))?;
1053 }
1054 _ => {
1055 bail!(
1059 "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
1060 workflow = workflow.name()
1061 );
1062 }
1063 }
1064 }
1065
1066 Ok(Inputs::Workflow(inputs))
1067 }
1068}
1069
1070impl From<TaskInputs> for Inputs {
1071 fn from(inputs: TaskInputs) -> Self {
1072 Self::Task(inputs)
1073 }
1074}
1075
1076impl From<WorkflowInputs> for Inputs {
1077 fn from(inputs: WorkflowInputs) -> Self {
1078 Self::Workflow(inputs)
1079 }
1080}
1081
1082impl Serialize for Inputs {
1083 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1084 where
1085 S: serde::Serializer,
1086 {
1087 match self {
1088 Self::Task(inputs) => inputs.serialize(serializer),
1089 Self::Workflow(inputs) => inputs.serialize(serializer),
1090 }
1091 }
1092}