1use std::collections::HashMap;
4use std::collections::HashSet;
5use std::fs::File;
6use std::io::BufReader;
7use std::path::Path;
8
9use anyhow::Context;
10use anyhow::Result;
11use anyhow::bail;
12use indexmap::IndexMap;
13use serde::Serialize;
14use serde_json::Value as JsonValue;
15use serde_yaml_ng::Value as YamlValue;
16use wdl_analysis::document::Document;
17use wdl_analysis::document::Task;
18use wdl_analysis::document::Workflow;
19use wdl_analysis::types::CallKind;
20use wdl_analysis::types::Coercible as _;
21use wdl_analysis::types::Type;
22use wdl_analysis::types::display_types;
23use wdl_analysis::types::v1::task_hint_types;
24use wdl_analysis::types::v1::task_requirement_types;
25
26use crate::Coercible;
27use crate::Object;
28use crate::Value;
29
30fn join_paths<'a>(
33 inputs: &mut IndexMap<String, Value>,
34 path: impl Fn(&str) -> Result<&'a Path>,
35 ty: impl Fn(&str) -> Option<Type>,
36) -> Result<()> {
37 for (name, value) in inputs.iter_mut() {
38 let ty = match ty(name) {
39 Some(ty) => ty,
40 _ => {
41 continue;
42 }
43 };
44
45 let path = path(name)?;
46
47 let mut current = std::mem::replace(value, Value::None);
51 if let Ok(mut v) = current.coerce(&ty) {
52 drop(current);
53 v.visit_paths_mut(false, &mut |_, v| {
54 v.expand_path()?;
55 v.join_path_to(path);
56 v.ensure_path_exists(false)
57 })?;
58 current = v;
59 }
60
61 *value = current;
62 }
63
64 Ok(())
65}
66
67#[derive(Default, Debug, Clone)]
69pub struct TaskInputs {
70 inputs: IndexMap<String, Value>,
72 requirements: HashMap<String, Value>,
74 hints: HashMap<String, Value>,
76}
77
78impl TaskInputs {
79 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
81 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
82 }
83
84 pub fn get(&self, name: &str) -> Option<&Value> {
86 self.inputs.get(name)
87 }
88
89 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
93 self.inputs.insert(name.into(), value.into())
94 }
95
96 pub fn requirement(&self, name: &str) -> Option<&Value> {
98 self.requirements.get(name)
99 }
100
101 pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
103 self.requirements.insert(name.into(), value.into());
104 }
105
106 pub fn hint(&self, name: &str) -> Option<&Value> {
108 self.hints.get(name)
109 }
110
111 pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
113 self.hints.insert(name.into(), value.into());
114 }
115
116 pub fn join_paths<'a>(
122 &mut self,
123 task: &Task,
124 path: impl Fn(&str) -> Result<&'a Path>,
125 ) -> Result<()> {
126 join_paths(&mut self.inputs, path, |name| {
127 task.inputs().get(name).map(|input| input.ty().clone())
128 })
129 }
130
131 pub fn validate(
136 &self,
137 document: &Document,
138 task: &Task,
139 specified: Option<&HashSet<String>>,
140 ) -> Result<()> {
141 let version = document.version().context("missing document version")?;
142
143 for (name, value) in &self.inputs {
145 let input = task
146 .inputs()
147 .get(name)
148 .with_context(|| format!("unknown input `{name}`"))?;
149 let ty = value.ty();
150 if !ty.is_coercible_to(input.ty()) {
151 bail!(
152 "expected type `{expected_ty}` for input `{name}`, but found `{ty}`",
153 expected_ty = input.ty(),
154 );
155 }
156 }
157
158 for (name, input) in task.inputs() {
160 if input.required()
161 && !self.inputs.contains_key(name)
162 && specified.map(|s| !s.contains(name)).unwrap_or(true)
163 {
164 bail!(
165 "missing required input `{name}` to task `{task}`",
166 task = task.name()
167 );
168 }
169 }
170
171 for (name, value) in &self.requirements {
173 let ty = value.ty();
174 if let Some(expected) = task_requirement_types(version, name.as_str()) {
175 if !expected.iter().any(|target| ty.is_coercible_to(target)) {
176 bail!(
177 "expected {expected} for requirement `{name}`, but found type `{ty}`",
178 expected = display_types(expected),
179 );
180 }
181
182 continue;
183 }
184
185 bail!("unsupported requirement `{name}`");
186 }
187
188 for (name, value) in &self.hints {
190 let ty = value.ty();
191 if let Some(expected) = task_hint_types(version, name.as_str(), false) {
192 if !expected.iter().any(|target| ty.is_coercible_to(target)) {
193 bail!(
194 "expected {expected} for hint `{name}`, but found type `{ty}`",
195 expected = display_types(expected),
196 );
197 }
198 }
199 }
200
201 Ok(())
202 }
203
204 fn set_path_value(
206 &mut self,
207 document: &Document,
208 task: &Task,
209 path: &str,
210 value: Value,
211 ) -> Result<()> {
212 let version = document.version().expect("document should have a version");
213
214 match path.split_once('.') {
215 Some((key, remainder)) => {
217 let (must_match, matched) = match key {
218 "runtime" => (
219 false,
220 task_requirement_types(version, remainder)
221 .map(|types| (true, types))
222 .or_else(|| {
223 task_hint_types(version, remainder, false)
224 .map(|types| (false, types))
225 }),
226 ),
227 "requirements" => (
228 true,
229 task_requirement_types(version, remainder).map(|types| (true, types)),
230 ),
231 "hints" => (
232 false,
233 task_hint_types(version, remainder, false).map(|types| (false, types)),
234 ),
235 _ => {
236 bail!(
237 "task `{task}` does not have an input named `{path}`",
238 task = task.name()
239 );
240 }
241 };
242
243 if let Some((requirement, expected)) = matched {
244 for ty in expected {
245 if value.ty().is_coercible_to(ty) {
246 if requirement {
247 self.requirements.insert(remainder.to_string(), value);
248 } else {
249 self.hints.insert(remainder.to_string(), value);
250 }
251 return Ok(());
252 }
253 }
254
255 bail!(
256 "expected {expected} for {key} key `{remainder}`, but found type `{ty}`",
257 expected = display_types(expected),
258 ty = value.ty()
259 );
260 } else if must_match {
261 bail!("unsupported {key} key `{remainder}`");
262 } else {
263 Ok(())
264 }
265 }
266 None => {
268 let input = task.inputs().get(path).with_context(|| {
269 format!(
270 "task `{name}` does not have an input named `{path}`",
271 name = task.name()
272 )
273 })?;
274
275 let actual = value.ty();
276 if !actual.is_coercible_to(input.ty()) {
277 bail!(
278 "expected type `{expected}` for input `{path}`, but found type `{actual}`",
279 expected = input.ty()
280 );
281 }
282 self.inputs.insert(path.to_string(), value);
283 Ok(())
284 }
285 }
286 }
287}
288
289impl<S, V> FromIterator<(S, V)> for TaskInputs
290where
291 S: Into<String>,
292 V: Into<Value>,
293{
294 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
295 Self {
296 inputs: iter
297 .into_iter()
298 .map(|(k, v)| (k.into(), v.into()))
299 .collect(),
300 requirements: Default::default(),
301 hints: Default::default(),
302 }
303 }
304}
305
306impl Serialize for TaskInputs {
307 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 S: serde::Serializer,
310 {
311 self.inputs.serialize(serializer)
313 }
314}
315
316#[derive(Default, Debug, Clone)]
318pub struct WorkflowInputs {
319 inputs: IndexMap<String, Value>,
321 calls: HashMap<String, Inputs>,
323}
324
325impl WorkflowInputs {
326 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
328 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
329 }
330
331 pub fn get(&self, name: &str) -> Option<&Value> {
333 self.inputs.get(name)
334 }
335
336 pub fn calls(&self) -> &HashMap<String, Inputs> {
338 &self.calls
339 }
340
341 pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
343 &mut self.calls
344 }
345
346 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
350 self.inputs.insert(name.into(), value.into())
351 }
352
353 pub fn contains(&self, name: &str) -> bool {
357 self.inputs.contains_key(name)
358 }
359
360 pub fn join_paths<'a>(
366 &mut self,
367 workflow: &Workflow,
368 path: impl Fn(&str) -> Result<&'a Path>,
369 ) -> Result<()> {
370 join_paths(&mut self.inputs, path, |name| {
371 workflow.inputs().get(name).map(|input| input.ty().clone())
372 })
373 }
374
375 pub fn validate(
380 &self,
381 document: &Document,
382 workflow: &Workflow,
383 specified: Option<&HashSet<String>>,
384 ) -> Result<()> {
385 for (name, value) in &self.inputs {
387 let input = workflow
388 .inputs()
389 .get(name)
390 .with_context(|| format!("unknown input `{name}`"))?;
391 let expected_ty = input.ty();
392 let ty = value.ty();
393 if !ty.is_coercible_to(expected_ty) {
394 bail!("expected type `{expected_ty}` for input `{name}`, but found type `{ty}`");
395 }
396 }
397
398 for (name, input) in workflow.inputs() {
400 if input.required()
401 && !self.inputs.contains_key(name)
402 && specified.map(|s| !s.contains(name)).unwrap_or(true)
403 {
404 bail!(
405 "missing required input `{name}` to workflow `{workflow}`",
406 workflow = workflow.name()
407 );
408 }
409 }
410
411 if !self.calls.is_empty() && !workflow.allows_nested_inputs() {
413 bail!(
414 "cannot specify a nested call input for workflow `{name}` as it does not allow \
415 nested inputs",
416 name = workflow.name()
417 );
418 }
419
420 for (name, inputs) in &self.calls {
422 let call = workflow.calls().get(name).with_context(|| {
423 format!(
424 "workflow `{workflow}` does not have a call named `{name}`",
425 workflow = workflow.name()
426 )
427 })?;
428
429 let document = call
432 .namespace()
433 .map(|ns| {
434 document
435 .namespace(ns)
436 .expect("namespace should be present")
437 .document()
438 })
439 .unwrap_or(document);
440
441 let inputs = match call.kind() {
443 CallKind::Task => {
444 let task = document
445 .task_by_name(call.name())
446 .expect("task should be present");
447
448 let task_inputs = inputs.as_task_inputs().with_context(|| {
449 format!("`{name}` is a call to a task, but workflow inputs were supplied")
450 })?;
451
452 task_inputs.validate(document, task, Some(call.specified()))?;
453 &task_inputs.inputs
454 }
455 CallKind::Workflow => {
456 let workflow = document.workflow().expect("should have a workflow");
457 assert_eq!(
458 workflow.name(),
459 call.name(),
460 "call name does not match workflow name"
461 );
462 let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
463 format!("`{name}` is a call to a workflow, but task inputs were supplied")
464 })?;
465
466 workflow_inputs.validate(document, workflow, Some(call.specified()))?;
467 &workflow_inputs.inputs
468 }
469 };
470
471 for input in inputs.keys() {
472 if call.specified().contains(input) {
473 bail!(
474 "cannot specify nested input `{input}` for call `{call}` as it was \
475 explicitly specified in the call itself",
476 call = call.name(),
477 );
478 }
479 }
480 }
481
482 if workflow.allows_nested_inputs() {
484 for (call, ty) in workflow.calls() {
485 let inputs = self.calls.get(call);
486
487 for (input, _) in ty
488 .inputs()
489 .iter()
490 .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
491 {
492 if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
493 bail!("missing required input `{input}` for call `{call}`");
494 }
495 }
496 }
497 }
498
499 Ok(())
500 }
501
502 fn set_path_value(
504 &mut self,
505 document: &Document,
506 workflow: &Workflow,
507 path: &str,
508 value: Value,
509 ) -> Result<()> {
510 match path.split_once('.') {
511 Some((name, remainder)) => {
512 if !workflow.allows_nested_inputs() {
514 bail!(
515 "cannot specify a nested call input for workflow `{workflow}` as it does \
516 not allow nested inputs",
517 workflow = workflow.name()
518 );
519 }
520
521 let call = workflow.calls().get(name).with_context(|| {
523 format!(
524 "workflow `{workflow}` does not have a call named `{name}`",
525 workflow = workflow.name()
526 )
527 })?;
528
529 let inputs =
531 self.calls
532 .entry(name.to_string())
533 .or_insert_with(|| match call.kind() {
534 CallKind::Task => Inputs::Task(Default::default()),
535 CallKind::Workflow => Inputs::Workflow(Default::default()),
536 });
537
538 let document = call
541 .namespace()
542 .map(|ns| {
543 document
544 .namespace(ns)
545 .expect("namespace should be present")
546 .document()
547 })
548 .unwrap_or(document);
549
550 let next = remainder
551 .split_once('.')
552 .map(|(n, _)| n)
553 .unwrap_or(remainder);
554 if call.specified().contains(next) {
555 bail!(
556 "cannot specify nested input `{next}` for call `{name}` as it was \
557 explicitly specified in the call itself",
558 );
559 }
560
561 match call.kind() {
563 CallKind::Task => {
564 let task = document
565 .task_by_name(call.name())
566 .expect("task should be present");
567 inputs
568 .as_task_inputs_mut()
569 .expect("should be a task input")
570 .set_path_value(document, task, remainder, value)
571 }
572 CallKind::Workflow => {
573 let workflow = document.workflow().expect("should have a workflow");
574 assert_eq!(
575 workflow.name(),
576 call.name(),
577 "call name does not match workflow name"
578 );
579 inputs
580 .as_workflow_inputs_mut()
581 .expect("should be a task input")
582 .set_path_value(document, workflow, remainder, value)
583 }
584 }
585 }
586 None => {
587 let input = workflow.inputs().get(path).with_context(|| {
588 format!(
589 "workflow `{workflow}` does not have an input named `{path}`",
590 workflow = workflow.name()
591 )
592 })?;
593
594 let expected = input.ty();
595 let actual = value.ty();
596 if !actual.is_coercible_to(expected) {
597 bail!(
598 "expected type `{expected}` for input `{path}`, but found type `{actual}`"
599 );
600 }
601 self.inputs.insert(path.to_string(), value);
602 Ok(())
603 }
604 }
605 }
606}
607
608impl<S, V> FromIterator<(S, V)> for WorkflowInputs
609where
610 S: Into<String>,
611 V: Into<Value>,
612{
613 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
614 Self {
615 inputs: iter
616 .into_iter()
617 .map(|(k, v)| (k.into(), v.into()))
618 .collect(),
619 calls: Default::default(),
620 }
621 }
622}
623
624impl Serialize for WorkflowInputs {
625 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
626 where
627 S: serde::Serializer,
628 {
629 self.inputs.serialize(serializer)
632 }
633}
634
635#[derive(Debug, Clone)]
637pub enum Inputs {
638 Task(TaskInputs),
640 Workflow(WorkflowInputs),
642}
643
644impl Inputs {
645 pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
659 let path = path.as_ref();
660
661 match path.extension().and_then(|ext| ext.to_str()) {
662 Some("json") => Self::parse_json(document, path),
663 Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
664 ext => bail!(
665 "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
666 and YAML (`.yaml` and `.yml`)",
667 ext = ext.unwrap_or("")
668 ),
669 }
670 .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
671 }
672
673 pub fn parse_json(
682 document: &Document,
683 path: impl AsRef<Path>,
684 ) -> Result<Option<(String, Self)>> {
685 let path = path.as_ref();
686
687 let file = File::open(path).with_context(|| {
688 format!("failed to open input file `{path}`", path = path.display())
689 })?;
690
691 let reader = BufReader::new(file);
693
694 let object = std::mem::take(
695 serde_json::from_reader::<_, JsonValue>(reader)?
696 .as_object_mut()
697 .with_context(|| {
698 format!(
699 "expected input file `{path}` to contain a JSON object",
700 path = path.display()
701 )
702 })?,
703 )
704 .into_iter()
705 .map(|(key, value)| {
706 let value: Value = serde_json::from_value(value)
707 .with_context(|| format!("invalid input key `{key}`"))?;
708 Ok((key, value))
709 })
710 .collect::<Result<IndexMap<_, _>>>()
711 .map(Object::from)?;
712
713 Self::parse_object(document, object)
714 }
715
716 pub fn parse_yaml(
725 document: &Document,
726 path: impl AsRef<Path>,
727 ) -> Result<Option<(String, Self)>> {
728 let path = path.as_ref();
729
730 let file = File::open(path).with_context(|| {
731 format!("failed to open input file `{path}`", path = path.display())
732 })?;
733
734 let reader = BufReader::new(file);
736 let mut yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
737 let object = std::mem::take(yaml.as_mapping_mut().with_context(|| {
738 format!(
739 "expected input file `{path}` to contain a YAML mapping",
740 path = path.display()
741 )
742 })?)
743 .into_iter()
744 .map(|(key, value)| {
745 let key = key
746 .as_str()
747 .with_context(|| {
748 format!(
749 "input file `{path}` contains non-string keys in mapping",
750 path = path.display()
751 )
752 })?
753 .to_owned();
754 let value: Value = serde_yaml_ng::from_value(value)
755 .with_context(|| format!("invalid input key `{key}`"))?;
756 Ok((key, value))
757 })
758 .collect::<Result<IndexMap<_, _>>>()
759 .map(Object::from)?;
760
761 Self::parse_object(document, object)
762 }
763
764 pub fn get(&self, name: &str) -> Option<&Value> {
766 match self {
767 Self::Task(t) => t.inputs.get(name),
768 Self::Workflow(w) => w.inputs.get(name),
769 }
770 }
771
772 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
776 match self {
777 Self::Task(inputs) => inputs.set(name, value),
778 Self::Workflow(inputs) => inputs.set(name, value),
779 }
780 }
781
782 pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
786 match self {
787 Self::Task(inputs) => Some(inputs),
788 Self::Workflow(_) => None,
789 }
790 }
791
792 pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
796 match self {
797 Self::Task(inputs) => Some(inputs),
798 Self::Workflow(_) => None,
799 }
800 }
801
802 pub fn unwrap_task_inputs(self) -> TaskInputs {
808 match self {
809 Self::Task(inputs) => inputs,
810 Self::Workflow(_) => panic!("inputs are for a workflow"),
811 }
812 }
813
814 pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
818 match self {
819 Self::Task(_) => None,
820 Self::Workflow(inputs) => Some(inputs),
821 }
822 }
823
824 pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
828 match self {
829 Self::Task(_) => None,
830 Self::Workflow(inputs) => Some(inputs),
831 }
832 }
833
834 pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
840 match self {
841 Self::Task(_) => panic!("inputs are for a task"),
842 Self::Workflow(inputs) => inputs,
843 }
844 }
845
846 pub fn parse_object(document: &Document, object: Object) -> Result<Option<(String, Self)>> {
852 let (key, name) = match object.iter().next() {
854 Some((key, _)) => match key.split_once('.') {
855 Some((name, _)) => (key, name),
856 None => {
857 bail!(
858 "invalid input key `{key}`: expected the value to be prefixed with the \
859 workflow or task name",
860 )
861 }
862 },
863 None => {
865 return Ok(None);
866 }
867 };
868
869 match (document.task_by_name(name), document.workflow()) {
870 (Some(task), _) => Ok(Some(Self::parse_task_inputs(document, task, object)?)),
871 (None, Some(workflow)) if workflow.name() == name => Ok(Some(
872 Self::parse_workflow_inputs(document, workflow, object)?,
873 )),
874 _ => bail!(
875 "invalid input key `{key}`: a task or workflow named `{name}` does not exist in \
876 the document"
877 ),
878 }
879 }
880
881 fn parse_task_inputs(
883 document: &Document,
884 task: &Task,
885 object: Object,
886 ) -> Result<(String, Self)> {
887 let mut inputs = TaskInputs::default();
888 for (key, value) in object.iter() {
889 match key.split_once(".") {
890 Some((prefix, remainder)) if prefix == task.name() => {
891 inputs
892 .set_path_value(document, task, remainder, value.clone())
893 .with_context(|| format!("invalid input key `{key}`"))?;
894 }
895 _ => {
896 bail!(
897 "invalid input key `{key}`: expected key to be prefixed with `{task}`",
898 task = task.name()
899 );
900 }
901 }
902 }
903
904 Ok((task.name().to_string(), Inputs::Task(inputs)))
905 }
906
907 fn parse_workflow_inputs(
909 document: &Document,
910 workflow: &Workflow,
911 object: Object,
912 ) -> Result<(String, Self)> {
913 let mut inputs = WorkflowInputs::default();
914 for (key, value) in object.iter() {
915 match key.split_once(".") {
916 Some((prefix, remainder)) if prefix == workflow.name() => {
917 inputs
918 .set_path_value(document, workflow, remainder, value.clone())
919 .with_context(|| format!("invalid input key `{key}`"))?;
920 }
921 _ => {
922 bail!(
923 "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
924 workflow = workflow.name()
925 );
926 }
927 }
928 }
929
930 Ok((workflow.name().to_string(), Inputs::Workflow(inputs)))
931 }
932}
933
934impl From<TaskInputs> for Inputs {
935 fn from(inputs: TaskInputs) -> Self {
936 Self::Task(inputs)
937 }
938}
939
940impl From<WorkflowInputs> for Inputs {
941 fn from(inputs: WorkflowInputs) -> Self {
942 Self::Workflow(inputs)
943 }
944}