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 if ty.as_array().is_some() && expected_ty.as_array().is_none() {
53 bail!(
54 "expected {expected_ty:#} for input `{name}`, but found {ty:#}\n\nnote: this can \
55 happen when a key is repeated on the command line (e.g., `{name}=a {name}=b`) or \
56 when an unquoted shell glob (e.g., `{name}=*.txt`) expands to more than one \
57 value; provide exactly one value for this scalar input, or change the input's \
58 declared type to an array if it should accept multiple values"
59 );
60 }
61 bail!("expected {expected_ty:#} for input `{name}`, but found {ty:#}");
62 }
63
64 Ok(())
65}
66
67async fn resolve_with_origins(
73 value: Value,
74 ty: &wdl_analysis::types::Type,
75 origins: &[EvaluationPath],
76) -> Result<Value> {
77 if origins.len() > 1
78 && let Value::Compound(CompoundValue::Array(ref array)) = value
79 {
80 let arr_ty = ty.as_array().expect("should be an array type");
81 assert_eq!(
82 origins.len(),
83 array.as_slice().len(),
84 "the number of origins should match the number of array elements"
85 );
86 let optional = arr_ty.element_type().is_optional();
87 let mut resolved = Vec::with_capacity(array.as_slice().len());
88 for (elem, base_dir) in array.as_slice().iter().zip(origins) {
89 resolved.push(
90 elem.resolve_paths(optional, None, None, &|p| p.expand(base_dir))
91 .await?,
92 );
93 }
94 return Ok(Value::Compound(CompoundValue::Array(Array::new_unchecked(
95 arr_ty.clone(),
96 resolved,
97 ))));
98 }
99
100 let base_dir = &origins[0];
101 value
102 .resolve_paths(ty.is_optional(), None, None, &|p| p.expand(base_dir))
103 .await
104}
105
106#[derive(Default, Debug, Clone)]
108pub struct TaskInputs {
109 inputs: IndexMap<String, Value>,
111 requirements: HashMap<String, Value>,
113 hints: HashMap<String, Value>,
115}
116
117impl TaskInputs {
118 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
120 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
121 }
122
123 pub fn is_empty(&self) -> bool {
125 self.len() == 0
126 }
127
128 pub fn len(&self) -> usize {
132 self.inputs.len() + self.requirements.len() + self.hints.len()
133 }
134
135 pub fn get(&self, name: &str) -> Option<&Value> {
137 self.inputs.get(name)
138 }
139
140 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
144 self.inputs.insert(name.into(), value.into())
145 }
146
147 pub fn requirement(&self, name: &str) -> Option<&Value> {
149 self.requirements.get(name)
150 }
151
152 pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
154 self.requirements.insert(name.into(), value.into());
155 }
156
157 pub fn hint(&self, name: &str) -> Option<&Value> {
159 self.hints.get(name)
160 }
161
162 pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
164 self.hints.insert(name.into(), value.into());
165 }
166
167 pub async fn join_paths<'a>(
173 &mut self,
174 task: &Task,
175 path: impl Fn(&str) -> Result<&'a [EvaluationPath]>,
176 ) -> Result<()> {
177 for (name, value) in self.inputs.iter_mut() {
178 let Some(ty) = task.inputs().get(name).map(|input| input.ty().clone()) else {
179 bail!("could not find an expected type for input {name}");
180 };
181
182 let origins = path(name)?;
183
184 if let Ok(v) = value.coerce(None, &ty) {
185 *value = resolve_with_origins(v, &ty, origins).await?;
186 }
187 }
188 Ok(())
189 }
190
191 pub fn validate(
196 &self,
197 document: &Document,
198 task: &Task,
199 specified: Option<&HashSet<String>>,
200 ) -> Result<()> {
201 let version = document.version().context("missing document version")?;
202
203 for (name, value) in &self.inputs {
205 let input = task
206 .inputs()
207 .get(name)
208 .with_context(|| format!("unknown input `{name}`"))?;
209
210 check_input_type(document, name, input, value)?;
211 }
212
213 for (name, input) in task.inputs() {
215 if input.required()
216 && !self.inputs.contains_key(name)
217 && specified.map(|s| !s.contains(name)).unwrap_or(true)
218 {
219 bail!(
220 "missing required input `{name}` to task `{task}`",
221 task = task.name()
222 );
223 }
224 }
225
226 for (name, value) in &self.requirements {
228 let ty = value.ty();
229 if let Some(expected) = task_requirement_types(version, name.as_str()) {
230 if !expected.iter().any(|target| ty.is_coercible_to(target)) {
231 bail!(
232 "expected {expected:#} for requirement `{name}`, but found {ty:#}",
233 expected = display_types(expected),
234 );
235 }
236
237 continue;
238 }
239
240 bail!("unsupported requirement `{name}`");
241 }
242
243 for (name, value) in &self.hints {
245 let ty = value.ty();
246 if let Some(expected) = task_hint_types(version, name.as_str(), false)
247 && !expected.iter().any(|target| ty.is_coercible_to(target))
248 {
249 bail!(
250 "expected {expected:#} for hint `{name}`, but found {ty:#}",
251 expected = display_types(expected),
252 );
253 }
254 }
255
256 Ok(())
257 }
258
259 fn set_path_value(
269 &mut self,
270 document: &Document,
271 task: &Task,
272 path: &str,
273 value: Value,
274 ) -> Result<bool> {
275 let version = document.version().expect("document should have a version");
276
277 match path.split_once('.') {
278 Some((key, remainder)) => {
280 let (must_match, matched) = match key {
281 "runtime" => (
282 false,
283 task_requirement_types(version, remainder)
284 .map(|types| (true, types))
285 .or_else(|| {
286 task_hint_types(version, remainder, false)
287 .map(|types| (false, types))
288 }),
289 ),
290 "requirements" => (
291 true,
292 task_requirement_types(version, remainder).map(|types| (true, types)),
293 ),
294 "hints" => (
295 false,
296 task_hint_types(version, remainder, false).map(|types| (false, types)),
297 ),
298 _ => {
299 bail!(
300 "task `{task}` does not have an input named `{path}`",
301 task = task.name()
302 );
303 }
304 };
305
306 if let Some((requirement, expected)) = matched {
307 for ty in expected {
308 if value.ty().is_coercible_to(ty) {
309 if requirement {
310 self.requirements.insert(remainder.to_string(), value);
311 } else {
312 self.hints.insert(remainder.to_string(), value);
313 }
314 return Ok(false);
315 }
316 }
317
318 bail!(
319 "expected {expected:#} for {key} key `{remainder}`, but found {ty:#}",
320 expected = display_types(expected),
321 ty = value.ty()
322 );
323 } else if must_match {
324 bail!("unsupported {key} key `{remainder}`");
325 } else {
326 Ok(false)
327 }
328 }
329 None => {
331 let input = task.inputs().get(path).with_context(|| {
332 format!(
333 "task `{name}` does not have an input named `{path}`",
334 name = task.name()
335 )
336 })?;
337
338 let actual = value.ty();
340 let expected = input.ty();
341 if let Some(PrimitiveType::String) = expected.as_primitive()
342 && let Some(actual) = actual.as_primitive()
343 && actual != PrimitiveType::String
344 {
345 self.inputs
346 .insert(path.to_string(), value.to_string().into());
347 return Ok(true);
348 }
349
350 let value = if let Some(arr_ty) = expected.as_array()
354 && !matches!(&value, Value::Compound(CompoundValue::Array(_)))
355 && value.ty().is_coercible_to(arr_ty.element_type())
356 {
357 Value::Compound(CompoundValue::Array(Array::new_unchecked(
358 expected.clone(),
359 vec![value],
360 )))
361 } else {
362 value
363 };
364
365 check_input_type(document, path, input, &value)?;
366 self.inputs.insert(path.to_string(), value);
367 Ok(true)
368 }
369 }
370 }
371}
372
373impl<S, V> FromIterator<(S, V)> for TaskInputs
374where
375 S: Into<String>,
376 V: Into<Value>,
377{
378 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
379 Self {
380 inputs: iter
381 .into_iter()
382 .map(|(k, v)| (k.into(), v.into()))
383 .collect(),
384 requirements: Default::default(),
385 hints: Default::default(),
386 }
387 }
388}
389
390impl Serialize for TaskInputs {
391 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
392 where
393 S: serde::Serializer,
394 {
395 let mut map = serializer.serialize_map(Some(self.len()))?;
396
397 for (k, v) in &self.inputs {
398 let v = crate::ValueSerializer::new(None, v, true);
399 map.serialize_entry(k, &v)?;
400 }
401
402 for (k, v) in &self.requirements {
403 let v = crate::ValueSerializer::new(None, v, true);
404 map.serialize_entry(&format!("requirements.{k}"), &v)?;
405 }
406
407 for (k, v) in &self.hints {
408 let v = crate::ValueSerializer::new(None, v, true);
409 map.serialize_entry(&format!("hints.{k}"), &v)?;
410 }
411
412 map.end()
413 }
414}
415
416#[derive(Default, Debug, Clone)]
418pub struct WorkflowInputs {
419 inputs: IndexMap<String, Value>,
421 calls: HashMap<String, Inputs>,
423}
424
425impl WorkflowInputs {
426 pub fn has_nested_inputs(&self) -> bool {
431 self.calls.values().any(|inputs| match inputs {
432 Inputs::Task(task) => !task.inputs.is_empty(),
433 Inputs::Workflow(workflow) => workflow.has_nested_inputs(),
434 })
435 }
436
437 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
439 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
440 }
441
442 pub fn is_empty(&self) -> bool {
444 self.len() == 0
445 }
446
447 pub fn len(&self) -> usize {
451 self.inputs.len() + self.calls.values().map(Inputs::len).sum::<usize>()
452 }
453
454 pub fn get(&self, name: &str) -> Option<&Value> {
456 self.inputs.get(name)
457 }
458
459 pub fn calls(&self) -> &HashMap<String, Inputs> {
461 &self.calls
462 }
463
464 pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
466 &mut self.calls
467 }
468
469 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
473 self.inputs.insert(name.into(), value.into())
474 }
475
476 pub fn contains(&self, name: &str) -> bool {
480 self.inputs.contains_key(name)
481 }
482
483 pub async fn join_paths<'a>(
489 &mut self,
490 workflow: &Workflow,
491 path: impl Fn(&str) -> Result<&'a [EvaluationPath]>,
492 ) -> Result<()> {
493 for (name, value) in self.inputs.iter_mut() {
494 let Some(ty) = workflow.inputs().get(name).map(|input| input.ty().clone()) else {
495 bail!("could not find an expected type for input {name}");
496 };
497
498 let origins = path(name)?;
499
500 if let Ok(v) = value.coerce(None, &ty) {
501 *value = resolve_with_origins(v, &ty, origins).await?;
502 }
503 }
504 Ok(())
505 }
506
507 pub fn validate(
512 &self,
513 document: &Document,
514 workflow: &Workflow,
515 specified: Option<&HashSet<String>>,
516 ) -> Result<()> {
517 for (name, value) in &self.inputs {
519 let input = workflow
520 .inputs()
521 .get(name)
522 .with_context(|| format!("unknown input `{name}`"))?;
523 check_input_type(document, name, input, value)?;
524 }
525
526 for (name, input) in workflow.inputs() {
528 if input.required()
529 && !self.inputs.contains_key(name)
530 && specified.map(|s| !s.contains(name)).unwrap_or(true)
531 {
532 bail!(
533 "missing required input `{name}` to workflow `{workflow}`",
534 workflow = workflow.name()
535 );
536 }
537 }
538
539 if !workflow.allows_nested_inputs() && self.has_nested_inputs() {
541 bail!(
542 "cannot specify a nested call input for workflow `{name}` as it does not allow \
543 nested inputs",
544 name = workflow.name()
545 );
546 }
547
548 for (name, inputs) in &self.calls {
550 let call = workflow.calls().get(name).with_context(|| {
551 format!(
552 "workflow `{workflow}` does not have a call named `{name}`",
553 workflow = workflow.name()
554 )
555 })?;
556
557 let document = call
560 .namespace()
561 .map(|ns| {
562 document
563 .namespace(ns)
564 .expect("namespace should be present")
565 .document()
566 })
567 .unwrap_or(document);
568
569 let inputs = match call.kind() {
571 CallKind::Task => {
572 let task = document
573 .task_by_name(call.name())
574 .expect("task should be present");
575
576 let task_inputs = inputs.as_task_inputs().with_context(|| {
577 format!("`{name}` is a call to a task, but workflow inputs were supplied")
578 })?;
579
580 task_inputs.validate(document, task, Some(call.specified()))?;
581 &task_inputs.inputs
582 }
583 CallKind::Workflow => {
584 let workflow = document.workflow().expect("should have a workflow");
585 assert_eq!(
586 workflow.name(),
587 call.name(),
588 "call name does not match workflow name"
589 );
590 let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
591 format!("`{name}` is a call to a workflow, but task inputs were supplied")
592 })?;
593
594 workflow_inputs.validate(document, workflow, Some(call.specified()))?;
595 &workflow_inputs.inputs
596 }
597 };
598
599 for input in inputs.keys() {
600 if call.specified().contains(input) {
601 bail!(
602 "cannot specify nested input `{input}` for call `{call}` as it was \
603 explicitly specified in the call itself",
604 call = call.name(),
605 );
606 }
607 }
608 }
609
610 if workflow.allows_nested_inputs() {
612 for (call, ty) in workflow.calls() {
613 let inputs = self.calls.get(call);
614
615 for (input, _) in ty
616 .inputs()
617 .iter()
618 .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
619 {
620 if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
621 bail!("missing required input `{input}` for call `{call}`");
622 }
623 }
624 }
625 }
626
627 Ok(())
628 }
629
630 fn set_path_value(
639 &mut self,
640 document: &Document,
641 workflow: &Workflow,
642 path: &str,
643 value: Value,
644 ) -> Result<bool> {
645 match path.split_once('.') {
646 Some((name, remainder)) => {
647 let call = workflow.calls().get(name).with_context(|| {
649 format!(
650 "workflow `{workflow}` does not have a call named `{name}`",
651 workflow = workflow.name()
652 )
653 })?;
654
655 let inputs =
657 self.calls
658 .entry(name.to_string())
659 .or_insert_with(|| match call.kind() {
660 CallKind::Task => Inputs::Task(Default::default()),
661 CallKind::Workflow => Inputs::Workflow(Default::default()),
662 });
663
664 let document = call
667 .namespace()
668 .map(|ns| {
669 document
670 .namespace(ns)
671 .expect("namespace should be present")
672 .document()
673 })
674 .unwrap_or(document);
675
676 let next = remainder
677 .split_once('.')
678 .map(|(n, _)| n)
679 .unwrap_or(remainder);
680 if call.specified().contains(next) {
681 bail!(
682 "cannot specify nested input `{next}` for call `{name}` as it was \
683 explicitly specified in the call itself",
684 );
685 }
686
687 let input = match call.kind() {
689 CallKind::Task => {
690 let task = document
691 .task_by_name(call.name())
692 .expect("task should be present");
693 inputs
694 .as_task_inputs_mut()
695 .expect("should be a task input")
696 .set_path_value(document, task, remainder, value)?
697 }
698 CallKind::Workflow => {
699 let workflow = document.workflow().expect("should have a workflow");
700 assert_eq!(
701 workflow.name(),
702 call.name(),
703 "call name does not match workflow name"
704 );
705 inputs
706 .as_workflow_inputs_mut()
707 .expect("should be a task input")
708 .set_path_value(document, workflow, remainder, value)?
709 }
710 };
711
712 if input && !workflow.allows_nested_inputs() {
713 bail!(
714 "cannot specify a nested call input for workflow `{workflow}` as it does \
715 not allow nested inputs",
716 workflow = workflow.name()
717 );
718 }
719
720 Ok(input)
721 }
722 None => {
723 let input = workflow.inputs().get(path).with_context(|| {
724 format!(
725 "workflow `{workflow}` does not have an input named `{path}`",
726 workflow = workflow.name()
727 )
728 })?;
729
730 let actual = value.ty();
732 let expected = input.ty();
733 if let Some(PrimitiveType::String) = expected.as_primitive()
734 && let Some(actual) = actual.as_primitive()
735 && actual != PrimitiveType::String
736 {
737 self.inputs
738 .insert(path.to_string(), value.to_string().into());
739 return Ok(true);
740 }
741
742 let value = if let Some(arr_ty) = expected.as_array()
746 && !matches!(&value, Value::Compound(CompoundValue::Array(_)))
747 && value.ty().is_coercible_to(arr_ty.element_type())
748 {
749 Value::Compound(CompoundValue::Array(Array::new_unchecked(
750 expected.clone(),
751 vec![value],
752 )))
753 } else {
754 value
755 };
756
757 check_input_type(document, path, input, &value)?;
758 self.inputs.insert(path.to_string(), value);
759 Ok(true)
760 }
761 }
762 }
763}
764
765impl<S, V> FromIterator<(S, V)> for WorkflowInputs
766where
767 S: Into<String>,
768 V: Into<Value>,
769{
770 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
771 Self {
772 inputs: iter
773 .into_iter()
774 .map(|(k, v)| (k.into(), v.into()))
775 .collect(),
776 calls: Default::default(),
777 }
778 }
779}
780
781impl Serialize for WorkflowInputs {
782 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
783 where
784 S: serde::Serializer,
785 {
786 let mut map = serializer.serialize_map(Some(self.len()))?;
787 for (k, v) in &self.inputs {
788 let serialized_value = crate::ValueSerializer::new(None, v, true);
789 map.serialize_entry(k, &serialized_value)?;
790 }
791
792 for (k, v) in &self.calls {
793 let serialized = serde_json::to_value(v).map_err(|_| {
794 serde::ser::Error::custom(format!("failed to serialize inputs for call `{k}`"))
795 })?;
796 let mut map = serde_json::Map::new();
797 if let JsonValue::Object(obj) = serialized {
798 for (inner, value) in obj {
799 map.insert(format!("{k}.{inner}"), value);
800 }
801 }
802 }
803
804 map.end()
805 }
806}
807
808#[derive(Debug, Clone)]
810pub enum Inputs {
811 Task(TaskInputs),
813 Workflow(WorkflowInputs),
815}
816
817impl Inputs {
818 pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
832 let path = path.as_ref();
833
834 match path.extension().and_then(|ext| ext.to_str()) {
835 Some("json") => Self::parse_json(document, path),
836 Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
837 ext => bail!(
838 "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
839 and YAML (`.yaml` and `.yml`)",
840 ext = ext.unwrap_or("")
841 ),
842 }
843 .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
844 }
845
846 pub fn parse_json(
855 document: &Document,
856 path: impl AsRef<Path>,
857 ) -> Result<Option<(String, Self)>> {
858 let path = path.as_ref();
859
860 let file = File::open(path).with_context(|| {
861 format!("failed to open input file `{path}`", path = path.display())
862 })?;
863
864 let reader = BufReader::new(file);
866
867 let map = std::mem::take(
868 serde_json::from_reader::<_, JsonValue>(reader)?
869 .as_object_mut()
870 .with_context(|| {
871 format!(
872 "expected input file `{path}` to contain a JSON object",
873 path = path.display()
874 )
875 })?,
876 );
877
878 Self::parse_json_object(document, map)
879 }
880
881 pub fn parse_yaml(
890 document: &Document,
891 path: impl AsRef<Path>,
892 ) -> Result<Option<(String, Self)>> {
893 let path = path.as_ref();
894
895 let file = File::open(path).with_context(|| {
896 format!("failed to open input file `{path}`", path = path.display())
897 })?;
898
899 let reader = BufReader::new(file);
901 let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
902
903 let mut json = serde_json::to_value(yaml).with_context(|| {
905 format!(
906 "failed to convert YAML to JSON for processing `{path}`",
907 path = path.display()
908 )
909 })?;
910
911 let object = std::mem::take(json.as_object_mut().with_context(|| {
912 format!(
913 "expected input file `{path}` to contain a YAML mapping",
914 path = path.display()
915 )
916 })?);
917
918 Self::parse_json_object(document, object)
919 }
920
921 pub fn is_empty(&self) -> bool {
923 self.len() == 0
924 }
925
926 pub fn len(&self) -> usize {
932 match self {
933 Self::Task(inputs) => inputs.len(),
934 Self::Workflow(inputs) => inputs.len(),
935 }
936 }
937
938 pub fn get(&self, name: &str) -> Option<&Value> {
940 match self {
941 Self::Task(t) => t.inputs.get(name),
942 Self::Workflow(w) => w.inputs.get(name),
943 }
944 }
945
946 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
950 match self {
951 Self::Task(inputs) => inputs.set(name, value),
952 Self::Workflow(inputs) => inputs.set(name, value),
953 }
954 }
955
956 pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
960 match self {
961 Self::Task(inputs) => Some(inputs),
962 Self::Workflow(_) => None,
963 }
964 }
965
966 pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
970 match self {
971 Self::Task(inputs) => Some(inputs),
972 Self::Workflow(_) => None,
973 }
974 }
975
976 pub fn unwrap_task_inputs(self) -> TaskInputs {
982 match self {
983 Self::Task(inputs) => inputs,
984 Self::Workflow(_) => panic!("inputs are for a workflow"),
985 }
986 }
987
988 pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
992 match self {
993 Self::Task(_) => None,
994 Self::Workflow(inputs) => Some(inputs),
995 }
996 }
997
998 pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
1002 match self {
1003 Self::Task(_) => None,
1004 Self::Workflow(inputs) => Some(inputs),
1005 }
1006 }
1007
1008 pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
1014 match self {
1015 Self::Task(_) => panic!("inputs are for a task"),
1016 Self::Workflow(inputs) => inputs,
1017 }
1018 }
1019
1020 pub fn parse_json_object(
1026 document: &Document,
1027 object: JsonMap,
1028 ) -> Result<Option<(String, Self)>> {
1029 if object.is_empty() {
1031 return Ok(None);
1032 }
1033
1034 let mut target_candidates = BTreeSet::new();
1037 for key in object.keys() {
1038 let Some((prefix, _)) = key.split_once('.') else {
1039 bail!(
1040 "invalid input key `{key}`: expected the key to be prefixed with the workflow \
1041 or task name",
1042 )
1043 };
1044 target_candidates.insert(prefix);
1045 }
1046
1047 let target_name = match target_candidates
1050 .iter()
1051 .take(2)
1052 .collect::<Vec<_>>()
1053 .as_slice()
1054 {
1055 [] => panic!("no target candidates for inputs; report this as a bug"),
1056 [target_name] => target_name.to_string(),
1057 _ => bail!(
1058 "invalid inputs: expected each input key to be prefixed with the same workflow or \
1059 task name, but found multiple prefixes: {target_candidates:?}",
1060 ),
1061 };
1062
1063 let inputs = match (document.task_by_name(&target_name), document.workflow()) {
1064 (Some(task), _) => Self::parse_task_inputs(document, task, object)?,
1065 (None, Some(workflow)) if workflow.name() == target_name => {
1066 Self::parse_workflow_inputs(document, workflow, object)?
1067 }
1068 _ => bail!(
1069 "invalid inputs: a task or workflow named `{target_name}` does not exist in the \
1070 document"
1071 ),
1072 };
1073 Ok(Some((target_name, inputs)))
1074 }
1075
1076 fn parse_task_inputs(document: &Document, task: &Task, object: JsonMap) -> Result<Self> {
1078 let mut inputs = TaskInputs::default();
1079 for (key, value) in object {
1080 let value = serde_json::from_value(value)
1082 .with_context(|| format!("invalid input value for key `{key}`"))?;
1083
1084 match key.split_once(".") {
1085 Some((prefix, remainder)) if prefix == task.name() => {
1086 inputs
1087 .set_path_value(document, task, remainder, value)
1088 .with_context(|| format!("invalid input key `{key}`"))?;
1089 }
1090 _ => {
1091 bail!(
1095 "invalid input key `{key}`: expected key to be prefixed with `{task}`",
1096 task = task.name()
1097 );
1098 }
1099 }
1100 }
1101
1102 Ok(Inputs::Task(inputs))
1103 }
1104
1105 fn parse_workflow_inputs(
1107 document: &Document,
1108 workflow: &Workflow,
1109 object: JsonMap,
1110 ) -> Result<Self> {
1111 let mut inputs = WorkflowInputs::default();
1112 for (key, value) in object {
1113 let value = serde_json::from_value(value)
1115 .with_context(|| format!("invalid input value for key `{key}`"))?;
1116
1117 match key.split_once(".") {
1118 Some((prefix, remainder)) if prefix == workflow.name() => {
1119 inputs
1120 .set_path_value(document, workflow, remainder, value)
1121 .with_context(|| format!("invalid input key `{key}`"))?;
1122 }
1123 _ => {
1124 bail!(
1128 "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
1129 workflow = workflow.name()
1130 );
1131 }
1132 }
1133 }
1134
1135 Ok(Inputs::Workflow(inputs))
1136 }
1137}
1138
1139impl From<TaskInputs> for Inputs {
1140 fn from(inputs: TaskInputs) -> Self {
1141 Self::Task(inputs)
1142 }
1143}
1144
1145impl From<WorkflowInputs> for Inputs {
1146 fn from(inputs: WorkflowInputs) -> Self {
1147 Self::Workflow(inputs)
1148 }
1149}
1150
1151impl Serialize for Inputs {
1152 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1153 where
1154 S: serde::Serializer,
1155 {
1156 match self {
1157 Self::Task(inputs) => inputs.serialize(serializer),
1158 Self::Workflow(inputs) => inputs.serialize(serializer),
1159 }
1160 }
1161}