[yaml] Add yaml recipe module.

This change adds yaml recipe module.

Change-Id: I9b10e9804060ff64cb1348eef63d2d31fa5617e9
diff --git a/recipe_modules/yaml/__init__.py b/recipe_modules/yaml/__init__.py
new file mode 100644
index 0000000..1bef2e1
--- /dev/null
+++ b/recipe_modules/yaml/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+DEPS = [
+    'recipe_engine/context',
+    'recipe_engine/file',
+    'recipe_engine/json',
+    'recipe_engine/path',
+    'recipe_engine/platform',
+    'recipe_engine/python',
+    'recipe_engine/raw_io',
+    'recipe_engine/step',
+]
+
+from recipe_engine.recipe_api import Property
+from recipe_engine.config import ConfigGroup, Single
diff --git a/recipe_modules/yaml/api.py b/recipe_modules/yaml/api.py
new file mode 100644
index 0000000..dcd49ed
--- /dev/null
+++ b/recipe_modules/yaml/api.py
@@ -0,0 +1,53 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from recipe_engine import recipe_api
+
+
+class YamlApi(recipe_api.RecipeApi):
+  """Provides functions to parse YAML files."""
+
+  def parse_yaml(self, file_path):
+    """parse a yaml file and return its content as a JSON object.
+
+    E.g. api.yaml.parse_yaml(file_path)
+
+        will read yaml file from file_path and converted into a JSON
+        object.
+
+    Args:
+      * file_path (Path) - The path to the dockerfile.
+    """
+    return self.m.python(
+        'load yaml %s' % file_path,
+        self.resource('parse_yaml.py'),
+        args=[file_path],
+        stdout=self.m.json.output()).stdout
+
+  def _traverse_json(self, json_data, field):
+    if isinstance(json_data, dict):
+      if field in json_data:
+        return json_data[field]
+      for key in json_data:
+        return_val = self._traverse_json(json_data[key], field)
+        if return_val != None:
+          return return_val
+    if isinstance(json_data, list):
+      for item in json_data:
+        return_val = self._traverse_json(item, field)
+        if return_val != None:
+          return return_val
+    return None
+
+  def retrieve_field(self, file_path, field_name):
+    """retrieve a field from a YAML file and return the first found value
+    if it exists. Otherwise return None.
+
+    E.g. api.yaml.retrieve_field(file_path, 'region')
+
+    Args:
+      * file_path (Path) - The path to the YAML file.
+      * field_name (str) - The name of the field.
+    """
+    return self._traverse_json(self.parse_yaml(file_path), field_name)
diff --git a/recipe_modules/yaml/examples/full.expected/example.json b/recipe_modules/yaml/examples/full.expected/example.json
new file mode 100644
index 0000000..21d42b9
--- /dev/null
+++ b/recipe_modules/yaml/examples/full.expected/example.json
@@ -0,0 +1,47 @@
+[
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[fuchsia::yaml]/resources/parse_yaml.py",
+      "[CLEANUP]/test.yaml"
+    ],
+    "name": "load yaml [CLEANUP]/test.yaml",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@json.output (invalid)@@@",
+      "@@@STEP_LOG_LINE@json.output (exception)@No JSON object could be decoded@@@",
+      "@@@STEP_LOG_END@json.output (exception)@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[fuchsia::yaml]/resources/parse_yaml.py",
+      "[CLEANUP]/test.yaml"
+    ],
+    "name": "load yaml [CLEANUP]/test.yaml (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@json.output (invalid)@@@",
+      "@@@STEP_LOG_LINE@json.output (exception)@No JSON object could be decoded@@@",
+      "@@@STEP_LOG_END@json.output (exception)@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[fuchsia::yaml]/resources/parse_yaml.py",
+      "[CLEANUP]/test2.yaml"
+    ],
+    "name": "load yaml [CLEANUP]/test2.yaml",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@json.output (invalid)@@@",
+      "@@@STEP_LOG_LINE@json.output (exception)@No JSON object could be decoded@@@",
+      "@@@STEP_LOG_END@json.output (exception)@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/yaml/examples/full.expected/test retrieve field.json b/recipe_modules/yaml/examples/full.expected/test retrieve field.json
new file mode 100644
index 0000000..2dc4b37
--- /dev/null
+++ b/recipe_modules/yaml/examples/full.expected/test retrieve field.json
@@ -0,0 +1,48 @@
+[
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[fuchsia::yaml]/resources/parse_yaml.py",
+      "[CLEANUP]/test.yaml"
+    ],
+    "name": "load yaml [CLEANUP]/test.yaml",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@json.output (invalid)@@@",
+      "@@@STEP_LOG_LINE@json.output (exception)@No JSON object could be decoded@@@",
+      "@@@STEP_LOG_END@json.output (exception)@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[fuchsia::yaml]/resources/parse_yaml.py",
+      "[CLEANUP]/test.yaml"
+    ],
+    "name": "load yaml [CLEANUP]/test.yaml (2)",
+    "~followup_annotations": [
+      "@@@STEP_LOG_END@json.output (invalid)@@@",
+      "@@@STEP_LOG_LINE@json.output (exception)@No JSON object could be decoded@@@",
+      "@@@STEP_LOG_END@json.output (exception)@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "python",
+      "-u",
+      "RECIPE_MODULE[fuchsia::yaml]/resources/parse_yaml.py",
+      "[CLEANUP]/test2.yaml"
+    ],
+    "name": "load yaml [CLEANUP]/test2.yaml",
+    "~followup_annotations": [
+      "@@@STEP_LOG_LINE@json.output@{@@@",
+      "@@@STEP_LOG_LINE@json.output@  \"a\": \"b\"@@@",
+      "@@@STEP_LOG_LINE@json.output@}@@@",
+      "@@@STEP_LOG_END@json.output@@@"
+    ]
+  },
+  {
+    "name": "$result"
+  }
+]
\ No newline at end of file
diff --git a/recipe_modules/yaml/examples/full.py b/recipe_modules/yaml/examples/full.py
new file mode 100644
index 0000000..5513a50
--- /dev/null
+++ b/recipe_modules/yaml/examples/full.py
@@ -0,0 +1,29 @@
+# Copyright 2018 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from recipe_engine.post_process import DoesNotRun, Filter, StatusFailure
+
+DEPS = [
+    'recipe_engine/context',
+    'recipe_engine/json',
+    'recipe_engine/path',
+    'recipe_engine/raw_io',
+    'recipe_engine/step',
+    'yaml',
+]
+
+
+def RunSteps(api):
+  api.yaml.parse_yaml(api.path['cleanup'].join('test.yaml'))
+  api.yaml._traverse_json(['a', 'b', {'c': 'd'}], 'c')
+  api.yaml._traverse_json({'a': 'b'}, 'a')
+  api.yaml._traverse_json({'a': 'b', 'e': {'c': 'd'}}, 'c')
+  api.yaml.retrieve_field(api.path['cleanup'].join('test.yaml'), 'a')
+  api.yaml.retrieve_field(api.path['cleanup'].join('test2.yaml'), 'a')
+
+
+def GenTests(api):
+  yield api.test('example')
+  yield api.test('test retrieve field') + api.step_data(
+      'load yaml [CLEANUP]/test2.yaml', stdout=api.json.output({'a': 'b'}))
diff --git a/recipe_modules/yaml/resources/parse_yaml.py b/recipe_modules/yaml/resources/parse_yaml.py
new file mode 100644
index 0000000..f451c2e
--- /dev/null
+++ b/recipe_modules/yaml/resources/parse_yaml.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# [VPYTHON:BEGIN]
+# python_version: "2.7"
+# wheel <
+#   name: "infra/python/wheels/pyyaml/${platform}_${py_python}_${py_abi}"
+#   version: "version:3.12"
+# >
+# [VPYTHON:END]
+
+import argparse
+import json
+import sys
+import yaml
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument('file')
+  args = parser.parse_args()
+
+  if args.file == '-':
+    print json.dumps(yaml.load(sys.stdin))
+  else:
+    with open(args.file) as f:
+      print json.dumps(yaml.load(f))
+
+
+if __name__ == '__main__':
+  sys.exit(main())