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::ser::SerializeMap;
15use serde_json::Value as JsonValue;
16use serde_yaml_ng::Value as YamlValue;
17use wdl_analysis::Document;
18use wdl_analysis::document::Task;
19use wdl_analysis::document::Workflow;
20use wdl_analysis::types::CallKind;
21use wdl_analysis::types::Coercible as _;
22use wdl_analysis::types::PrimitiveType;
23use wdl_analysis::types::Type;
24use wdl_analysis::types::display_types;
25use wdl_analysis::types::v1::task_hint_types;
26use wdl_analysis::types::v1::task_requirement_types;
27
28use crate::Coercible;
29use crate::Value;
30
31pub type JsonMap = serde_json::Map<String, JsonValue>;
33
34fn join_paths<'a>(
37 inputs: &mut IndexMap<String, Value>,
38 path: impl Fn(&str) -> Result<&'a Path>,
39 ty: impl Fn(&str) -> Option<Type>,
40) -> Result<()> {
41 for (name, value) in inputs.iter_mut() {
42 let ty = match ty(name) {
43 Some(ty) => ty,
44 _ => {
45 continue;
46 }
47 };
48
49 let path = path(name)?;
50
51 let mut current = std::mem::replace(value, Value::None(value.ty()));
55 if let Ok(mut v) = current.coerce(None, &ty) {
56 drop(current);
57 v.visit_paths_mut(false, &mut |_, v| {
58 v.expand_path(path)?;
59 v.ensure_path_exists(false, None)
60 })?;
61 current = v;
62 }
63
64 *value = current;
65 }
66
67 Ok(())
68}
69
70#[derive(Default, Debug, Clone)]
72pub struct TaskInputs {
73 inputs: IndexMap<String, Value>,
75 requirements: HashMap<String, Value>,
77 hints: HashMap<String, Value>,
79}
80
81impl TaskInputs {
82 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
84 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
85 }
86
87 pub fn get(&self, name: &str) -> Option<&Value> {
89 self.inputs.get(name)
90 }
91
92 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
96 self.inputs.insert(name.into(), value.into())
97 }
98
99 pub fn requirement(&self, name: &str) -> Option<&Value> {
101 self.requirements.get(name)
102 }
103
104 pub fn override_requirement(&mut self, name: impl Into<String>, value: impl Into<Value>) {
106 self.requirements.insert(name.into(), value.into());
107 }
108
109 pub fn hint(&self, name: &str) -> Option<&Value> {
111 self.hints.get(name)
112 }
113
114 pub fn override_hint(&mut self, name: impl Into<String>, value: impl Into<Value>) {
116 self.hints.insert(name.into(), value.into());
117 }
118
119 pub fn join_paths<'a>(
125 &mut self,
126 task: &Task,
127 path: impl Fn(&str) -> Result<&'a Path>,
128 ) -> Result<()> {
129 join_paths(&mut self.inputs, path, |name| {
130 task.inputs().get(name).map(|input| input.ty().clone())
131 })
132 }
133
134 pub fn validate(
139 &self,
140 document: &Document,
141 task: &Task,
142 specified: Option<&HashSet<String>>,
143 ) -> Result<()> {
144 let version = document.version().context("missing document version")?;
145
146 for (name, value) in &self.inputs {
148 let input = task
149 .inputs()
150 .get(name)
151 .with_context(|| format!("unknown input `{name}`"))?;
152 let ty = value.ty();
153 if !ty.is_coercible_to(input.ty()) {
154 bail!(
155 "expected type `{expected_ty}` for input `{name}`, but found `{ty}`",
156 expected_ty = input.ty(),
157 );
158 }
159 }
160
161 for (name, input) in task.inputs() {
163 if input.required()
164 && !self.inputs.contains_key(name)
165 && specified.map(|s| !s.contains(name)).unwrap_or(true)
166 {
167 bail!(
168 "missing required input `{name}` to task `{task}`",
169 task = task.name()
170 );
171 }
172 }
173
174 for (name, value) in &self.requirements {
176 let ty = value.ty();
177 if let Some(expected) = task_requirement_types(version, name.as_str()) {
178 if !expected.iter().any(|target| ty.is_coercible_to(target)) {
179 bail!(
180 "expected {expected} for requirement `{name}`, but found type `{ty}`",
181 expected = display_types(expected),
182 );
183 }
184
185 continue;
186 }
187
188 bail!("unsupported requirement `{name}`");
189 }
190
191 for (name, value) in &self.hints {
193 let ty = value.ty();
194 if let Some(expected) = task_hint_types(version, name.as_str(), false)
195 && !expected.iter().any(|target| ty.is_coercible_to(target))
196 {
197 bail!(
198 "expected {expected} for hint `{name}`, but found type `{ty}`",
199 expected = display_types(expected),
200 );
201 }
202 }
203
204 Ok(())
205 }
206
207 fn set_path_value(
214 &mut self,
215 document: &Document,
216 task: &Task,
217 path: &str,
218 value: Value,
219 ) -> Result<()> {
220 let version = document.version().expect("document should have a version");
221
222 match path.split_once('.') {
223 Some((key, remainder)) => {
225 let (must_match, matched) = match key {
226 "runtime" => (
227 false,
228 task_requirement_types(version, remainder)
229 .map(|types| (true, types))
230 .or_else(|| {
231 task_hint_types(version, remainder, false)
232 .map(|types| (false, types))
233 }),
234 ),
235 "requirements" => (
236 true,
237 task_requirement_types(version, remainder).map(|types| (true, types)),
238 ),
239 "hints" => (
240 false,
241 task_hint_types(version, remainder, false).map(|types| (false, types)),
242 ),
243 _ => {
244 bail!(
245 "task `{task}` does not have an input named `{path}`",
246 task = task.name()
247 );
248 }
249 };
250
251 if let Some((requirement, expected)) = matched {
252 for ty in expected {
253 if value.ty().is_coercible_to(ty) {
254 if requirement {
255 self.requirements.insert(remainder.to_string(), value);
256 } else {
257 self.hints.insert(remainder.to_string(), value);
258 }
259 return Ok(());
260 }
261 }
262
263 bail!(
264 "expected {expected} for {key} key `{remainder}`, but found type `{ty}`",
265 expected = display_types(expected),
266 ty = value.ty()
267 );
268 } else if must_match {
269 bail!("unsupported {key} key `{remainder}`");
270 } else {
271 Ok(())
272 }
273 }
274 None => {
276 let input = task.inputs().get(path).with_context(|| {
277 format!(
278 "task `{name}` does not have an input named `{path}`",
279 name = task.name()
280 )
281 })?;
282
283 let actual = value.ty();
284 let expected = input.ty();
285 if let Some(expected_prim_ty) = expected.as_primitive()
286 && expected_prim_ty == PrimitiveType::String
287 && let Some(actual_prim_ty) = actual.as_primitive()
288 && actual_prim_ty != PrimitiveType::String
289 {
290 self.inputs
291 .insert(path.to_string(), value.to_string().into());
292 return Ok(());
293 }
294 if !actual.is_coercible_to(expected) {
295 bail!(
296 "expected type `{expected}` for input `{path}`, but found type `{actual}`",
297 );
298 }
299 self.inputs.insert(path.to_string(), value);
300 Ok(())
301 }
302 }
303 }
304}
305
306impl<S, V> FromIterator<(S, V)> for TaskInputs
307where
308 S: Into<String>,
309 V: Into<Value>,
310{
311 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
312 Self {
313 inputs: iter
314 .into_iter()
315 .map(|(k, v)| (k.into(), v.into()))
316 .collect(),
317 requirements: Default::default(),
318 hints: Default::default(),
319 }
320 }
321}
322
323impl Serialize for TaskInputs {
324 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
325 where
326 S: serde::Serializer,
327 {
328 let mut map = serializer.serialize_map(Some(self.inputs.len()))?;
330 for (k, v) in &self.inputs {
331 let serialized_value = crate::ValueSerializer::new(v, true);
332 map.serialize_entry(k, &serialized_value)?;
333 }
334 map.end()
335 }
336}
337
338#[derive(Default, Debug, Clone)]
340pub struct WorkflowInputs {
341 inputs: IndexMap<String, Value>,
343 calls: HashMap<String, Inputs>,
345}
346
347impl WorkflowInputs {
348 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> + use<'_> {
350 self.inputs.iter().map(|(k, v)| (k.as_str(), v))
351 }
352
353 pub fn get(&self, name: &str) -> Option<&Value> {
355 self.inputs.get(name)
356 }
357
358 pub fn calls(&self) -> &HashMap<String, Inputs> {
360 &self.calls
361 }
362
363 pub fn calls_mut(&mut self) -> &mut HashMap<String, Inputs> {
365 &mut self.calls
366 }
367
368 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
372 self.inputs.insert(name.into(), value.into())
373 }
374
375 pub fn contains(&self, name: &str) -> bool {
379 self.inputs.contains_key(name)
380 }
381
382 pub fn join_paths<'a>(
388 &mut self,
389 workflow: &Workflow,
390 path: impl Fn(&str) -> Result<&'a Path>,
391 ) -> Result<()> {
392 join_paths(&mut self.inputs, path, |name| {
393 workflow.inputs().get(name).map(|input| input.ty().clone())
394 })
395 }
396
397 pub fn validate(
402 &self,
403 document: &Document,
404 workflow: &Workflow,
405 specified: Option<&HashSet<String>>,
406 ) -> Result<()> {
407 for (name, value) in &self.inputs {
409 let input = workflow
410 .inputs()
411 .get(name)
412 .with_context(|| format!("unknown input `{name}`"))?;
413 let expected_ty = input.ty();
414 let ty = value.ty();
415 if !ty.is_coercible_to(expected_ty) {
416 bail!("expected type `{expected_ty}` for input `{name}`, but found type `{ty}`");
417 }
418 }
419
420 for (name, input) in workflow.inputs() {
422 if input.required()
423 && !self.inputs.contains_key(name)
424 && specified.map(|s| !s.contains(name)).unwrap_or(true)
425 {
426 bail!(
427 "missing required input `{name}` to workflow `{workflow}`",
428 workflow = workflow.name()
429 );
430 }
431 }
432
433 if !self.calls.is_empty() && !workflow.allows_nested_inputs() {
435 bail!(
436 "cannot specify a nested call input for workflow `{name}` as it does not allow \
437 nested inputs",
438 name = workflow.name()
439 );
440 }
441
442 for (name, inputs) in &self.calls {
444 let call = workflow.calls().get(name).with_context(|| {
445 format!(
446 "workflow `{workflow}` does not have a call named `{name}`",
447 workflow = workflow.name()
448 )
449 })?;
450
451 let document = call
454 .namespace()
455 .map(|ns| {
456 document
457 .namespace(ns)
458 .expect("namespace should be present")
459 .document()
460 })
461 .unwrap_or(document);
462
463 let inputs = match call.kind() {
465 CallKind::Task => {
466 let task = document
467 .task_by_name(call.name())
468 .expect("task should be present");
469
470 let task_inputs = inputs.as_task_inputs().with_context(|| {
471 format!("`{name}` is a call to a task, but workflow inputs were supplied")
472 })?;
473
474 task_inputs.validate(document, task, Some(call.specified()))?;
475 &task_inputs.inputs
476 }
477 CallKind::Workflow => {
478 let workflow = document.workflow().expect("should have a workflow");
479 assert_eq!(
480 workflow.name(),
481 call.name(),
482 "call name does not match workflow name"
483 );
484 let workflow_inputs = inputs.as_workflow_inputs().with_context(|| {
485 format!("`{name}` is a call to a workflow, but task inputs were supplied")
486 })?;
487
488 workflow_inputs.validate(document, workflow, Some(call.specified()))?;
489 &workflow_inputs.inputs
490 }
491 };
492
493 for input in inputs.keys() {
494 if call.specified().contains(input) {
495 bail!(
496 "cannot specify nested input `{input}` for call `{call}` as it was \
497 explicitly specified in the call itself",
498 call = call.name(),
499 );
500 }
501 }
502 }
503
504 if workflow.allows_nested_inputs() {
506 for (call, ty) in workflow.calls() {
507 let inputs = self.calls.get(call);
508
509 for (input, _) in ty
510 .inputs()
511 .iter()
512 .filter(|(n, i)| i.required() && !ty.specified().contains(*n))
513 {
514 if !inputs.map(|i| i.get(input).is_some()).unwrap_or(false) {
515 bail!("missing required input `{input}` for call `{call}`");
516 }
517 }
518 }
519 }
520
521 Ok(())
522 }
523
524 fn set_path_value(
531 &mut self,
532 document: &Document,
533 workflow: &Workflow,
534 path: &str,
535 value: Value,
536 ) -> Result<()> {
537 match path.split_once('.') {
538 Some((name, remainder)) => {
539 if !workflow.allows_nested_inputs() {
541 bail!(
542 "cannot specify a nested call input for workflow `{workflow}` as it does \
543 not allow nested inputs",
544 workflow = workflow.name()
545 );
546 }
547
548 let call = workflow.calls().get(name).with_context(|| {
550 format!(
551 "workflow `{workflow}` does not have a call named `{name}`",
552 workflow = workflow.name()
553 )
554 })?;
555
556 let inputs =
558 self.calls
559 .entry(name.to_string())
560 .or_insert_with(|| match call.kind() {
561 CallKind::Task => Inputs::Task(Default::default()),
562 CallKind::Workflow => Inputs::Workflow(Default::default()),
563 });
564
565 let document = call
568 .namespace()
569 .map(|ns| {
570 document
571 .namespace(ns)
572 .expect("namespace should be present")
573 .document()
574 })
575 .unwrap_or(document);
576
577 let next = remainder
578 .split_once('.')
579 .map(|(n, _)| n)
580 .unwrap_or(remainder);
581 if call.specified().contains(next) {
582 bail!(
583 "cannot specify nested input `{next}` for call `{name}` as it was \
584 explicitly specified in the call itself",
585 );
586 }
587
588 match call.kind() {
590 CallKind::Task => {
591 let task = document
592 .task_by_name(call.name())
593 .expect("task should be present");
594 inputs
595 .as_task_inputs_mut()
596 .expect("should be a task input")
597 .set_path_value(document, task, remainder, value)
598 }
599 CallKind::Workflow => {
600 let workflow = document.workflow().expect("should have a workflow");
601 assert_eq!(
602 workflow.name(),
603 call.name(),
604 "call name does not match workflow name"
605 );
606 inputs
607 .as_workflow_inputs_mut()
608 .expect("should be a task input")
609 .set_path_value(document, workflow, remainder, value)
610 }
611 }
612 }
613 None => {
614 let input = workflow.inputs().get(path).with_context(|| {
615 format!(
616 "workflow `{workflow}` does not have an input named `{path}`",
617 workflow = workflow.name()
618 )
619 })?;
620
621 let expected = input.ty();
622 let actual = value.ty();
623 if let Some(expected_prim_ty) = expected.as_primitive()
624 && expected_prim_ty == PrimitiveType::String
625 && let Some(actual_prim_ty) = actual.as_primitive()
626 && actual_prim_ty != PrimitiveType::String
627 {
628 self.inputs
629 .insert(path.to_string(), value.to_string().into());
630 return Ok(());
631 }
632 if !actual.is_coercible_to(expected) {
633 bail!(
634 "expected type `{expected}` for input `{path}`, but found type `{actual}`"
635 );
636 }
637 self.inputs.insert(path.to_string(), value);
638 Ok(())
639 }
640 }
641 }
642}
643
644impl<S, V> FromIterator<(S, V)> for WorkflowInputs
645where
646 S: Into<String>,
647 V: Into<Value>,
648{
649 fn from_iter<T: IntoIterator<Item = (S, V)>>(iter: T) -> Self {
650 Self {
651 inputs: iter
652 .into_iter()
653 .map(|(k, v)| (k.into(), v.into()))
654 .collect(),
655 calls: Default::default(),
656 }
657 }
658}
659
660impl Serialize for WorkflowInputs {
661 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
662 where
663 S: serde::Serializer,
664 {
665 let mut map = serializer.serialize_map(Some(self.inputs.len()))?;
668 for (k, v) in &self.inputs {
669 let serialized_value = crate::ValueSerializer::new(v, true);
670 map.serialize_entry(k, &serialized_value)?;
671 }
672 map.end()
673 }
674}
675
676#[derive(Debug, Clone)]
678pub enum Inputs {
679 Task(TaskInputs),
681 Workflow(WorkflowInputs),
683}
684
685impl Inputs {
686 pub fn parse(document: &Document, path: impl AsRef<Path>) -> Result<Option<(String, Self)>> {
700 let path = path.as_ref();
701
702 match path.extension().and_then(|ext| ext.to_str()) {
703 Some("json") => Self::parse_json(document, path),
704 Some("yml") | Some("yaml") => Self::parse_yaml(document, path),
705 ext => bail!(
706 "unsupported file extension: `{ext}`; the supported formats are JSON (`.json`) \
707 and YAML (`.yaml` and `.yml`)",
708 ext = ext.unwrap_or("")
709 ),
710 }
711 .with_context(|| format!("failed to parse input file `{path}`", path = path.display()))
712 }
713
714 pub fn parse_json(
723 document: &Document,
724 path: impl AsRef<Path>,
725 ) -> Result<Option<(String, Self)>> {
726 let path = path.as_ref();
727
728 let file = File::open(path).with_context(|| {
729 format!("failed to open input file `{path}`", path = path.display())
730 })?;
731
732 let reader = BufReader::new(file);
734
735 let map = std::mem::take(
736 serde_json::from_reader::<_, JsonValue>(reader)?
737 .as_object_mut()
738 .with_context(|| {
739 format!(
740 "expected input file `{path}` to contain a JSON object",
741 path = path.display()
742 )
743 })?,
744 );
745
746 Self::parse_object(document, map)
747 }
748
749 pub fn parse_yaml(
758 document: &Document,
759 path: impl AsRef<Path>,
760 ) -> Result<Option<(String, Self)>> {
761 let path = path.as_ref();
762
763 let file = File::open(path).with_context(|| {
764 format!("failed to open input file `{path}`", path = path.display())
765 })?;
766
767 let reader = BufReader::new(file);
769 let yaml = serde_yaml_ng::from_reader::<_, YamlValue>(reader)?;
770
771 let mut json = serde_json::to_value(yaml).with_context(|| {
773 format!(
774 "failed to convert YAML to JSON for processing `{path}`",
775 path = path.display()
776 )
777 })?;
778
779 let object = std::mem::take(json.as_object_mut().with_context(|| {
780 format!(
781 "expected input file `{path}` to contain a YAML mapping",
782 path = path.display()
783 )
784 })?);
785
786 Self::parse_object(document, object)
787 }
788
789 pub fn get(&self, name: &str) -> Option<&Value> {
791 match self {
792 Self::Task(t) => t.inputs.get(name),
793 Self::Workflow(w) => w.inputs.get(name),
794 }
795 }
796
797 pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) -> Option<Value> {
801 match self {
802 Self::Task(inputs) => inputs.set(name, value),
803 Self::Workflow(inputs) => inputs.set(name, value),
804 }
805 }
806
807 pub fn as_task_inputs(&self) -> Option<&TaskInputs> {
811 match self {
812 Self::Task(inputs) => Some(inputs),
813 Self::Workflow(_) => None,
814 }
815 }
816
817 pub fn as_task_inputs_mut(&mut self) -> Option<&mut TaskInputs> {
821 match self {
822 Self::Task(inputs) => Some(inputs),
823 Self::Workflow(_) => None,
824 }
825 }
826
827 pub fn unwrap_task_inputs(self) -> TaskInputs {
833 match self {
834 Self::Task(inputs) => inputs,
835 Self::Workflow(_) => panic!("inputs are for a workflow"),
836 }
837 }
838
839 pub fn as_workflow_inputs(&self) -> Option<&WorkflowInputs> {
843 match self {
844 Self::Task(_) => None,
845 Self::Workflow(inputs) => Some(inputs),
846 }
847 }
848
849 pub fn as_workflow_inputs_mut(&mut self) -> Option<&mut WorkflowInputs> {
853 match self {
854 Self::Task(_) => None,
855 Self::Workflow(inputs) => Some(inputs),
856 }
857 }
858
859 pub fn unwrap_workflow_inputs(self) -> WorkflowInputs {
865 match self {
866 Self::Task(_) => panic!("inputs are for a task"),
867 Self::Workflow(inputs) => inputs,
868 }
869 }
870
871 pub fn parse_object(document: &Document, object: JsonMap) -> Result<Option<(String, Self)>> {
877 let (key, name) = match object.iter().next() {
879 Some((key, _)) => match key.split_once('.') {
880 Some((name, _remainder)) => (key, name),
881 None => {
882 bail!(
883 "invalid input key `{key}`: expected the value to be prefixed with the \
884 workflow or task name",
885 )
886 }
887 },
888 None => {
890 return Ok(None);
891 }
892 };
893
894 match (document.task_by_name(name), document.workflow()) {
895 (Some(task), _) => Ok(Some(Self::parse_task_inputs(document, task, object)?)),
896 (None, Some(workflow)) if workflow.name() == name => Ok(Some(
897 Self::parse_workflow_inputs(document, workflow, object)?,
898 )),
899 _ => bail!(
900 "invalid input key `{key}`: a task or workflow named `{name}` does not exist in \
901 the document"
902 ),
903 }
904 }
905
906 fn parse_task_inputs(
908 document: &Document,
909 task: &Task,
910 object: JsonMap,
911 ) -> Result<(String, Self)> {
912 let mut inputs = TaskInputs::default();
913 for (key, value) in object {
914 let value = serde_json::from_value(value)
916 .with_context(|| format!("invalid input key `{key}`"))?;
917
918 match key.split_once(".") {
919 Some((prefix, remainder)) if prefix == task.name() => {
920 inputs
921 .set_path_value(document, task, remainder, value)
922 .with_context(|| format!("invalid input key `{key}`"))?;
923 }
924 _ => {
925 bail!(
926 "invalid input key `{key}`: expected key to be prefixed with `{task}`",
927 task = task.name()
928 );
929 }
930 }
931 }
932
933 Ok((task.name().to_string(), Inputs::Task(inputs)))
934 }
935
936 fn parse_workflow_inputs(
938 document: &Document,
939 workflow: &Workflow,
940 object: JsonMap,
941 ) -> Result<(String, Self)> {
942 let mut inputs = WorkflowInputs::default();
943 for (key, value) in object {
944 let value = serde_json::from_value(value)
946 .with_context(|| format!("invalid input key `{key}`"))?;
947
948 match key.split_once(".") {
949 Some((prefix, remainder)) if prefix == workflow.name() => {
950 inputs
951 .set_path_value(document, workflow, remainder, value)
952 .with_context(|| format!("invalid input key `{key}`"))?;
953 }
954 _ => {
955 bail!(
956 "invalid input key `{key}`: expected key to be prefixed with `{workflow}`",
957 workflow = workflow.name()
958 );
959 }
960 }
961 }
962
963 Ok((workflow.name().to_string(), Inputs::Workflow(inputs)))
964 }
965}
966
967impl From<TaskInputs> for Inputs {
968 fn from(inputs: TaskInputs) -> Self {
969 Self::Task(inputs)
970 }
971}
972
973impl From<WorkflowInputs> for Inputs {
974 fn from(inputs: WorkflowInputs) -> Self {
975 Self::Workflow(inputs)
976 }
977}