From 9b785dee9fa06e6f6efba4d1ae8f34505d0c5c3e Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Wed, 5 Jun 2013 09:37:19 +1000 Subject: [PATCH] Add Fn::Select template function It also knows how to lookup keys in dict's too. The strings can be a serialized json blob too. blueprint fn-select blueprint fn-mapselect Change-Id: Ie40afa47c1f0a699abb852bc22388f3f675172e6 --- heat/engine/parser.py | 1 + heat/engine/template.py | 45 +++++++++++++++++++++++++++++++ heat/tests/test_parser.py | 57 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/heat/engine/parser.py b/heat/engine/parser.py index d0dabaff..6902443b 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -573,6 +573,7 @@ def resolve_runtime_data(template, resources, snippet): resources=resources), functools.partial(template.resolve_attributes, resources=resources), + template.resolve_select, template.resolve_joins, template.resolve_base64]) diff --git a/heat/engine/template.py b/heat/engine/template.py index 3bd77f31..369dcb42 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -14,6 +14,7 @@ # under the License. import collections +import json from heat.db import api as db_api from heat.common import exception @@ -198,6 +199,50 @@ class Template(collections.Mapping): return _resolve(lambda k, v: k == 'Fn::Join', handle_join, s) + @staticmethod + def resolve_select(s): + ''' + Resolve constructs of the form: + (for a list lookup) + { "Fn::Select" : [ "2", [ "apples", "grapes", "mangoes" ] ] } + returns "mangoes" + + (for a dict lookup) + { "Fn::Select" : [ "red", {"red": "a", "flu": "b"} ] } + returns "a" + + Note: can raise IndexError, KeyError, ValueError and TypeError + ''' + def handle_select(args): + if not isinstance(args, (list, tuple)): + raise TypeError('Arguments to "Fn::Select" must be a list') + + try: + lookup, strings = args + except ValueError as ex: + example = '"Fn::Select" : [ "4", [ "str1", "str2"]]' + raise ValueError('Incorrect arguments to "Fn::Select" %s: %s' % + ('should be', example)) + + try: + index = int(lookup) + except ValueError as ex: + index = lookup + + if isinstance(strings, basestring): + # might be serialized json. + # if not allow it to raise a ValueError + strings = json.loads(strings) + + if isinstance(strings, (list, tuple)) and isinstance(index, int): + return strings[index] + if isinstance(strings, dict) and isinstance(index, basestring): + return strings[index] + + raise TypeError('Arguments to "Fn::Select" not fully resolved') + + return _resolve(lambda k, v: k == 'Fn::Select', handle_select, s) + @staticmethod def resolve_joins(s): ''' diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 5baa4c0f..8bed4460 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -12,8 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import json import mox - import time import uuid @@ -204,6 +204,61 @@ class TemplateTest(HeatTestCase): resources), p_snippet) + def test_select_from_list(self): + data = {"Fn::Select": ["1", ["foo", "bar"]]} + self.assertEqual(parser.Template.resolve_select(data), "bar") + + def test_select_from_list_not_int(self): + data = {"Fn::Select": ["one", ["foo", "bar"]]} + self.assertRaises(TypeError, parser.Template.resolve_select, + data) + + def test_select_from_list_out_of_bound(self): + data = {"Fn::Select": ["3", ["foo", "bar"]]} + self.assertRaises(IndexError, parser.Template.resolve_select, + data) + + def test_select_from_dict(self): + data = {"Fn::Select": ["red", {"red": "robin", "re": "foo"}]} + self.assertEqual(parser.Template.resolve_select(data), "robin") + + def test_select_from_dict_not_str(self): + data = {"Fn::Select": ["1", {"red": "robin", "re": "foo"}]} + self.assertRaises(TypeError, parser.Template.resolve_select, + data) + + def test_select_from_dict_not_existing(self): + data = {"Fn::Select": ["green", {"red": "robin", "re": "foo"}]} + self.assertRaises(KeyError, parser.Template.resolve_select, + data) + + def test_select_from_serialized_json_map(self): + js = json.dumps({"red": "robin", "re": "foo"}) + data = {"Fn::Select": ["re", js]} + self.assertEqual(parser.Template.resolve_select(data), "foo") + + def test_select_from_serialized_json_list(self): + js = json.dumps(["foo", "fee", "fum"]) + data = {"Fn::Select": ["0", js]} + self.assertEqual(parser.Template.resolve_select(data), "foo") + + def test_select_from_serialized_json_wrong(self): + js = "this is really not serialized json" + data = {"Fn::Select": ["not", js]} + self.assertRaises(ValueError, parser.Template.resolve_select, + data) + + def test_select_wrong_num_args(self): + join0 = {"Fn::Select": []} + self.assertRaises(ValueError, parser.Template.resolve_select, + join0) + join1 = {"Fn::Select": ["4"]} + self.assertRaises(ValueError, parser.Template.resolve_select, + join1) + join3 = {"Fn::Select": ["foo", {"foo": "bar"}, ""]} + self.assertRaises(ValueError, parser.Template.resolve_select, + join3) + def test_join_reduce(self): join = {"Fn::Join": [" ", ["foo", "bar", "baz", {'Ref': 'baz'}, "bink", "bonk"]]} -- 2.45.2