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:
parent
f9b47c61c3
commit
b862945889
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]}}}
|
||||
|
|
Loading…
Reference in New Issue