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;
29
30use crate::Array;
31use crate::Coercible;
32use crate::CompoundValue;
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 input.ty().optional()
46 } else {
47 input.ty().clone()
48 };
49
50 let ty = value.ty();
51 if !ty.is_coercible_to(&expected_ty) {
52 bail!("expected {expected_ty:#} for input `{name}`, but found {ty:#}");
53 }
54
55 Ok(())
56}
57
58async fn resolve_with_origins(
64 value: Value,
65 ty: &wdl_analysis::types::Type,
66 origins: &[EvaluationPath],
67) -> Result<Value> {
68 if origins.len() > 1
69 && let Value::Compound(CompoundValue::Array(ref array)) = value
70 {
71 let arr_ty = ty.as_array().expect("should be an array type");
72 assert_eq!(
73 origins.len(),
74 array.as_slice().len(),
75 "the number of origins should match the number of array elements"
76 );
77 let optional = arr_ty.element_type().is_optional();
78 let mut resolved = Vec::with_capacity(array.as_slice().len());
79 for (elem, base_dir) in array.as_slice().iter().zip(origins) {
80 resolved.push(
81 elem.resolve_paths(optional, None, None, &|p| p.expand(base_dir))
82 .await?,
83 );
84 }
85 return Ok(Value::Compound(CompoundValue::Array(Array::new_unchecked(
86 arr_ty.clone(),
87 resolved,
88 ))));
89 }
90
91 let base_dir = &origins[0];
92 value
93 .resolve_paths(ty.is_optional(), None, None, &|p| p.expand(base_dir))
94 .await
95}
96
97#[derive(Default, Debug, Clone)]
99pub struct TaskInputs {
100 inputs: IndexMap<String, Value>,
102 requirements: HashMap<String, Value>,
104 hints: HashMap<String, Value>,
106}
107
108impl TaskInputs {
109 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
111 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
112 }
113
114 pub fn is_empty(&self) -> bool {
116 self.len() == 0
117 }
118
119 pub fn len(&self) -> usize {
123 self.inputs.len() + self.requirements.len() + self.hints.len()
124 }
125
126 pub fn get(&self, name: &str) -> Option<&Value> {
128 self.inputs.get(name)
129 }
130
131 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
135 self.inputs.insert(name.into(), value.into())
136 }
137
138 pub fn requirement(&self, name: &str) -> Option<&Value> {
140 self.requirements.get(name)
141 }
142
143 pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
145 self.requirements.insert(name.into(), value.into());
146 }
147
148 pub fn hint(&self, name: &str) -> Option<&Value> {
150 self.hints.get(name)
151 }
152
153 pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
155 self.hints.insert(name.into(), value.into());
156 }
157
158 pub async fn join_paths<'a>(
164 &mut self,
165 task: &Task,
166 path: impl Fn(&str) -> Result<&'a [EvaluationPath]>,
167 ) -> Result<()> {
168 for (name, value) in self.inputs.iter_mut() {
169 let Some(ty) = task.inputs().get(name).map(|input| input.ty().clone()) else {
170 bail!("could not find an expected type for input {name}");
171 };
172
173 let origins = path(name)?;
174
175 if let Ok(v) = value.coerce(None, &ty) {
176 *value = resolve_with_origins(v, &ty, origins).await?;
177 }
178 }
179 Ok(())
180 }
181
182 pub fn validate(
187 &self,
188 document: &Document,
189 task: &Task,
190 specified: Option<&HashSet<String>>,
191 ) -> Result<()> {
192 let version = document.version().context("missing document version")?;
193
194 for (name, value) in &self.inputs {
196 let input = task
197 .inputs()
198 .get(name)
199 .with_context(|| format!("unknown input `{name}`"))?;
200
201 check_input_type(document, name, input, value)?;
202 }
203
204 for (name, input) in task.inputs() {
206 if input.required()
207 && !self.inputs.contains_key(name)
208 && specified.map(|s| !s.contains(name)).unwrap_or(true)
209 {
210 bail!(
211 "missing required input `{name}` to task `{task}`",
212 task = task.name()
213 );
214 }
215 }
216
217 for (name, value) in &self.requirements {
219 let ty = value.ty();
220 if let Some(expected) = task_requirement_types(version, name.as_str()) {
221 if !expected.iter().any(|target| ty.is_coercible_to(target)) {
222 bail!(
223 "expected {expected:#} for requirement `{name}`, but found {ty:#}",
224 expected = display_types(expected),
225 );
226 }
227
228 continue;
229 }
230
231 bail!("unsupported requirement `{name}`");
232 }
233
234 for (name, value) in &self.hints {
236 let ty = value.ty();
237 if let Some(expected) = task_hint_types(version, name.as_str(), false)
238 && !expected.iter().any(|target| ty.is_coercible_to(target))
239 {
240 bail!(
241 "expected {expected:#} for hint `{name}`, but found {ty:#}",
242 expected = display_types(expected),
243 );
244 }
245 }
246
247 Ok(())
248 }
249
250 fn set_path_value(
260 &mut self,
261 document: &Document,
262 task: &Task,
263 path: &str,
264 value: Value,
265 ) -> Result<bool> {
266 let version = document.version().expect("document should have a version");
267
268 match path.split_once('.') {
269 Some((key, remainder)) => {
271 let (must_match, matched) = match key {
272 "runtime" => (
273 false,
274 task_requirement_types(version, remainder)
275 .map(|types| (true, types))
276 .or_else(|| {
277 task_hint_types(version, remainder, false)
278 .map(|types| (false, types))
279 }),
280 ),
281 "requirements" => (
282 true,
283 task_requirement_types(version, remainder).map(|types| (true, types)),
284 ),
285 "hints" => (
286 false,
287 task_hint_types(version, remainder, false).map(|types| (false, types)),
288 ),
289 _ => {
290 bail!(
291 "task `{task}` does not have an input named `{path}`",
292 task = task.name()
293 );
294 }
295 };
296
297 if let Some((requirement, expected)) = matched {
298 for ty in expected {
299 if value.ty().is_coercible_to(ty) {
300 if requirement {
301 self.requirements.insert(remainder.to_string(), value);
302 } else {
303 self.hints.insert(remainder.to_string(), value);
304 }
305 return Ok(false);
306 }
307 }
308
309 bail!(
310 "expected {expected:#} for {key} key `{remainder}`, but found {ty:#}",
311 expected = display_types(expected),
312 ty = value.ty()
313 );
314 } else if must_match {
315 bail!("unsupported {key} key `{remainder}`");
316 } else {
317 Ok(false)
318 }
319 }
320 None => {
322 let input = task.inputs().get(path).with_context(|| {
323 format!(
324 "task `{name}` does not have an input named `{path}`",
325 name = task.name()
326 )
327 })?;
328
329 let actual = value.ty();
331 let expected = input.ty();
332 if let Some(PrimitiveType::String) = expected.as_primitive()
333 && let Some(actual) = actual.as_primitive()
334 && actual != PrimitiveType::String
335 {
336 self.inputs
337 .insert(path.to_string(), value.to_string().into());
338 return Ok(true);
339 }
340
341 let value = if let Some(arr_ty) = expected.as_array()
345 && !matches!(&value, Value::Compound(CompoundValue::Array(_)))
346 && value.ty().is_coercible_to(arr_ty.element_type())
347 {
348 Value::Compound(CompoundValue::Array(Array::new_unchecked(
349 expected.clone(),
350 vec![value],
351 )))
352 } else {
353 value
354 };
355
356 check_input_type(document, path, input, &value)?;
357 self.inputs.insert(path.to_string(), value);
358 Ok(true)
359 }
360 }
361 }
362}
363
364impl<S, V> FromIterator<(S, V)> for TaskInputs
365where
366 S: Into<String>,
367 V: Into<Value>,
368{
369 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
370 Self {
371 inputs: iter
372 .into_iter()
373 .map(|(k, v)| (k.into(), v.into()))
374 .collect(),
375 requirements: Default::default(),
376 hints: Default::default(),
377 }
378 }
379}
380
381impl Serialize for TaskInputs {
382 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
383 where
384 S: serde::Serializer,
385 {
386 let mut map = serializer.serialize_map(Some(self.len()))?;
387
388 for (k, v) in &self.inputs {
389 let v = crate::ValueSerializer::new(None, v, true);
390 map.serialize_entry(k, &v)?;
391 }
392
393 for (k, v) in &self.requirements {
394 let v = crate::ValueSerializer::new(None, v, true);
395 map.serialize_entry(&format!("requirements.{k}"), &v)?;
396 }
397
398 for (k, v) in &self.hints {
399 let v = crate::ValueSerializer::new(None, v, true);
400 map.serialize_entry(&format!("hints.{k}"), &v)?;
401 }
402
403 map.end()
404 }
405}
406
407#[derive(Default, Debug, Clone)]
409pub struct WorkflowInputs {
410 inputs: IndexMap<String, Value>,
412 calls: HashMap<String, Inputs>,
414}
415
416impl WorkflowInputs {
417 pub fn has_nested_inputs(&self) -> bool {
422 self.calls.values().any(|inputs| match inputs {
423 Inputs::Task(task) => !task.inputs.is_empty(),
424 Inputs::Workflow(workflow) => workflow.has_nested_inputs(),
425 })
426 }
427
428 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
430 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
431 }
432
433 pub fn is_empty(&self) -> bool {
435 self.len() == 0
436 }
437
438 pub fn len(&self) -> usize {
442 self.inputs.len() + self.calls.values().map(Inputs::len).sum::<usize>()
443 }
444
445 pub fn get(&self, name: &str) -> Option<&Value> {
447 self.inputs.get(name)
448 }
449
450 pub fn calls(&self) -> &HashMap<String, Inputs> {
452 &self.calls
453 }
454
455 pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
457 &mut self.calls
458 }
459
460 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
464 self.inputs.insert(name.into(), value.into())
465 }
466
467 pub fn contains(&self, name: &str) -> bool {
471 self.inputs.contains_key(name)
472 }
473
474 pub async fn join_paths<'a>(
480 &mut self,
481 workflow: &Workflow,
482 path: impl Fn(&str) -> Result<&'a [EvaluationPath]>,
483 ) -> Result<()> {
484 for (name, value) in self.inputs.iter_mut() {
485 let Some(ty) = workflow.inputs().get(name).map(|input| input.ty().clone()) else {
486 bail!("could not find an expected type for input {name}");
487 };
488
489 let origins = path(name)?;
490
491 if let Ok(v) = value.coerce(None, &ty) {
492 *value = resolve_with_origins(v, &ty, origins).await?;
493 }
494 }
495 Ok(())
496 }
497
498 pub fn validate(
503 &self,
504 document: &Document,
505 workflow: &Workflow,
506 specified: Option<&HashSet<String>>,
507 ) -> Result<()> {
508 for (name, value) in &self.inputs {
510 let input = workflow
511 .inputs()
512 .get(name)
513 .with_context(|| format!("unknown input `{name}`"))?;
514 check_input_type(document, name, input, value)?;
515 }
516
517 for (name, input) in workflow.inputs() {
519 if input.required()
520 && !self.inputs.contains_key(name)
521 && specified.map(|s| !s.contains(name)).unwrap_or(true)
522 {
523 bail!(
524 "missing required input `{name}` to workflow `{workflow}`",
525 workflow = workflow.name()
526 );
527 }
528 }
529
530 if !workflow.allows_nested_inputs() && self.has_nested_inputs() {
532 bail!(
533 "cannot specify a nested call input for workflow `{name}` as it does not allow \
534 nested inputs",
535 name = workflow.name()
536 );
537 }
538
539 for (name, inputs) in &self.calls {
541 let call = workflow.calls().get(name).with_context(|| {
542 format!(
543 "workflow `{workflow}` does not have a call named `{name}`",
544 workflow = workflow.name()
545 )
546 })?;
547
548 let document = call
551 .namespace()
552 .map(|ns| {
553 document
554 .namespace(ns)
555 .expect("namespace should be present")
556 .document()
557 })
558 .unwrap_or(document);
559
560 let inputs = match call.kind() {
562 CallKind::Task => {
563 let task = document
564 .task_by_name(call.name())
565 .expect("task should be present");
566
567 let task_inputs = inputs.as_task_inputs().with_context(|| {
568 format!("`{name}` is a call to a task, but workflow inputs were supplied")
569 })?;
570
571 task_inputs.validate(document, task, Some(call.specified()))?;
572 &task_inputs.inputs
573 }
574 CallKind::Workflow => {
575 let workflow = document.workflow().expect("should have a workflow");
576 assert_eq!(
577 workflow.name(),
578 call.name(),
579 "call name does not match workflow name"
580 );
581 let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
582 format!("`{name}` is a call to a workflow, but task inputs were supplied")
583 })?;
584
585 workflow_inputs.validate(document, workflow, Some(call.specified()))?;
586 &workflow_inputs.inputs
587 }
588 };
589
590 for input in inputs.keys() {
591 if call.specified().contains(input) {
592 bail!(
593 "cannot specify nested input `{input}` for call `{call}` as it was \
594 explicitly specified in the call itself",
595 call = call.name(),
596 );
597 }
598 }
599 }
600
601 if workflow.allows_nested_inputs() {
603 for (call, ty) in workflow.calls() {
604 let inputs = self.calls.get(call);
605
606 for (input, _) in ty
607 .inputs()
608 .iter()
609 .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
610 {
611 if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
612 bail!("missing required input `{input}` for call `{call}`");
613 }
614 }
615 }
616 }
617
618 Ok(())
619 }
620
621 fn set_path_value(
630 &mut self,
631 document: &Document,
632 workflow: &Workflow,
633 path: &str,
634 value: Value,
635 ) -> Result<bool> {
636 match path.split_once('.') {
637 Some((name, remainder)) => {
638 let call = workflow.calls().get(name).with_context(|| {
640 format!(
641 "workflow `{workflow}` does not have a call named `{name}`",
642 workflow = workflow.name()
643 )
644 })?;
645
646 let inputs =
648 self.calls
649 .entry(name.to_string())
650 .or_insert_with(|| match call.kind() {
651 CallKind::Task => Inputs::Task(Default::default()),
652 CallKind::Workflow => Inputs::Workflow(Default::default()),
653 });
654
655 let document = call
658 .namespace()
659 .map(|ns| {
660 document
661 .namespace(ns)
662 .expect("namespace should be present")
663 .document()
664 })
665 .unwrap_or(document);
666
667 let next = remainder
668 .split_once('.')
669 .map(|(n, _)| n)
670 .unwrap_or(remainder);
671 if call.specified().contains(next) {
672 bail!(
673 "cannot specify nested input `{next}` for call `{name}` as it was \
674 explicitly specified in the call itself",
675 );
676 }
677
678 let input = match call.kind() {
680 CallKind::Task => {
681 let task = document
682 .task_by_name(call.name())
683 .expect("task should be present");
684 inputs
685 .as_task_inputs_mut()
686 .expect("should be a task input")
687 .set_path_value(document, task, remainder, value)?
688 }
689 CallKind::Workflow => {
690 let workflow = document.workflow().expect("should have a workflow");
691 assert_eq!(
692 workflow.name(),
693 call.name(),
694 "call name does not match workflow name"
695 );
696 inputs
697 .as_workflow_inputs_mut()
698 .expect("should be a task input")
699 .set_path_value(document, workflow, remainder, value)?
700 }
701 };
702
703 if input && !workflow.allows_nested_inputs() {
704 bail!(
705 "cannot specify a nested call input for workflow `{workflow}` as it does \
706 not allow nested inputs",
707 workflow = workflow.name()
708 );
709 }
710
711 Ok(input)
712 }
713 None => {
714 let input = workflow.inputs().get(path).with_context(|| {
715 format!(
716 "workflow `{workflow}` does not have an input named `{path}`",
717 workflow = workflow.name()
718 )
719 })?;
720
721 let actual = value.ty();
723 let expected = input.ty();
724 if let Some(PrimitiveType::String) = expected.as_primitive()
725 && let Some(actual) = actual.as_primitive()
726 && actual != PrimitiveType::String
727 {
728 self.inputs
729 .insert(path.to_string(), value.to_string().into());
730 return Ok(true);
731 }
732
733 let value = if let Some(arr_ty) = expected.as_array()
737 && !matches!(&value, Value::Compound(CompoundValue::Array(_)))
738 && value.ty().is_coercible_to(arr_ty.element_type())
739 {
740 Value::Compound(CompoundValue::Array(Array::new_unchecked(
741 expected.clone(),
742 vec![value],
743 )))
744 } else {
745 value
746 };
747
748 check_input_type(document, path, input, &value)?;
749 self.inputs.insert(path.to_string(), value);
750 Ok(true)
751 }
752 }
753 }
754}
755
756impl<S, V> FromIterator<(S, V)> for WorkflowInputs
757where
758 S: Into<String>,
759 V: Into<Value>,
760{
761 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
762 Self {
763 inputs: iter
764 .into_iter()
765 .map(|(k, v)| (k.into(), v.into()))
766 .collect(),
767 calls: Default::default(),
768 }
769 }
770}
771
772impl Serialize for WorkflowInputs {
773 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
774 where
775 S: serde::Serializer,
776 {
777 let mut map = serializer.serialize_map(Some(self.len()))?;
778 for (k, v) in &self.inputs {
779 let serialized_value = crate::ValueSerializer::new(None, v, true);
780 map.serialize_entry(k, &serialized_value)?;
781 }
782
783 for (k, v) in &self.calls {
784 let serialized = serde_json::to_value(v).map_err(|_| {
785 serde::ser::Error::custom(format!("failed to serialize inputs for call `{k}`"))
786 })?;
787 let mut map = serde_json::Map::new();
788 if let JsonValue::Object(obj) = serialized {
789 for (inner, value) in obj {
790 map.insert(format!("{k}.{inner}"), value);
791 }
792 }
793 }
794
795 map.end()
796 }
797}
798
799#[derive(Debug, Clone)]
801pub enum Inputs {
802 Task(TaskInputs),
804 Workflow(WorkflowInputs),
806}
807
808impl Inputs {
809 pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
823 let path = path.as_ref();
824
825 match path.extension().and_then(|ext| ext.to_str()) {
826 Some("json") => Self::parse_json(document, path),
827 Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
828 ext => bail!(
829 "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
830 and YAML (`.yaml` and `.yml`)",
831 ext = ext.unwrap_or("")
832 ),
833 }
834 .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
835 }
836
837 pub fn parse_json(
846 document: &Document,
847 path: impl AsRef<Path>,
848 ) -> Result<Option<(String, Self)>> {
849 let path = path.as_ref();
850
851 let file = File::open(path).with_context(|| {
852 format!("failed to open input file `{path}`", path = path.display())
853 })?;
854
855 let reader = BufReader::new(file);
857
858 let map = std::mem::take(
859 serde_json::from_reader::<_, JsonValue>(reader)?
860 .as_object_mut()
861 .with_context(|| {
862 format!(
863 "expected input file `{path}` to contain a JSON object",
864 path = path.display()
865 )
866 })?,
867 );
868
869 Self::parse_json_object(document, map)
870 }
871
872 pub fn parse_yaml(
881 document: &Document,
882 path: impl AsRef<Path>,
883 ) -> Result<Option<(String, Self)>> {
884 let path = path.as_ref();
885
886 let file = File::open(path).with_context(|| {
887 format!("failed to open input file `{path}`", path = path.display())
888 })?;
889
890 let reader = BufReader::new(file);
892 let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
893
894 let mut json = serde_json::to_value(yaml).with_context(|| {
896 format!(
897 "failed to convert YAML to JSON for processing `{path}`",
898 path = path.display()
899 )
900 })?;
901
902 let object = std::mem::take(json.as_object_mut().with_context(|| {
903 format!(
904 "expected input file `{path}` to contain a YAML mapping",
905 path = path.display()
906 )
907 })?);
908
909 Self::parse_json_object(document, object)
910 }
911
912 pub fn is_empty(&self) -> bool {
914 self.len() == 0
915 }
916
917 pub fn len(&self) -> usize {
923 match self {
924 Self::Task(inputs) => inputs.len(),
925 Self::Workflow(inputs) => inputs.len(),
926 }
927 }
928
929 pub fn get(&self, name: &str) -> Option<&Value> {
931 match self {
932 Self::Task(t) => t.inputs.get(name),
933 Self::Workflow(w) => w.inputs.get(name),
934 }
935 }
936
937 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
941 match self {
942 Self::Task(inputs) => inputs.set(name, value),
943 Self::Workflow(inputs) => inputs.set(name, value),
944 }
945 }
946
947 pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
951 match self {
952 Self::Task(inputs) => Some(inputs),
953 Self::Workflow(_) => None,
954 }
955 }
956
957 pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
961 match self {
962 Self::Task(inputs) => Some(inputs),
963 Self::Workflow(_) => None,
964 }
965 }
966
967 pub fn unwrap_task_inputs(self) -> TaskInputs {
973 match self {
974 Self::Task(inputs) => inputs,
975 Self::Workflow(_) => panic!("inputs are for a workflow"),
976 }
977 }
978
979 pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
983 match self {
984 Self::Task(_) => None,
985 Self::Workflow(inputs) => Some(inputs),
986 }
987 }
988
989 pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
993 match self {
994 Self::Task(_) => None,
995 Self::Workflow(inputs) => Some(inputs),
996 }
997 }
998
999 pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
1005 match self {
1006 Self::Task(_) => panic!("inputs are for a task"),
1007 Self::Workflow(inputs) => inputs,
1008 }
1009 }
1010
1011 pub fn parse_json_object(
1017 document: &Document,
1018 object: JsonMap,
1019 ) -> Result<Option<(String, Self)>> {
1020 if object.is_empty() {
1022 return Ok(None);
1023 }
1024
1025 let mut target_candidates = BTreeSet::new();
1028 for key in object.keys() {
1029 let Some((prefix, _)) = key.split_once('.') else {
1030 bail!(
1031 "invalid input key `{key}`: expected the key to be prefixed with the workflow \
1032 or task name",
1033 )
1034 };
1035 target_candidates.insert(prefix);
1036 }
1037
1038 let target_name = match target_candidates
1041 .iter()
1042 .take(2)
1043 .collect::<Vec<_>>()
1044 .as_slice()
1045 {
1046 [] => panic!("no target candidates for inputs; report this as a bug"),
1047 [target_name] => target_name.to_string(),
1048 _ => bail!(
1049 "invalid inputs: expected each input key to be prefixed with the same workflow or \
1050 task name, but found multiple prefixes: {target_candidates:?}",
1051 ),
1052 };
1053
1054 let inputs = match (document.task_by_name(&target_name), document.workflow()) {
1055 (Some(task), _) => Self::parse_task_inputs(document, task, object)?,
1056 (None, Some(workflow)) if workflow.name() == target_name => {
1057 Self::parse_workflow_inputs(document, workflow, object)?
1058 }
1059 _ => bail!(
1060 "invalid inputs: a task or workflow named `{target_name}` does not exist in the \
1061 document"
1062 ),
1063 };
1064 Ok(Some((target_name, inputs)))
1065 }
1066
1067 fn parse_task_inputs(document: &Document, task: &Task, object: JsonMap) -> Result<Self> {
1069 let mut inputs = TaskInputs::default();
1070 for (key, value) in object {
1071 let value = serde_json::from_value(value)
1073 .with_context(|| format!("invalid input value for key `{key}`"))?;
1074
1075 match key.split_once(".") {
1076 Some((prefix, remainder)) if prefix == task.name() => {
1077 inputs
1078 .set_path_value(document, task, remainder, value)
1079 .with_context(|| format!("invalid input key `{key}`"))?;
1080 }
1081 _ => {
1082 bail!(
1086 "invalid input key `{key}`: expected key to be prefixed with `{task}`",
1087 task = task.name()
1088 );
1089 }
1090 }
1091 }
1092
1093 Ok(Inputs::Task(inputs))
1094 }
1095
1096 fn parse_workflow_inputs(
1098 document: &Document,
1099 workflow: &Workflow,
1100 object: JsonMap,
1101 ) -> Result<Self> {
1102 let mut inputs = WorkflowInputs::default();
1103 for (key, value) in object {
1104 let value = serde_json::from_value(value)
1106 .with_context(|| format!("invalid input value for key `{key}`"))?;
1107
1108 match key.split_once(".") {
1109 Some((prefix, remainder)) if prefix == workflow.name() => {
1110 inputs
1111 .set_path_value(document, workflow, remainder, value)
1112 .with_context(|| format!("invalid input key `{key}`"))?;
1113 }
1114 _ => {
1115 bail!(
1119 "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
1120 workflow = workflow.name()
1121 );
1122 }
1123 }
1124 }
1125
1126 Ok(Inputs::Workflow(inputs))
1127 }
1128}
1129
1130impl From<TaskInputs> for Inputs {
1131 fn from(inputs: TaskInputs) -> Self {
1132 Self::Task(inputs)
1133 }
1134}
1135
1136impl From<WorkflowInputs> for Inputs {
1137 fn from(inputs: WorkflowInputs) -> Self {
1138 Self::Workflow(inputs)
1139 }
1140}
1141
1142impl Serialize for Inputs {
1143 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1144 where
1145 S: serde::Serializer,
1146 {
1147 match self {
1148 Self::Task(inputs) => inputs.serialize(serializer),
1149 Self::Workflow(inputs) => inputs.serialize(serializer),
1150 }
1151 }
1152}