Repository: DataBiosphere/toil

Issue:
as_map() implemented for WDL.
Description of WDL function to be added:

```
## Map[X,Y] as_map(Array[Pair[X,Y]])

Given an Array consisting of Pairs, the `as_map` function returns a Map in which the left elements of the Pairs are the keys and the right elements the values. The left element of the Pairs passed to `as_map` must be a primitive type. The order of the key/value pairs in the resulting Map is the same as the order of the Pairs in the Array.

In cases where multiple Pairs would produce the same key, the workflow will fail.
```
https://github.com/openwdl/wdl/blob/main/versions/development/SPEC.md#mapxy-as_maparraypairxy

 - [ ] Add the following WDL function to https://github.com/DataBiosphere/toil/blob/master/src/toil/wdl/wdl_functions.py
 - [ ] Add a test that runs a wdl/json with toil and then checks against cromwell to verify that toil and cromwell have the same behavior
       See: https://github.com/DailyDreaming/wdl-conformance-tests/tree/master/tests/standard_library

For example:

```wdl
Array[Pair[String,Int]] x = [("a", 1), ("b", 2), ("c", 3)]
Array[Pair[String,Pair[File,File]]] y = [("a", ("a.bam", "a.bai")), ("b", ("b.bam", "b.bai"))]

Map[String,Int] xmap = as_map(x) # {"a": 1, "b": 2, "c": 3}
Map[String,Pair[File,File]] ymap = as_map(y) # {"a": ("a.bam", "a.bai"), "b": ("b.bam", "b.bai")}
```

┆Issue is synchronized with this [Jira Task](https://ucsc-cgl.atlassian.net/browse/TOIL-641)
┆Epic: WDL Standard Library Implementation
┆Issue Number: TOIL-641



Discussion:

Resolving patch:
diff --git a/src/toil/wdl/wdl_functions.py b/src/toil/wdl/wdl_functions.py
--- a/src/toil/wdl/wdl_functions.py
+++ b/src/toil/wdl/wdl_functions.py
@@ -946,3 +946,25 @@ def cross(left: List[Any], right: List[Any]) -> List[WDLPair]:
         raise WDLRuntimeError(f'cross() requires both inputs to be Array[]!  Not: {type(left)} and {type(right)}')
 
     return list(WDLPair(left=left_val, right=right_val) for left_val in left for right_val in right)
+
+
+def as_map(in_array: List[WDLPair]) -> dict:
+    """
+    Given an Array consisting of Pairs, the `as_map` function returns a Map in
+    which the left elements of the Pairs are the keys and the right elements the
+    values.
+
+    WDL syntax: Map[X,Y] as_map(Array[Pair[X,Y]])
+    """
+    if not isinstance(in_array, list):
+        raise WDLRuntimeError(f'as_map() requires "{in_array}" to be a list!  Not: {type(in_array)}')
+
+    map = {}
+
+    for pair in in_array:
+        if map.get(pair.left):
+            raise WDLRuntimeError('Cannot evaluate "as_map()" with duplicated keys.')
+
+        map[pair.left] = pair.right
+
+    return map
diff --git a/src/toil/wdl/wdl_synthesis.py b/src/toil/wdl/wdl_synthesis.py
--- a/src/toil/wdl/wdl_synthesis.py
+++ b/src/toil/wdl/wdl_synthesis.py
@@ -142,6 +142,7 @@ def write_modules(self):
                     from toil.wdl.wdl_functions import length
                     from toil.wdl.wdl_functions import wdl_zip
                     from toil.wdl.wdl_functions import cross
+                    from toil.wdl.wdl_functions import as_map
                     import fnmatch
                     import textwrap
                     import subprocess


Test patch:
diff --git a/src/toil/test/wdl/builtinTest.py b/src/toil/test/wdl/builtinTest.py
--- a/src/toil/test/wdl/builtinTest.py
+++ b/src/toil/test/wdl/builtinTest.py
@@ -484,6 +484,9 @@ def test_cross(self):
                                             '{"left":2,"right":"a"},{"left":2,"right":"b"},'
                                             '{"left":3,"right":"a"},{"left":3,"right":"b"}]')
 
+    def test_as_map(self):
+        self.check_function('as_map', cases=['as_input'], expected_result='{"a":1,"b":2,"c":3}')
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/src/toil/test/wdl/standard_library/as_map.json b/src/toil/test/wdl/standard_library/as_map.json
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/as_map.json
@@ -0,0 +1,16 @@
+{
+  "asMapWorkflow.in_array": [
+    {
+      "left": "a",
+      "right": 1
+    },
+    {
+      "left": "b",
+      "right": 2
+    },
+    {
+      "left": "c",
+      "right": 3
+    }
+  ]
+}
diff --git a/src/toil/test/wdl/standard_library/as_map_as_input.wdl b/src/toil/test/wdl/standard_library/as_map_as_input.wdl
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/as_map_as_input.wdl
@@ -0,0 +1,23 @@
+version development
+
+workflow asMapWorkflow {
+  input {
+    Array[Pair[String, Int]] in_array
+  }
+
+  call copy_output {input: in_map=as_map(in_array)}
+}
+
+task copy_output {
+  input {
+    Map[String, Int] in_map
+  }
+
+  command {
+    cp ~{write_json(in_map)} output.txt
+  }
+
+  output {
+    File the_output = 'output.txt'
+  }
+}