Implement map_replace intrinsic function

This adds a new map_replace function that can iterate over a map
(e.g json parameter) doing key/value replacements.

Change-Id: I29f0e438c398fda715c79727ed5de8383e5b5d7b
Implements-Blueprint: blueprint map-replace-function
This commit is contained in:
Steven Hardy 2016-07-18 16:22:40 +01:00
parent f9b47c61c3
commit b862945889
4 changed files with 189 additions and 2 deletions

View File

@ -210,8 +210,9 @@ for the ``heat_template_version`` key:
document is a HOT template and it may contain features added and/or removed
up until the Newton release. This version adds the ``yaql`` function which
can be used for evaluation of complex expressions, and also adds ``equals``
function which can be used to compare whether two values are equal. The
complete list of supported functions is::
function which can be used to compare whether two values are equal, and
the ``map_replace`` function that can do key/value replacements on a mapping.
The complete list of supported functions is::
digest
get_attr
@ -220,6 +221,7 @@ for the ``heat_template_version`` key:
get_resource
list_join
map_merge
map_replace
repeat
resource_facade
str_replace
@ -1292,6 +1294,41 @@ This resolves to a map containing ``{'k1': 'v2', 'k2': 'v2'}``.
Maps containing no items resolve to {}.
map_replace
-----------
The ``map_replace`` function does key/value replacements on an existing mapping.
An input mapping is processed by iterating over all keys/values and performing
a replacement if an exact match is found in either of the optional keys/values
mappings.
The syntax of the ``map_replace`` function is
.. code-block:: yaml
map_replace:
- <input map>
- keys: <map of key replacements>
values: <map of value replacements>
For example
.. code-block:: yaml
map_replace:
- k1: v1
k2: v2
- keys:
k1: K1
values:
v2: V2
This resolves to a map containing ``{'K1': 'v1', 'k2': 'V2'}``.
The keys/values mappings are optional, either or both may be specified.
Note that an error is raised if a replacement defined in "keys" results
in a collision with an existing keys in the input map.
yaql
----
The ``yaql`` evaluates yaql expression on a given data.

View File

