refactor: Clean up jsonpath_replace method
This patch set cleans up jsonpath_replace method to increase its readability. Currently, there is some weird legacy code in there which doesn't make much sense and is most likely dead code. In fact, before this change, there was no unit test for validating that `MissingDocumentPattern` is raised correctly. The `jsonpath_replace` is refactored to only call the inner private function _do_replace once, after performing data expansion (populating the data dictionary with nonexistent nested keys). Unit tests have been added to validate the exception above. Change-Id: I1c18c4f8c79c1b9d3124747f8aa04743f27434eb
This commit is contained in:
parent
5979f7f93a
commit
2acbff8d57
|
@ -20,7 +20,6 @@ from beaker.cache import CacheManager
|
||||||
from beaker.util import parse_cache_config_options
|
from beaker.util import parse_cache_config_options
|
||||||
import jsonpath_ng
|
import jsonpath_ng
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import excutils
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from deckhand.conf import config
|
from deckhand.conf import config
|
||||||
|
@ -107,8 +106,8 @@ def jsonpath_parse(data, jsonpath, match_all=False):
|
||||||
return result if match_all else result[0]
|
return result if match_all else result[0]
|
||||||
|
|
||||||
|
|
||||||
def _populate_data_with_attributes(jsonpath, data):
|
def _execute_data_expansion(jsonpath, data):
|
||||||
# Populates ``data`` with any path specified in ``jsonpath``. For example,
|
# Expand ``data`` with any path specified in ``jsonpath``. For example,
|
||||||
# if jsonpath is ".foo[0].bar.baz" then for each subpath -- foo[0], bar,
|
# if jsonpath is ".foo[0].bar.baz" then for each subpath -- foo[0], bar,
|
||||||
# and baz -- that key will be added to ``data`` if missing.
|
# and baz -- that key will be added to ``data`` if missing.
|
||||||
d = data
|
d = data
|
||||||
|
@ -179,41 +178,38 @@ def jsonpath_replace(data, value, jsonpath, pattern=None):
|
||||||
raise ValueError('The provided jsonpath %s does not begin with "." '
|
raise ValueError('The provided jsonpath %s does not begin with "." '
|
||||||
'or "$"' % jsonpath)
|
'or "$"' % jsonpath)
|
||||||
|
|
||||||
def _do_replace():
|
def _execute_replace(path, path_to_change):
|
||||||
p = _jsonpath_parse(jsonpath)
|
if path_to_change:
|
||||||
p_to_change = p.find(data)
|
|
||||||
|
|
||||||
if p_to_change:
|
|
||||||
new_value = value
|
new_value = value
|
||||||
if pattern:
|
if pattern:
|
||||||
to_replace = p_to_change[0].value
|
to_replace = path_to_change[0].value
|
||||||
# `new_value` represents the value to inject into `to_replace`
|
# `new_value` represents the value to inject into `to_replace`
|
||||||
# that matches the `pattern`.
|
# that matches the `pattern`.
|
||||||
try:
|
try:
|
||||||
|
# A pattern requires us to look up the data located at
|
||||||
|
# data[jsonpath] and then figure out what
|
||||||
|
# re.match(data[jsonpath], pattern) is (in pseudocode).
|
||||||
|
# Raise an exception in case the path isn't present in the
|
||||||
|
# data and a pattern has been provided since it is
|
||||||
|
# otherwise impossible to do the look-up.
|
||||||
new_value = re.sub(pattern, str(value), to_replace)
|
new_value = re.sub(pattern, str(value), to_replace)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
with excutils.save_and_reraise_exception():
|
LOG.error('Failed to substitute the value %s into %s '
|
||||||
LOG.error('Failed to substitute the value %s into %s '
|
'using pattern %s. Details: %s', str(value),
|
||||||
'using pattern %s. Details: %s', str(value),
|
to_replace, pattern, six.text_type(e))
|
||||||
to_replace, pattern, six.text_type(e))
|
raise errors.MissingDocumentPattern(jsonpath=jsonpath,
|
||||||
return p.update(data, new_value)
|
pattern=pattern)
|
||||||
|
|
||||||
result = _do_replace()
|
return path.update(data, new_value)
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
|
|
||||||
# A pattern requires us to look up the data located at data[jsonpath]
|
# Deckhand should be smart enough to create the nested keys in the
|
||||||
# and then figure out what re.match(data[jsonpath], pattern) is (in
|
|
||||||
# pseudocode). But raise an exception in case the path isn't present in the
|
|
||||||
# data and a pattern has been provided since it is impossible to do the
|
|
||||||
# look-up.
|
|
||||||
if pattern:
|
|
||||||
raise errors.MissingDocumentPattern(path=jsonpath, pattern=pattern)
|
|
||||||
|
|
||||||
# However, Deckhand should be smart enough to create the nested keys in the
|
|
||||||
# data if they don't exist and a pattern isn't required.
|
# data if they don't exist and a pattern isn't required.
|
||||||
_populate_data_with_attributes(jsonpath, data)
|
path = _jsonpath_parse(jsonpath)
|
||||||
return _do_replace()
|
path_to_change = path.find(data)
|
||||||
|
if not path_to_change:
|
||||||
|
_execute_data_expansion(jsonpath, data)
|
||||||
|
path_to_change = path.find(data)
|
||||||
|
return _execute_replace(path, path_to_change)
|
||||||
|
|
||||||
|
|
||||||
def multisort(data, sort_by=None, order_by=None):
|
def multisort(data, sort_by=None, order_by=None):
|
||||||
|
|
|
@ -131,6 +131,7 @@ class RenderedDocumentsResource(api_base.BaseResource):
|
||||||
errors.IndeterminateDocumentParent,
|
errors.IndeterminateDocumentParent,
|
||||||
errors.LayeringPolicyNotFound,
|
errors.LayeringPolicyNotFound,
|
||||||
errors.MissingDocumentKey,
|
errors.MissingDocumentKey,
|
||||||
|
errors.MissingDocumentPattern,
|
||||||
errors.SubstitutionSourceDataNotFound,
|
errors.SubstitutionSourceDataNotFound,
|
||||||
errors.SubstitutionSourceNotFound,
|
errors.SubstitutionSourceNotFound,
|
||||||
errors.UnknownSubstitutionError,
|
errors.UnknownSubstitutionError,
|
||||||
|
|
|
@ -19,6 +19,7 @@ from testtools.matchers import Equals
|
||||||
from testtools.matchers import MatchesAny
|
from testtools.matchers import MatchesAny
|
||||||
|
|
||||||
from deckhand.common import utils
|
from deckhand.common import utils
|
||||||
|
from deckhand import errors
|
||||||
from deckhand.tests.unit import base as test_base
|
from deckhand.tests.unit import base as test_base
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,6 +49,30 @@ class TestJSONPathReplace(test_base.DeckhandTestCase):
|
||||||
result = utils.jsonpath_replace({}, 'foo', path)
|
result = utils.jsonpath_replace({}, 'foo', path)
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_jsonpath_replace_with_pattern(self):
|
||||||
|
path = ".values.endpoints.admin"
|
||||||
|
body = {"values": {"endpoints": {"admin": "REGEX_FRESH"}}}
|
||||||
|
expected = {"values": {"endpoints": {"admin": "EAT_FRESH"}}}
|
||||||
|
result = utils.jsonpath_replace(body, "EAT", jsonpath=path,
|
||||||
|
pattern="REGEX")
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
|
||||||
|
class TestJSONPathReplaceNegative(test_base.DeckhandTestCase):
|
||||||
|
"""Validate JSONPath replace negative scenarios."""
|
||||||
|
|
||||||
|
def test_jsonpath_replace_without_expected_pattern_raises_exc(self):
|
||||||
|
empty_body = {}
|
||||||
|
error_re = (".*missing the pattern %s specified under .* at path %s.*")
|
||||||
|
|
||||||
|
self.assertRaisesRegex(errors.MissingDocumentPattern,
|
||||||
|
error_re % ("way invalid", "\$.path"),
|
||||||
|
utils.jsonpath_replace,
|
||||||
|
empty_body,
|
||||||
|
value="test",
|
||||||
|
jsonpath=".path",
|
||||||
|
pattern="way invalid")
|
||||||
|
|
||||||
|
|
||||||
class TestJSONPathUtilsCaching(test_base.DeckhandTestCase):
|
class TestJSONPathUtilsCaching(test_base.DeckhandTestCase):
|
||||||
"""Validate that JSONPath caching works."""
|
"""Validate that JSONPath caching works."""
|
||||||
|
|
Loading…
Reference in New Issue