From f32dbe7838c9cf9b935247a145bd55e806ea44b2 Mon Sep 17 00:00:00 2001 From: Giulio Fidente Date: Fri, 12 Aug 2016 16:00:03 -0400 Subject: [PATCH] Perform str_replace trying to match longest string first Purpose is to avoid str_replace from doing partial replace when a shorter instance of a matching key is found. Conflicts: heat/engine/hot/functions.py Change-Id: I63c13f4eb3b1375cb2df1a34fd4f09561564f400 Co-Authored-By: Zane Bitter Closes-Bug: 1600209 (cherry picked from commit a2f5b5cb9ae74fb3262948b0cf1c23fc04c4b7cb) --- heat/engine/cfn/functions.py | 8 ++++++-- heat/engine/hot/functions.py | 30 +++++++++++++++++++++++++----- heat/tests/test_hot.py | 20 ++++++++++++++++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/heat/engine/cfn/functions.py b/heat/engine/cfn/functions.py index 42f1f5d78c..2064ee1c17 100644 --- a/heat/engine/cfn/functions.py +++ b/heat/engine/cfn/functions.py @@ -394,8 +394,9 @@ class Replace(function.Function): " " - This is implemented using python str.replace on each key. The order in - which replacements are performed is undefined. + This is implemented using python str.replace on each key. Longer keys are + substituted before shorter ones, but the order in which replacements are + performed is otherwise undefined. """ def __init__(self, stack, fn_name, args): @@ -455,6 +456,9 @@ class Replace(function.Function): return string.replace(placeholder, six.text_type(value)) + mapping = collections.OrderedDict(sorted(mapping.items(), + key=lambda t: len(t[0]), + reverse=True)) return six.moves.reduce(replace, six.iteritems(mapping), template) diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index 0326683fde..4ad8d2b362 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -247,8 +247,9 @@ class Replace(cfn_funcs.Replace): " " - This is implemented using Python's str.replace on each key. The order in - which replacements are performed is undefined. + This is implemented using python str.replace on each key. Longer keys are + substituted before shorter ones, but the order in which replacements are + performed is otherwise undefined. """ def _parse_args(self): @@ -275,9 +276,25 @@ class ReplaceJson(Replace): ''' A function for performing string substitutions. - Behaves the same as Replace, but tolerates non-string parameter - values, e.g map/list - these are serialized as json before doing - the string substitution. + Takes the form:: + + str_replace: + template: + params: + : + : + ... + + And resolves to:: + + " " + + This is implemented using python str.replace on each key. Longer keys are + substituted before shorter ones, but the order in which replacements are + performed is otherwise undefined. + + Non-string param values (e.g maps or lists) are serialized as JSON before + being substituted in. ''' def result(self): @@ -319,6 +336,9 @@ class ReplaceJson(Replace): return string.replace(placeholder, six.text_type(value)) + mapping = collections.OrderedDict(sorted(mapping.items(), + key=lambda t: len(t[0]), + reverse=True)) return six.moves.reduce(replace, six.iteritems(mapping), template) diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index eaf2688654..57ec6a02e1 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -558,6 +558,26 @@ class HOTemplateTest(common.HeatTestCase): self.assertEqual(snippet_resolved, self.resolve(snippet, tmpl)) + def test_str_replace_order(self, tpl=hot_tpl_empty): + """Test str_replace function substitution order.""" + + snippet = {'str_replace': {'template': '1234567890', + 'params': {'1': 'a', + '12': 'b', + '123': 'c', + '1234': 'd', + '12345': 'e', + '123456': 'f', + '1234567': 'g'}}} + + tmpl = template.Template(tpl) + + self.assertEqual('g890', self.resolve(snippet, tmpl)) + + def test_str_replace_liberty_order(self): + """Test str_replace function substitution order.""" + self.test_str_replace_order(hot_liberty_tpl_empty) + def test_str_replace_syntax(self): """ Test str_replace function syntax.