layering: Support layering for primitive types

This patchset adds support for layering between primitive types
of data. It is not possible to merge together a string with
an integer or a boolean with a dictionary -- however, merge
actions can still be resolved in such cases in order to get layering
to work:

merge => child replaces parent data
replace => child replaces parent data
delete => empty dictionary (in effect)

The only path that is possible in such cases is "." as
a primitive doesn't have a JSON path. Else missing document key
exceptions are raised.

Change-Id: I9da557bf9444c37b3e59672f3a93f49fdf1e4a02
This commit is contained in:
Felipe Monteiro 2018-06-28 03:18:17 -04:00
parent 108ac7c216
commit 41f5aecdf2
3 changed files with 88 additions and 13 deletions

View File

@ -545,6 +545,13 @@ class DocumentLayering(object):
overall_data = copy.deepcopy(overall_data)
child_data = copy.deepcopy(child_data)
# If None is used, then consider it as a placeholder and coerce the
# data into a dictionary.
if overall_data is None:
overall_data = {}
if child_data is None:
child_data = {}
action_path = action['path']
if action_path.startswith('.data'):
action_path = action_path[5:]
@ -568,7 +575,7 @@ class DocumentLayering(object):
engine_utils.deep_delete(from_child, overall_data.data, None)
elif method == self._MERGE_ACTION:
from_parent = utils.jsonpath_parse(overall_data.data, action_path)
from_overall = utils.jsonpath_parse(overall_data.data, action_path)
from_child = utils.jsonpath_parse(child_data.data, action_path)
if from_child is None:
@ -581,13 +588,29 @@ class DocumentLayering(object):
parent_name=overall_data.name,
action=action)
if (isinstance(from_parent, dict) and
isinstance(from_child, dict)):
engine_utils.deep_merge(from_parent, from_child)
# If both the child and parent data are dictionaries, then
# traditional merging is possible using JSON path resolution.
# Otherwise, JSON path resolution is not possible, so the only
# way to perform layering is to prioritize the child data over
# that of the parent. This applies when the child data is a
# non-dict, the parent data is a non-dict, or both.
if all(isinstance(x, dict) for x in (from_overall, from_child)):
engine_utils.deep_merge(from_overall, from_child)
else:
LOG.info('Child data is type: %s for [%s, %s] %s. Parent data '
'is type: %s for [%s, %s] %s. Both must be '
'dictionaries for regular JSON path merging to work. '
'Because this is not the case, child data will be '
'prioritized over parent data for "merge" action.',
type(from_child), child_data.schema, child_data.layer,
child_data.name, type(from_overall),
overall_data.schema, overall_data.layer,
overall_data.name)
from_overall = from_child
if from_parent is not None:
if from_overall is not None:
overall_data.data = utils.jsonpath_replace(
overall_data.data, from_parent, action_path)
overall_data.data, from_overall, action_path)
else:
overall_data.data = utils.jsonpath_replace(
overall_data.data, from_child, action_path)

View File

@ -610,6 +610,58 @@ class TestDocumentLayering2Layers(TestDocumentLayering):
documents = doc_factory.gen_test(mapping, site_abstract=False)
self._test_layering(documents, site_expected[idx])
def test_layering_with_primitives(self):
"""Validates that layering works with non-dictionaries or "primitives"
for short, which include integers, strings, etc.
"""
# Validate layering with strings.
mapping = {
"_GLOBAL_DATA_1_": {"data": "global"},
"_SITE_DATA_1_": {"data": "site"},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]}
}
doc_factory = factories.DocumentFactory(2, [1, 1])
documents = doc_factory.gen_test(mapping, site_abstract=False)
self._test_layering(documents, site_expected="site")
# Validate layering with integers.
mapping = {
"_GLOBAL_DATA_1_": {"data": 2},
"_SITE_DATA_1_": {"data": 1},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]}
}
doc_factory = factories.DocumentFactory(2, [1, 1])
documents = doc_factory.gen_test(mapping, site_abstract=False)
self._test_layering(documents, site_expected=1)
# Validate layering with mixed primitives.
mapping = {
"_GLOBAL_DATA_1_": {"data": False},
"_SITE_DATA_1_": {"data": 'a'},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]}
}
doc_factory = factories.DocumentFactory(2, [1, 1])
documents = doc_factory.gen_test(mapping, site_abstract=False)
self._test_layering(documents, site_expected='a')
def test_layering_with_incompatible_types(self):
"""Validates that layering works for incompatible types: that is,
merging a dictionary with a primitive is not possible. For this
case, the child data is simply selected over the parent data.
"""
mapping = {
"_GLOBAL_DATA_1_": {"data": {"foo": "bar"}},
"_SITE_DATA_1_": {"data": "site"},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]}
}
doc_factory = factories.DocumentFactory(2, [1, 1])
documents = doc_factory.gen_test(mapping, site_abstract=False)
self._test_layering(documents, site_expected="site")
class TestDocumentLayering2LayersAbstractConcrete(TestDocumentLayering):
"""The the 2-layer payload with site/global layers concrete.

View File

@ -219,7 +219,7 @@ class TestDocumentLayeringWithSubstitution(
"path": "."
}
}],
"_SITE_DATA_1_": {"data": "placeholder"},
"_SITE_DATA_1_": {"data": {}},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]},
"_SITE_SUBSTITUTIONS_1_": [{
@ -275,7 +275,7 @@ class TestDocumentLayeringWithSubstitution(
"path": "."
}
}],
"_SITE_DATA_1_": {"data": "placeholder"},
"_SITE_DATA_1_": {"data": {}},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]},
"_SITE_SUBSTITUTIONS_1_": [{
@ -288,7 +288,7 @@ class TestDocumentLayeringWithSubstitution(
"path": "."
}
}],
"_SITE_DATA_2_": {"data": "placeholder"},
"_SITE_DATA_2_": {"data": {}},
"_SITE_ACTIONS_2_": {
"actions": [{"method": "merge", "path": "."}]},
"_SITE_SUBSTITUTIONS_2_": [{
@ -349,7 +349,7 @@ class TestDocumentLayeringWithSubstitution(
"path": "."
}
}],
"_SITE_DATA_1_": {"data": "placeholder"},
"_SITE_DATA_1_": {"data": {}},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]},
"_SITE_SUBSTITUTIONS_1_": [{
@ -362,7 +362,7 @@ class TestDocumentLayeringWithSubstitution(
"path": "."
}
}],
"_SITE_DATA_2_": {"data": "placeholder"},
"_SITE_DATA_2_": {"data": {}},
"_SITE_ACTIONS_2_": {
"actions": [{"method": "merge", "path": "."}]},
"_SITE_SUBSTITUTIONS_2_": [{
@ -423,7 +423,7 @@ class TestDocumentLayeringWithSubstitution(
"path": "."
}
}],
"_SITE_DATA_1_": {"data": "placeholder"},
"_SITE_DATA_1_": {"data": {}},
"_SITE_ACTIONS_1_": {
"actions": [{"method": "merge", "path": "."}]},
"_SITE_SUBSTITUTIONS_1_": [
@ -448,7 +448,7 @@ class TestDocumentLayeringWithSubstitution(
}
}
],
"_SITE_DATA_2_": {"data": "placeholder"},
"_SITE_DATA_2_": {"data": {}},
"_SITE_ACTIONS_2_": {
"actions": [{"method": "merge", "path": "."}]},
"_SITE_SUBSTITUTIONS_2_": [