@ -492,6 +492,78 @@ class MapMerge(function.Function):
return ret_map
class MapReplace(function.Function):
"""A function for performing substitutions on maps.
Takes the form::
{"map_replace" : [{'k1': 'v1', 'k2': 'v2'},
{'keys': {'k1': 'K1'},
'values': {'v2': 'V2'}}]}
And resolves to::
{'K1': 'v1', 'k2': 'V2'}
"""
def __init__(self, stack, fn_name, args):
super(MapReplace, self).__init__(stack, fn_name, args)
example = (_('"%s" : [ { "key1": "val1" }, '
'{"keys": {"key1": "key2"}, "values": {"val1": "val2"}}]')
% fn_name)
self.fmt_data = {'fn_name': fn_name, 'example': example}
def result(self):
args = function.resolve(self.args)
def ensure_map(m):
if m is None:
return {}
elif isinstance(m, collections.Mapping):
return m
else:
msg = (_('Incorrect arguments: to "%(fn_name)s", arguments '
'must be a list of maps. Example: %(example)s')
% self.fmt_data)
raise TypeError(msg)
try:
in_map = ensure_map(args.pop(0))
repl_map = ensure_map(args.pop(0))
if args != []:
raise IndexError
except (IndexError, AttributeError):
raise TypeError(_('Incorrect arguments to "%(fn_name)s" '
'should be: %(example)s') % self.fmt_data)
for k in repl_map:
if k not in ('keys', 'values'):
raise ValueError(_('Incorrect arguments to "%(fn_name)s" '
'should be: %(example)s') % self.fmt_data)
repl_keys = repl_map.get('keys', {})
repl_values = repl_map.get('values', {})
ret_map = {}
for k, v in six.iteritems(in_map):
key = repl_keys.get(k)
if key is None:
key = k
elif key in in_map:
# Keys collide
msg = _('key replacement %s collides with '
'a key in the input map')
raise ValueError(msg % key)
elif key in ret_map:
# Keys collide
msg = _('key replacement %s collides with '
'a key in the output map')
raise ValueError(msg % key)
value = repl_values.get(v, v)
ret_map[key] = value
return ret_map
class ResourceFacade(cfn_funcs.ResourceFacade):
"""A function for retrieving data in a parent provider template.

View File

@ -427,6 +427,7 @@ class HOTemplate20161014(HOTemplate20160408):
# functions added in 2016-10-14
'yaql': hot_funcs.Yaql,
'equals': cfn_funcs.Equals,
'map_replace': hot_funcs.MapReplace,
# functions removed from 2015-10-15
'Fn::Select': hot_funcs.Removed,

View File

@ -901,6 +901,83 @@ class HOTemplateTest(common.HeatTestCase):
self.assertEqual('role1', resolved['role1'])
self.assertEqual('role2', resolved['role2'])
def test_map_replace(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'},
{'keys': {'f1': 'F1'},
'values': {'b2': 'B2'}}]}
tmpl = template.Template(hot_newton_tpl_empty)
resolved = self.resolve(snippet, tmpl)
self.assertEqual({'F1': 'b1', 'f2': 'B2'},
resolved)
def test_map_replace_nokeys(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'},
{'values': {'b2': 'B2'}}]}
tmpl = template.Template(hot_newton_tpl_empty)
resolved = self.resolve(snippet, tmpl)
self.assertEqual({'f1': 'b1', 'f2': 'B2'},
resolved)
def test_map_replace_novalues(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'},
{'keys': {'f2': 'F2'}}]}
tmpl = template.Template(hot_newton_tpl_empty)
resolved = self.resolve(snippet, tmpl)
self.assertEqual({'f1': 'b1', 'F2': 'b2'},
resolved)
def test_map_replace_keys_collide(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'},
{'keys': {'f2': 'f1'}}]}
tmpl = template.Template(hot_newton_tpl_empty)
msg = "key replacement f1 collides with a key in the input map"
self.assertRaisesRegexp(ValueError, msg, self.resolve, snippet, tmpl)
def test_map_replace_replaced_keys_collide(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'},
{'keys': {'f1': 'f3', 'f2': 'f3'}}]}
tmpl = template.Template(hot_newton_tpl_empty)
msg = "key replacement f3 collides with a key in the output map"
self.assertRaisesRegexp(ValueError, msg, self.resolve, snippet, tmpl)
def test_map_replace_invalid_str_arg1(self):
snippet = {'map_replace': 'ab'}
tmpl = template.Template(hot_newton_tpl_empty)
msg = "Incorrect arguments to \"map_replace\" should be:"
self.assertRaisesRegexp(TypeError, msg, self.resolve, snippet, tmpl)
def test_map_replace_invalid_str_arg2(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'}, "ab"]}
tmpl = template.Template(hot_newton_tpl_empty)
msg = ("Incorrect arguments: to \"map_replace\", "
"arguments must be a list of maps")
self.assertRaisesRegexp(TypeError, msg, self.resolve, snippet, tmpl)
def test_map_replace_invalid_empty(self):
snippet = {'map_replace': []}
tmpl = template.Template(hot_newton_tpl_empty)
msg = "Incorrect arguments to \"map_replace\" should be:"
self.assertRaisesRegexp(TypeError, msg, self.resolve, snippet, tmpl)
def test_map_replace_invalid_missing1(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'}]}
tmpl = template.Template(hot_newton_tpl_empty)
msg = "Incorrect arguments to \"map_replace\" should be:"
self.assertRaisesRegexp(TypeError, msg, self.resolve, snippet, tmpl)
def test_map_replace_invalid_missing2(self):
snippet = {'map_replace': [{'keys': {'f1': 'f3', 'f2': 'f3'}}]}
tmpl = template.Template(hot_newton_tpl_empty)
msg = "Incorrect arguments to \"map_replace\" should be:"
self.assertRaisesRegexp(TypeError, msg, self.resolve, snippet, tmpl)
def test_map_replace_invalid_wrongkey(self):
snippet = {'map_replace': [{'f1': 'b1', 'f2': 'b2'},
{'notkeys': {'f2': 'F2'}}]}
tmpl = template.Template(hot_newton_tpl_empty)
msg = "Incorrect arguments to \"map_replace\" should be:"
self.assertRaisesRegexp(ValueError, msg, self.resolve, snippet, tmpl)
def test_yaql(self):
snippet = {'yaql': {'expression': '$.data.var1.sum()',
'data': {'var1': [1, 2, 3, 4]}}}