Repository: DataBiosphere/toil

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

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

Given a Map, the `as_pairs` function returns an Array containing each element in the form of a Pair. The key will be the left element of the Pair and the value the right element. The order of the the Pairs in the resulting Array is the same as the order of the key/value pairs in the Map.
```
https://github.com/openwdl/wdl/blob/main/versions/development/SPEC.md#arraypairxy-as_pairsmapxy

 - [ ] 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
Map[String, Int] x = {"a": 1, "b": 2, "c": 3}
Map[String,Pair[File,File]] y = {"a": ("a.bam", "a.bai"), "b": ("b.bam", "b.bai")}

Array[Pair[String,Int]] xpairs = as_pairs(x) # [("a", 1), ("b", 2), ("c", 3)]
Array[Pair[String,Pair[File,File]]] ypairs = as_pairs(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-640)
┆Epic: WDL Standard Library Implementation
┆Issue Number: TOIL-640



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
@@ -948,6 +948,21 @@ def cross(left: List[Any], right: List[Any]) -> List[WDLPair]:
     return list(WDLPair(left=left_val, right=right_val) for left_val in left for right_val in right)
 
 
+def as_pairs(in_map: dict) -> List[WDLPair]:
+    """
+    Given a Map, the `as_pairs` function returns an Array containing each element
+    in the form of a Pair. The key will be the left element of the Pair and the
+    value the right element. The order of the the Pairs in the resulting Array
+    is the same as the order of the key/value pairs in the Map.
+
+    WDL syntax: Array[Pair[X,Y]] as_pairs(Map[X,Y])
+    """
+    if not isinstance(in_map, dict):
+        raise WDLRuntimeError(f'as_pairs() requires "{in_map}" to be Map[]!  Not: {type(in_map)}')
+
+    return list(WDLPair(left=k, right=v) for k, v in in_map.items())
+
+  
 def as_map(in_array: List[WDLPair]) -> dict:
     """
     Given an Array consisting of Pairs, the `as_map` function returns a Map in
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_pairs
                     from toil.wdl.wdl_functions import as_map
                     import fnmatch
                     import textwrap


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,10 @@ def test_cross(self):
                                             '{"left":2,"right":"a"},{"left":2,"right":"b"},'
                                             '{"left":3,"right":"a"},{"left":3,"right":"b"}]')
 
+    def test_as_pairs(self):
+        self.check_function('as_pairs', cases=['as_input'],
+                            expected_result='[{"left":"a","right":1},{"left":"b","right":2},{"left":"c","right":3}]')
+
     def test_as_map(self):
         self.check_function('as_map', cases=['as_input'], expected_result='{"a":1,"b":2,"c":3}')
 
diff --git a/src/toil/test/wdl/standard_library/as_pairs.json b/src/toil/test/wdl/standard_library/as_pairs.json
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/as_pairs.json
@@ -0,0 +1,7 @@
+{
+  "asPairsWorkflow.in_map": {
+    "a": 1,
+    "b": 2,
+    "c": 3
+  }
+}
diff --git a/src/toil/test/wdl/standard_library/as_pairs_as_input.wdl b/src/toil/test/wdl/standard_library/as_pairs_as_input.wdl
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/as_pairs_as_input.wdl
@@ -0,0 +1,23 @@
+version development
+
+workflow asPairsWorkflow {
+  input {
+    Map[String, Int] in_map
+  }
+
+  call copy_output {input: in_pairs=as_pairs(in_map)}
+}
+
+task copy_output {
+  input {
+    Array[Pair[String, Int]] in_pairs
+  }
+
+  command {
+    cp ~{write_json(in_pairs)} output.txt
+  }
+
+  output {
+    File the_output = 'output.txt'
+  }
+}