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;
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::Value;
28
29pub type JsonMap = serde_json::Map<String, JsonValue>;
31
32fn join_paths<'a>(
35 inputs: &mut IndexMap<String, Value>,
36 path: impl Fn(&str) -> Result<&'a Path>,
37 ty: impl Fn(&str) -> Option<Type>,
38) -> Result<()> {
39 for (name, value) in inputs.iter_mut() {
40 let ty = match ty(name) {
41 Some(ty) => ty,
42 _ => {
43 continue;
44 }
45 };
46
47 let path = path(name)?;
48
49 let mut current = std::mem::replace(value, Value::None);
53 if let Ok(mut v) = current.coerce(&ty) {
54 drop(current);
55 v.visit_paths_mut(false, &mut |_, v| {
56 v.expand_path()?;
57 v.join_path_to(path);
58 v.ensure_path_exists(false)
59 })?;
60 current = v;
61 }
62
63 *value = current;
64 }
65
66 Ok(())
67}
68
69#[derive(Default, Debug, Clone)]
71pub struct TaskInputs {
72 inputs: IndexMap<String, Value>,
74 requirements: HashMap<String, Value>,
76 hints: HashMap<String, Value>,
78}
79
80impl TaskInputs {
81 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
83 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
84 }
85
86 pub fn get(&self, name: &str) -> Option<&Value> {
88 self.inputs.get(name)
89 }
90
91 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
95 self.inputs.insert(name.into(), value.into())
96 }
97
98 pub fn requirement(&self, name: &str) -> Option<&Value> {
100 self.requirements.get(name)
101 }
102
103 pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
105 self.requirements.insert(name.into(), value.into());
106 }
107
108 pub fn hint(&self, name: &str) -> Option<&Value> {
110 self.hints.get(name)
111 }
112
113 pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
115 self.hints.insert(name.into(), value.into());
116 }
117
118 pub fn join_paths<'a>(
124 &mut self,
125 task: &Task,
126 path: impl Fn(&str) -> Result<&'a Path>,
127 ) -> Result<()> {
128 join_paths(&mut self.inputs, path, |name| {
129 task.inputs().get(name).map(|input| input.ty().clone())
130 })
131 }
132
133 pub fn validate(
138 &self,
139 document: &Document,
140 task: &Task,
141 specified: Option<&HashSet<String>>,
142 ) -> Result<()> {
143 let version = document.version().context("missing document version")?;
144
145 for (name, value) in &self.inputs {
147 let input = task
148 .inputs()
149 .get(name)
150 .with_context(|| format!("unknown input `{name}`"))?;
151 let ty = value.ty();
152 if !ty.is_coercible_to(input.ty()) {
153 bail!(
154 "expected type `{expected_ty}` for input `{name}`, but found `{ty}`",
155 expected_ty = input.ty(),
156 );
157 }
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 if !expected.iter().any(|target| ty.is_coercible_to(target)) {
195 bail!(
196 "expected {expected} for hint `{name}`, but found type `{ty}`",
197 expected = display_types(expected),
198 );
199 }
200 }
201 }
202
203 Ok(())
204 }
205
206 fn set_path_value(
208 &mut self,
209 document: &Document,
210 task: &Task,
211 path: &str,
212 value: Value,
213 ) -> Result<()> {
214 let version = document.version().expect("document should have a version");
215
216 match path.split_once('.') {
217 Some((key, remainder)) => {
219 let (must_match, matched) = match key {
220 "runtime" => (
221 false,
222 task_requirement_types(version, remainder)
223 .map(|types| (true, types))
224 .or_else(|| {
225 task_hint_types(version, remainder, false)
226 .map(|types| (false, types))
227 }),
228 ),
229 "requirements" => (
230 true,
231 task_requirement_types(version, remainder).map(|types| (true, types)),
232 ),
233 "hints" => (
234 false,
235 task_hint_types(version, remainder, false).map(|types| (false, types)),
236 ),
237 _ => {
238 bail!(
239 "task `{task}` does not have an input named `{path}`",
240 task = task.name()
241 );
242 }
243 };
244
245 if let Some((requirement, expected)) = matched {
246 for ty in expected {
247 if value.ty().is_coercible_to(ty) {
248 if requirement {
249 self.requirements.insert(remainder.to_string(), value);
250 } else {
251 self.hints.insert(remainder.to_string(), value);
252 }
253 return Ok(());
254 }
255 }
256
257 bail!(
258 "expected {expected} for {key} key `{remainder}`, but found type `{ty}`",
259 expected = display_types(expected),
260 ty = value.ty()
261 );
262 } else if must_match {
263 bail!("unsupported {key} key `{remainder}`");
264 } else {
265 Ok(())
266 }
267 }
268 None => {
270 let input = task.inputs().get(path).with_context(|| {
271 format!(
272 "task `{name}` does not have an input named `{path}`",
273 name = task.name()
274 )
275 })?;
276
277 let actual = value.ty();
278 if !actual.is_coercible_to(input.ty()) {
279 bail!(
280 "expected type `{expected}` for input `{path}`, but found type `{actual}`",
281 expected = input.ty()
282 );
283 }
284 self.inputs.insert(path.to_string(), value);
285 Ok(())
286 }
287 }
288 }
289}
290
291impl<S, V> FromIterator<(S, V)> for TaskInputs
292where
293 S: Into<String>,
294 V: Into<Value>,
295{
296 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
297 Self {
298 inputs: iter
299 .into_iter()
300 .map(|(k, v)| (k.into(), v.into()))
301 .collect(),
302 requirements: Default::default(),
303 hints: Default::default(),
304 }
305 }
306}
307
308impl Serialize for TaskInputs {
309 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
310 where
311 S: serde::Serializer,
312 {
313 self.inputs.serialize(serializer)
315 }
316}
317
318#[derive(Default, Debug, Clone)]
320pub struct WorkflowInputs {
321 inputs: IndexMap<String, Value>,
323 calls: HashMap<String, Inputs>,
325}
326
327impl WorkflowInputs {
328 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
330 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
331 }
332
333 pub fn get(&self, name: &str) -> Option<&Value> {
335 self.inputs.get(name)
336 }
337
338 pub fn calls(&self) -> &HashMap<String, Inputs> {
340 &self.calls
341 }
342
343 pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
345 &mut self.calls
346 }
347
348 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
352 self.inputs.insert(name.into(), value.into())
353 }
354
355 pub fn contains(&self, name: &str) -> bool {
359 self.inputs.contains_key(name)
360 }
361
362 pub fn join_paths<'a>(
368 &mut self,
369 workflow: &Workflow,
370 path: impl Fn(&str) -> Result<&'a Path>,
371 ) -> Result<()> {
372 join_paths(&mut self.inputs, path, |name| {
373 workflow.inputs().get(name).map(|input| input.ty().clone())
374 })
375 }
376
377 pub fn validate(
382 &self,
383 document: &Document,
384 workflow: &Workflow,
385 specified: Option<&HashSet<String>>,
386 ) -> Result<()> {
387 for (name, value) in &self.inputs {
389 let input = workflow
390 .inputs()
391 .get(name)
392 .with_context(|| format!("unknown input `{name}`"))?;
393 let expected_ty = input.ty();
394 let ty = value.ty();
395 if !ty.is_coercible_to(expected_ty) {
396 bail!("expected type `{expected_ty}` for input `{name}`, but found type `{ty}`");
397 }
398 }
399
400 for (name, input) in workflow.inputs() {
402 if input.required()
403 && !self.inputs.contains_key(name)
404 && specified.map(|s| !s.contains(name)).unwrap_or(true)
405 {
406 bail!(
407 "missing required input `{name}` to workflow `{workflow}`",
408 workflow = workflow.name()
409 );
410 }
411 }
412
413 if !self.calls.is_empty() && !workflow.allows_nested_inputs() {
415 bail!(
416 "cannot specify a nested call input for workflow `{name}` as it does not allow \
417 nested inputs",
418 name = workflow.name()
419 );
420 }
421
422 for (name, inputs) in &self.calls {
424 let call = workflow.calls().get(name).with_context(|| {
425 format!(
426 "workflow `{workflow}` does not have a call named `{name}`",
427 workflow = workflow.name()
428 )
429 })?;
430
431 let document = call
434 .namespace()
435 .map(|ns| {
436 document
437 .namespace(ns)
438 .expect("namespace should be present")
439 .document()
440 })
441 .unwrap_or(document);
442
443 let inputs = match call.kind() {
445 CallKind::Task => {
446 let task = document
447 .task_by_name(call.name())
448 .expect("task should be present");
449
450 let task_inputs = inputs.as_task_inputs().with_context(|| {
451 format!("`{name}` is a call to a task, but workflow inputs were supplied")
452 })?;
453
454 task_inputs.validate(document, task, Some(call.specified()))?;
455 &task_inputs.inputs
456 }
457 CallKind::Workflow => {
458 let workflow = document.workflow().expect("should have a workflow");
459 assert_eq!(
460 workflow.name(),
461 call.name(),
462 "call name does not match workflow name"
463 );
464 let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
465 format!("`{name}` is a call to a workflow, but task inputs were supplied")
466 })?;
467
468 workflow_inputs.validate(document, workflow, Some(call.specified()))?;
469 &workflow_inputs.inputs
470 }
471 };
472
473 for input in inputs.keys() {
474 if call.specified().contains(input) {
475 bail!(
476 "cannot specify nested input `{input}` for call `{call}` as it was \
477 explicitly specified in the call itself",
478 call = call.name(),
479 );
480 }
481 }
482 }
483
484 if workflow.allows_nested_inputs() {
486 for (call, ty) in workflow.calls() {
487 let inputs = self.calls.get(call);
488
489 for (input, _) in ty
490 .inputs()
491 .iter()
492 .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
493 {
494 if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
495 bail!("missing required input `{input}` for call `{call}`");
496 }
497 }
498 }
499 }
500
501 Ok(())
502 }
503
504 fn set_path_value(
506 &mut self,
507 document: &Document,
508 workflow: &Workflow,
509 path: &str,
510 value: Value,
511 ) -> Result<()> {
512 match path.split_once('.') {
513 Some((name, remainder)) => {
514 if !workflow.allows_nested_inputs() {
516 bail!(
517 "cannot specify a nested call input for workflow `{workflow}` as it does \
518 not allow nested inputs",
519 workflow = workflow.name()
520 );
521 }
522
523 let call = workflow.calls().get(name).with_context(|| {
525 format!(
526 "workflow `{workflow}` does not have a call named `{name}`",
527 workflow = workflow.name()
528 )
529 })?;
530
531 let inputs =
533 self.calls
534 .entry(name.to_string())
535 .or_insert_with(|| match call.kind() {
536 CallKind::Task => Inputs::Task(Default::default()),
537 CallKind::Workflow => Inputs::Workflow(Default::default()),
538 });
539
540 let document = call
543 .namespace()
544 .map(|ns| {
545 document
546 .namespace(ns)
547 .expect("namespace should be present")
548 .document()
549 })
550 .unwrap_or(document);
551
552 let next = remainder
553 .split_once('.')
554 .map(|(n, _)| n)
555 .unwrap_or(remainder);
556 if call.specified().contains(next) {
557 bail!(
558 "cannot specify nested input `{next}` for call `{name}` as it was \
559 explicitly specified in the call itself",
560 );
561 }
562
563 match call.kind() {
565 CallKind::Task => {
566 let task = document
567 .task_by_name(call.name())
568 .expect("task should be present");
569 inputs
570 .as_task_inputs_mut()
571 .expect("should be a task input")
572 .set_path_value(document, task, remainder, value)
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 inputs
582 .as_workflow_inputs_mut()
583 .expect("should be a task input")
584 .set_path_value(document, workflow, remainder, value)
585 }
586 }
587 }
588 None => {
589 let input = workflow.inputs().get(path).with_context(|| {
590 format!(
591 "workflow `{workflow}` does not have an input named `{path}`",
592 workflow = workflow.name()
593 )
594 })?;
595
596 let expected = input.ty();
597 let actual = value.ty();
598 if !actual.is_coercible_to(expected) {
599 bail!(
600 "expected type `{expected}` for input `{path}`, but found type `{actual}`"
601 );
602 }
603 self.inputs.insert(path.to_string(), value);
604 Ok(())
605 }
606 }
607 }
608}
609
610impl<S, V> FromIterator<(S, V)> for WorkflowInputs
611where
612 S: Into<String>,
613 V: Into<Value>,
614{
615 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
616 Self {
617 inputs: iter
618 .into_iter()
619 .map(|(k, v)| (k.into(), v.into()))
620 .collect(),
621 calls: Default::default(),
622 }
623 }
624}
625
626impl Serialize for WorkflowInputs {
627 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
628 where
629 S: serde::Serializer,
630 {
631 self.inputs.serialize(serializer)
634 }
635}
636
637#[derive(Debug, Clone)]
639pub enum Inputs {
640 Task(TaskInputs),
642 Workflow(WorkflowInputs),
644}
645
646impl Inputs {
647 pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
661 let path = path.as_ref();
662
663 match path.extension().and_then(|ext| ext.to_str()) {
664 Some("json") => Self::parse_json(document, path),
665 Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
666 ext => bail!(
667 "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
668 and YAML (`.yaml` and `.yml`)",
669 ext = ext.unwrap_or("")
670 ),
671 }
672 .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
673 }
674
675 pub fn parse_json(
684 document: &Document,
685 path: impl AsRef<Path>,
686 ) -> Result<Option<(String, Self)>> {
687 let path = path.as_ref();
688
689 let file = File::open(path).with_context(|| {
690 format!("failed to open input file `{path}`", path = path.display())
691 })?;
692
693 let reader = BufReader::new(file);
695
696 let map = std::mem::take(
697 serde_json::from_reader::<_, JsonValue>(reader)?
698 .as_object_mut()
699 .with_context(|| {
700 format!(
701 "expected input file `{path}` to contain a JSON object",
702 path = path.display()
703 )
704 })?,
705 );
706
707 Self::parse_object(document, map)
708 }
709
710 pub fn parse_yaml(
719 document: &Document,
720 path: impl AsRef<Path>,
721 ) -> Result<Option<(String, Self)>> {
722 let path = path.as_ref();
723
724 let file = File::open(path).with_context(|| {
725 format!("failed to open input file `{path}`", path = path.display())
726 })?;
727
728 let reader = BufReader::new(file);
730 let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
731
732 let mut json = serde_json::to_value(yaml).with_context(|| {
734 format!(
735 "failed to convert YAML to JSON for processing `{path}`",
736 path = path.display()
737 )
738 })?;
739
740 let object = std::mem::take(json.as_object_mut().with_context(|| {
741 format!(
742 "expected input file `{path}` to contain a YAML mapping",
743 path = path.display()
744 )
745 })?);
746
747 Self::parse_object(document, object)
748 }
749
750 pub fn get(&self, name: &str) -> Option<&Value> {
752 match self {
753 Self::Task(t) => t.inputs.get(name),
754 Self::Workflow(w) => w.inputs.get(name),
755 }
756 }
757
758 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
762 match self {
763 Self::Task(inputs) => inputs.set(name, value),
764 Self::Workflow(inputs) => inputs.set(name, value),
765 }
766 }
767
768 pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
772 match self {
773 Self::Task(inputs) => Some(inputs),
774 Self::Workflow(_) => None,
775 }
776 }
777
778 pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
782 match self {
783 Self::Task(inputs) => Some(inputs),
784 Self::Workflow(_) => None,
785 }
786 }
787
788 pub fn unwrap_task_inputs(self) -> TaskInputs {
794 match self {
795 Self::Task(inputs) => inputs,
796 Self::Workflow(_) => panic!("inputs are for a workflow"),
797 }
798 }
799
800 pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
804 match self {
805 Self::Task(_) => None,
806 Self::Workflow(inputs) => Some(inputs),
807 }
808 }
809
810 pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
814 match self {
815 Self::Task(_) => None,
816 Self::Workflow(inputs) => Some(inputs),
817 }
818 }
819
820 pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
826 match self {
827 Self::Task(_) => panic!("inputs are for a task"),
828 Self::Workflow(inputs) => inputs,
829 }
830 }
831
832 pub fn parse_object(document: &Document, object: JsonMap) -> Result<Option<(String, Self)>> {
838 let (key, name) = match object.iter().next() {
840 Some((key, _)) => match key.split_once('.') {
841 Some((name, _)) => (key, name),
842 None => {
843 bail!(
844 "invalid input key `{key}`: expected the value to be prefixed with the \
845 workflow or task name",
846 )
847 }
848 },
849 None => {
851 return Ok(None);
852 }
853 };
854
855 match (document.task_by_name(name), document.workflow()) {
856 (Some(task), _) => Ok(Some(Self::parse_task_inputs(document, task, object)?)),
857 (None, Some(workflow)) if workflow.name() == name => Ok(Some(
858 Self::parse_workflow_inputs(document, workflow, object)?,
859 )),
860 _ => bail!(
861 "invalid input key `{key}`: a task or workflow named `{name}` does not exist in \
862 the document"
863 ),
864 }
865 }
866
867 fn parse_task_inputs(
869 document: &Document,
870 task: &Task,
871 object: JsonMap,
872 ) -> Result<(String, Self)> {
873 let mut inputs = TaskInputs::default();
874 for (key, value) in object {
875 let value = serde_json::from_value(value)
876 .with_context(|| format!("invalid input key `{key}`"))?;
877
878 match key.split_once(".") {
879 Some((prefix, remainder)) if prefix == task.name() => {
880 inputs
881 .set_path_value(document, task, remainder, value)
882 .with_context(|| format!("invalid input key `{key}`"))?;
883 }
884 _ => {
885 bail!(
886 "invalid input key `{key}`: expected key to be prefixed with `{task}`",
887 task = task.name()
888 );
889 }
890 }
891 }
892
893 Ok((task.name().to_string(), Inputs::Task(inputs)))
894 }
895
896 fn parse_workflow_inputs(
898 document: &Document,
899 workflow: &Workflow,
900 object: JsonMap,
901 ) -> Result<(String, Self)> {
902 let mut inputs = WorkflowInputs::default();
903 for (key, value) in object {
904 let value = serde_json::from_value(value)
905 .with_context(|| format!("invalid input key `{key}`"))?;
906
907 match key.split_once(".") {
908 Some((prefix, remainder)) if prefix == workflow.name() => {
909 inputs
910 .set_path_value(document, workflow, remainder, value)
911 .with_context(|| format!("invalid input key `{key}`"))?;
912 }
913 _ => {
914 bail!(
915 "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
916 workflow = workflow.name()
917 );
918 }
919 }
920 }
921
922 Ok((workflow.name().to_string(), Inputs::Workflow(inputs)))
923 }
924}
925
926impl From<TaskInputs> for Inputs {
927 fn from(inputs: TaskInputs) -> Self {
928 Self::Task(inputs)
929 }
930}
931
932impl From<WorkflowInputs> for Inputs {
933 fn from(inputs: WorkflowInputs) -> Self {
934 Self::Workflow(inputs)
935 }
936}