[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())