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 get(&self, name: &str) -> Option<&Value> {
82 self.inputs.get(name)
83 }
84
85 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 pub fn requirement(&self, name: &str) -> Option<&Value> {
94 self.requirements.get(name)
95 }
96
97 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 pub fn hint(&self, name: &str) -> Option<&Value> {
104 self.hints.get(name)
105 }
106
107 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 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 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 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 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 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 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 fn set_path_value(
216 &mut self,
217 document: &Document,
218 task: &Task,
219 path: &str,
220 value: Value,
221 ) -> Result<bool> {
222 let version = document.version().expect("document should have a version");
223
224 match path.split_once('.') {
225 Some((key, remainder)) => {
227 let (must_match, matched) = match key {
228 "runtime" => (
229 false,
230 task_requirement_types(version, remainder)
231 .map(|types| (true, types))
232 .or_else(|| {
233 task_hint_types(version, remainder, false)
234 .map(|types| (false, types))
235 }),
236 ),
237 "requirements" => (
238 true,
239 task_requirement_types(version, remainder).map(|types| (true, types)),
240 ),
241 "hints" => (
242 false,
243 task_hint_types(version, remainder, false).map(|types| (false, types)),
244 ),
245 _ => {
246 bail!(
247 "task `{task}` does not have an input named `{path}`",
248 task = task.name()
249 );
250 }
251 };
252
253 if let Some((requirement, expected)) = matched {
254 for ty in expected {
255 if value.ty().is_coercible_to(ty) {
256 if requirement {
257 self.requirements.insert(remainder.to_string(), value);
258 } else {
259 self.hints.insert(remainder.to_string(), value);
260 }
261 return Ok(false);
262 }
263 }
264
265 bail!(
266 "expected {expected} for {key} key `{remainder}`, but found type `{ty}`",
267 expected = display_types(expected),
268 ty = value.ty()
269 );
270 } else if must_match {
271 bail!("unsupported {key} key `{remainder}`");
272 } else {
273 Ok(false)
274 }
275 }
276 None => {
278 let input = task.inputs().get(path).with_context(|| {
279 format!(
280 "task `{name}` does not have an input named `{path}`",
281 name = task.name()
282 )
283 })?;
284
285 let actual = value.ty();
287 let expected = input.ty();
288 if let Some(PrimitiveType::String) = expected.as_primitive()
289 && let Some(actual) = actual.as_primitive()
290 && actual != PrimitiveType::String
291 {
292 self.inputs
293 .insert(path.to_string(), value.to_string().into());
294 return Ok(true);
295 }
296
297 check_input_type(document, path, input, &value)?;
298 self.inputs.insert(path.to_string(), value);
299 Ok(true)
300 }
301 }
302 }
303}
304
305impl<S, V> FromIterator<(S, V)> for TaskInputs
306where
307 S: Into<String>,
308 V: Into<Value>,
309{
310 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
311 Self {
312 inputs: iter
313 .into_iter()
314 .map(|(k, v)| (k.into(), v.into()))
315 .collect(),
316 requirements: Default::default(),
317 hints: Default::default(),
318 }
319 }
320}
321
322impl Serialize for TaskInputs {
323 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
324 where
325 S: serde::Serializer,
326 {
327 let mut map = serializer.serialize_map(Some(self.inputs.len()))?;
329 for (k, v) in &self.inputs {
330 let serialized_value = crate::ValueSerializer::new(None, v, true);
331 map.serialize_entry(k, &serialized_value)?;
332 }
333 map.end()
334 }
335}
336
337#[derive(Default, Debug, Clone)]
339pub struct WorkflowInputs {
340 inputs: IndexMap<String, Value>,
342 calls: HashMap<String, Inputs>,
344}
345
346impl WorkflowInputs {
347 pub fn has_nested_inputs(&self) -> bool {
352 self.calls.values().any(|inputs| match inputs {
353 Inputs::Task(task) => !task.inputs.is_empty(),
354 Inputs::Workflow(workflow) => workflow.has_nested_inputs(),
355 })
356 }
357
358 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
360 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
361 }
362
363 pub fn get(&self, name: &str) -> Option<&Value> {
365 self.inputs.get(name)
366 }
367
368 pub fn calls(&self) -> &HashMap<String, Inputs> {
370 &self.calls
371 }
372
373 pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
375 &mut self.calls
376 }
377
378 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
382 self.inputs.insert(name.into(), value.into())
383 }
384
385 pub fn contains(&self, name: &str) -> bool {
389 self.inputs.contains_key(name)
390 }
391
392 pub async fn join_paths<'a>(
398 &mut self,
399 workflow: &Workflow,
400 path: impl Fn(&str) -> Result<&'a EvaluationPath>,
401 ) -> Result<()> {
402 for (name, value) in self.inputs.iter_mut() {
403 let Some(ty) = workflow.inputs().get(name).map(|input| input.ty().clone()) else {
404 bail!("could not find an expected type for input {name}");
405 };
406
407 let base_dir = path(name)?;
408
409 if let Ok(v) = value.coerce(None, &ty) {
410 *value = v
411 .resolve_paths(ty.is_optional(), None, None, &|path| path.expand(base_dir))
412 .await?;
413 }
414 }
415 Ok(())
416 }
417
418 pub fn validate(
423 &self,
424 document: &Document,
425 workflow: &Workflow,
426 specified: Option<&HashSet<String>>,
427 ) -> Result<()> {
428 for (name, value) in &self.inputs {
430 let input = workflow
431 .inputs()
432 .get(name)
433 .with_context(|| format!("unknown input `{name}`"))?;
434 check_input_type(document, name, input, value)?;
435 }
436
437 for (name, input) in workflow.inputs() {
439 if input.required()
440 && !self.inputs.contains_key(name)
441 && specified.map(|s| !s.contains(name)).unwrap_or(true)
442 {
443 bail!(
444 "missing required input `{name}` to workflow `{workflow}`",
445 workflow = workflow.name()
446 );
447 }
448 }
449
450 if !workflow.allows_nested_inputs() && self.has_nested_inputs() {
452 bail!(
453 "cannot specify a nested call input for workflow `{name}` as it does not allow \
454 nested inputs",
455 name = workflow.name()
456 );
457 }
458
459 for (name, inputs) in &self.calls {
461 let call = workflow.calls().get(name).with_context(|| {
462 format!(
463 "workflow `{workflow}` does not have a call named `{name}`",
464 workflow = workflow.name()
465 )
466 })?;
467
468 let document = call
471 .namespace()
472 .map(|ns| {
473 document
474 .namespace(ns)
475 .expect("namespace should be present")
476 .document()
477 })
478 .unwrap_or(document);
479
480 let inputs = match call.kind() {
482 CallKind::Task => {
483 let task = document
484 .task_by_name(call.name())
485 .expect("task should be present");
486
487 let task_inputs = inputs.as_task_inputs().with_context(|| {
488 format!("`{name}` is a call to a task, but workflow inputs were supplied")
489 })?;
490
491 task_inputs.validate(document, task, Some(call.specified()))?;
492 &task_inputs.inputs
493 }
494 CallKind::Workflow => {
495 let workflow = document.workflow().expect("should have a workflow");
496 assert_eq!(
497 workflow.name(),
498 call.name(),
499 "call name does not match workflow name"
500 );
501 let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
502 format!("`{name}` is a call to a workflow, but task inputs were supplied")
503 })?;
504
505 workflow_inputs.validate(document, workflow, Some(call.specified()))?;
506 &workflow_inputs.inputs
507 }
508 };
509
510 for input in inputs.keys() {
511 if call.specified().contains(input) {
512 bail!(
513 "cannot specify nested input `{input}` for call `{call}` as it was \
514 explicitly specified in the call itself",
515 call = call.name(),
516 );
517 }
518 }
519 }
520
521 if workflow.allows_nested_inputs() {
523 for (call, ty) in workflow.calls() {
524 let inputs = self.calls.get(call);
525
526 for (input, _) in ty
527 .inputs()
528 .iter()
529 .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
530 {
531 if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
532 bail!("missing required input `{input}` for call `{call}`");
533 }
534 }
535 }
536 }
537
538 Ok(())
539 }
540
541 fn set_path_value(
550 &mut self,
551 document: &Document,
552 workflow: &Workflow,
553 path: &str,
554 value: Value,
555 ) -> Result<bool> {
556 match path.split_once('.') {
557 Some((name, remainder)) => {
558 let call = workflow.calls().get(name).with_context(|| {
560 format!(
561 "workflow `{workflow}` does not have a call named `{name}`",
562 workflow = workflow.name()
563 )
564 })?;
565
566 let inputs =
568 self.calls
569 .entry(name.to_string())
570 .or_insert_with(|| match call.kind() {
571 CallKind::Task => Inputs::Task(Default::default()),
572 CallKind::Workflow => Inputs::Workflow(Default::default()),
573 });
574
575 let document = call
578 .namespace()
579 .map(|ns| {
580 document
581 .namespace(ns)
582 .expect("namespace should be present")
583 .document()
584 })
585 .unwrap_or(document);
586
587 let next = remainder
588 .split_once('.')
589 .map(|(n, _)| n)
590 .unwrap_or(remainder);
591 if call.specified().contains(next) {
592 bail!(
593 "cannot specify nested input `{next}` for call `{name}` as it was \
594 explicitly specified in the call itself",
595 );
596 }
597
598 let input = match call.kind() {
600 CallKind::Task => {
601 let task = document
602 .task_by_name(call.name())
603 .expect("task should be present");
604 inputs
605 .as_task_inputs_mut()
606 .expect("should be a task input")
607 .set_path_value(document, task, remainder, value)?
608 }
609 CallKind::Workflow => {
610 let workflow = document.workflow().expect("should have a workflow");
611 assert_eq!(
612 workflow.name(),
613 call.name(),
614 "call name does not match workflow name"
615 );
616 inputs
617 .as_workflow_inputs_mut()
618 .expect("should be a task input")
619 .set_path_value(document, workflow, remainder, value)?
620 }
621 };
622
623 if input && !workflow.allows_nested_inputs() {
624 bail!(
625 "cannot specify a nested call input for workflow `{workflow}` as it does \
626 not allow nested inputs",
627 workflow = workflow.name()
628 );
629 }
630
631 Ok(input)
632 }
633 None => {
634 let input = workflow.inputs().get(path).with_context(|| {
635 format!(
636 "workflow `{workflow}` does not have an input named `{path}`",
637 workflow = workflow.name()
638 )
639 })?;
640
641 let actual = value.ty();
643 let expected = input.ty();
644 if let Some(PrimitiveType::String) = expected.as_primitive()
645 && let Some(actual) = actual.as_primitive()
646 && actual != PrimitiveType::String
647 {
648 self.inputs
649 .insert(path.to_string(), value.to_string().into());
650 return Ok(true);
651 }
652
653 check_input_type(document, path, input, &value)?;
654 self.inputs.insert(path.to_string(), value);
655 Ok(true)
656 }
657 }
658 }
659}
660
661impl<S, V> FromIterator<(S, V)> for WorkflowInputs
662where
663 S: Into<String>,
664 V: Into<Value>,
665{
666 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
667 Self {
668 inputs: iter
669 .into_iter()
670 .map(|(k, v)| (k.into(), v.into()))
671 .collect(),
672 calls: Default::default(),
673 }
674 }
675}
676
677impl Serialize for WorkflowInputs {
678 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
679 where
680 S: serde::Serializer,
681 {
682 let mut map = serializer.serialize_map(Some(self.inputs.len()))?;
685 for (k, v) in &self.inputs {
686 let serialized_value = crate::ValueSerializer::new(None, v, true);
687 map.serialize_entry(k, &serialized_value)?;
688 }
689 map.end()
690 }
691}
692
693#[derive(Debug, Clone)]
695pub enum Inputs {
696 Task(TaskInputs),
698 Workflow(WorkflowInputs),
700}
701
702impl Inputs {
703 pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
717 let path = path.as_ref();
718
719 match path.extension().and_then(|ext| ext.to_str()) {
720 Some("json") => Self::parse_json(document, path),
721 Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
722 ext => bail!(
723 "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
724 and YAML (`.yaml` and `.yml`)",
725 ext = ext.unwrap_or("")
726 ),
727 }
728 .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
729 }
730
731 pub fn parse_json(
740 document: &Document,
741 path: impl AsRef<Path>,
742 ) -> Result<Option<(String, Self)>> {
743 let path = path.as_ref();
744
745 let file = File::open(path).with_context(|| {
746 format!("failed to open input file `{path}`", path = path.display())
747 })?;
748
749 let reader = BufReader::new(file);
751
752 let map = std::mem::take(
753 serde_json::from_reader::<_, JsonValue>(reader)?
754 .as_object_mut()
755 .with_context(|| {
756 format!(
757 "expected input file `{path}` to contain a JSON object",
758 path = path.display()
759 )
760 })?,
761 );
762
763 Self::parse_json_object(document, map)
764 }
765
766 pub fn parse_yaml(
775 document: &Document,
776 path: impl AsRef<Path>,
777 ) -> Result<Option<(String, Self)>> {
778 let path = path.as_ref();
779
780 let file = File::open(path).with_context(|| {
781 format!("failed to open input file `{path}`", path = path.display())
782 })?;
783
784 let reader = BufReader::new(file);
786 let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
787
788 let mut json = serde_json::to_value(yaml).with_context(|| {
790 format!(
791 "failed to convert YAML to JSON for processing `{path}`",
792 path = path.display()
793 )
794 })?;
795
796 let object = std::mem::take(json.as_object_mut().with_context(|| {
797 format!(
798 "expected input file `{path}` to contain a YAML mapping",
799 path = path.display()
800 )
801 })?);
802
803 Self::parse_json_object(document, object)
804 }
805
806 pub fn get(&self, name: &str) -> Option<&Value> {
808 match self {
809 Self::Task(t) => t.inputs.get(name),
810 Self::Workflow(w) => w.inputs.get(name),
811 }
812 }
813
814 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
818 match self {
819 Self::Task(inputs) => inputs.set(name, value),
820 Self::Workflow(inputs) => inputs.set(name, value),
821 }
822 }
823
824 pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
828 match self {
829 Self::Task(inputs) => Some(inputs),
830 Self::Workflow(_) => None,
831 }
832 }
833
834 pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
838 match self {
839 Self::Task(inputs) => Some(inputs),
840 Self::Workflow(_) => None,
841 }
842 }
843
844 pub fn unwrap_task_inputs(self) -> TaskInputs {
850 match self {
851 Self::Task(inputs) => inputs,
852 Self::Workflow(_) => panic!("inputs are for a workflow"),
853 }
854 }
855
856 pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
860 match self {
861 Self::Task(_) => None,
862 Self::Workflow(inputs) => Some(inputs),
863 }
864 }
865
866 pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
870 match self {
871 Self::Task(_) => None,
872 Self::Workflow(inputs) => Some(inputs),
873 }
874 }
875
876 pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
882 match self {
883 Self::Task(_) => panic!("inputs are for a task"),
884 Self::Workflow(inputs) => inputs,
885 }
886 }
887
888 pub fn parse_json_object(
894 document: &Document,
895 object: JsonMap,
896 ) -> Result<Option<(String, Self)>> {
897 if object.is_empty() {
899 return Ok(None);
900 }
901
902 let mut target_candidates = BTreeSet::new();
905 for key in object.keys() {
906 let Some((prefix, _)) = key.split_once('.') else {
907 bail!(
908 "invalid input key `{key}`: expected the key to be prefixed with the workflow \
909 or task name",
910 )
911 };
912 target_candidates.insert(prefix);
913 }
914
915 let target_name = match target_candidates
918 .iter()
919 .take(2)
920 .collect::<Vec<_>>()
921 .as_slice()
922 {
923 [] => panic!("no target candidates for inputs; report this as a bug"),
924 [target_name] => target_name.to_string(),
925 _ => bail!(
926 "invalid inputs: expected each input key to be prefixed with the same workflow or \
927 task name, but found multiple prefixes: {target_candidates:?}",
928 ),
929 };
930
931 let inputs = match (document.task_by_name(&target_name), document.workflow()) {
932 (Some(task), _) => Self::parse_task_inputs(document, task, object)?,
933 (None, Some(workflow)) if workflow.name() == target_name => {
934 Self::parse_workflow_inputs(document, workflow, object)?
935 }
936 _ => bail!(
937 "invalid inputs: a task or workflow named `{target_name}` does not exist in the \
938 document"
939 ),
940 };
941 Ok(Some((target_name, inputs)))
942 }
943
944 fn parse_task_inputs(document: &Document, task: &Task, object: JsonMap) -> Result<Self> {
946 let mut inputs = TaskInputs::default();
947 for (key, value) in object {
948 let value = serde_json::from_value(value)
950 .with_context(|| format!("invalid input value for key `{key}`"))?;
951
952 match key.split_once(".") {
953 Some((prefix, remainder)) if prefix == task.name() => {
954 inputs
955 .set_path_value(document, task, remainder, value)
956 .with_context(|| format!("invalid input key `{key}`"))?;
957 }
958 _ => {
959 bail!(
963 "invalid input key `{key}`: expected key to be prefixed with `{task}`",
964 task = task.name()
965 );
966 }
967 }
968 }
969
970 Ok(Inputs::Task(inputs))
971 }
972
973 fn parse_workflow_inputs(
975 document: &Document,
976 workflow: &Workflow,
977 object: JsonMap,
978 ) -> Result<Self> {
979 let mut inputs = WorkflowInputs::default();
980 for (key, value) in object {
981 let value = serde_json::from_value(value)
983 .with_context(|| format!("invalid input value for key `{key}`"))?;
984
985 match key.split_once(".") {
986 Some((prefix, remainder)) if prefix == workflow.name() => {
987 inputs
988 .set_path_value(document, workflow, remainder, value)
989 .with_context(|| format!("invalid input key `{key}`"))?;
990 }
991 _ => {
992 bail!(
996 "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
997 workflow = workflow.name()
998 );
999 }
1000 }
1001 }
1002
1003 Ok(Inputs::Workflow(inputs))
1004 }
1005}
1006
1007impl From<TaskInputs> for Inputs {
1008 fn from(inputs: TaskInputs) -> Self {
1009 Self::Task(inputs)
1010 }
1011}
1012
1013impl From<WorkflowInputs> for Inputs {
1014 fn from(inputs: WorkflowInputs) -> Self {
1015 Self::Workflow(inputs)
1016 }
1017}