Repository: DataBiosphere/toil

Issue:
keys() implemented as WDL.
Description of WDL function to be added:

```
## Array[X] keys(Map[X,Y])

Given a Map, the `keys` function returns an Array consisting of the keys in the Map. The order of the keys in the resulting Array is the same as the order of the Pairs in the Map.

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#arrayx-keysmapxy

 - [ ] 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[String] xmap = keys(x) # ["a", "b", "c"]
Array[String] ymap = keys(y) # ["a", "b"]
```

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



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
@@ -945,3 +945,15 @@ def as_map(in_array: List[WDLPair]) -> dict:
         map[pair.left] = pair.right
 
     return map
+
+
+def keys(in_map: dict) -> list:
+    """
+    Given a Map, the `keys` function returns an Array consisting of the keys in
+    the Map. The order of the keys in the resulting Array is the same as the
+    order of the Pairs in the Map.
+
+    WDL syntax: Array[X] keys(Map[X,Y])
+    """
+
+    return list(in_map.keys())
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
@@ -144,6 +144,7 @@ def write_modules(self):
                     from toil.wdl.wdl_functions import cross
                     from toil.wdl.wdl_functions import as_pairs
                     from toil.wdl.wdl_functions import as_map
+                    from toil.wdl.wdl_functions import keys
                     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
@@ -491,6 +491,9 @@ def test_as_pairs(self):
     def test_as_map(self):
         self.check_function('as_map', cases=['as_input'], expected_result='{"a":1,"b":2,"c":3}')
 
+    def test_keys(self):
+        self.check_function('keys', cases=['as_input'], expected_result='["a","b","c"]')
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/src/toil/test/wdl/standard_library/keys.json b/src/toil/test/wdl/standard_library/keys.json
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/keys.json
@@ -0,0 +1,8 @@
+{
+  "keysWorkflow.in_map": {
+    "a": 1,
+    "b": 2,
+    "c": 3
+  }
+}
+
diff --git a/src/toil/test/wdl/standard_library/keys_as_input.wdl b/src/toil/test/wdl/standard_library/keys_as_input.wdl
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/keys_as_input.wdl
@@ -0,0 +1,24 @@
+version development
+
+workflow keysWorkflow {
+  input {
+    Map[String, Int] in_map
+  }
+
+  call copy_output {input: in_array=keys(in_map)}
+}
+
+task copy_output {
+  input {
+    Array[String] in_array
+  }
+
+  command {
+    cp ~{write_json(in_array)} output.txt
+  }
+
+  output {
+    File the_output = 'output.txt'
+  }
+}
+