Exemplo n.º 1
0
    def _apply_action(self, action, child_data, overall_data):
        """Apply actions to each layer that is rendered.

        Supported actions include:

            * ``merge`` - a "deep" merge that layers new and modified data onto
              existing data
            * ``replace`` - overwrite data at the specified path and replace it
              with the data given in this document
            * ``delete`` - remove the data at the specified path

        :raises UnsupportedActionMethod: If the layering action isn't found
            among ``self.SUPPORTED_METHODS``.
        :raises MissingDocumentKey: If a layering action path isn't found
            in the child document.
        """

        method = action['method']
        if method not in self._SUPPORTED_METHODS:
            raise errors.UnsupportedActionMethod(
                action=action, document=child_data)

        # Use copy to prevent these data from being updated referentially.
        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:]
        elif action_path.startswith('$.data'):
            action_path = action_path[6:]
        if not (action_path.startswith('.') or action_path.startswith('$.')):
            action_path = '.' + action_path

        if method == self._DELETE_ACTION:
            if action_path == '.':
                overall_data.data = {}
            else:
                from_child = utils.jsonpath_parse(overall_data.data,
                                                  action_path)
                if from_child is None:
                    raise errors.MissingDocumentKey(
                        child_schema=child_data.schema,
                        child_layer=child_data.layer,
                        child_name=child_data.name,
                        parent_schema=overall_data.schema,
                        parent_layer=overall_data.layer,
                        parent_name=overall_data.name,
                        action=action)

                engine_utils.deep_delete(from_child, overall_data.data, None)

        elif method == self._MERGE_ACTION:
            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:
                raise errors.MissingDocumentKey(
                    child_schema=child_data.schema,
                    child_layer=child_data.layer,
                    child_name=child_data.name,
                    parent_schema=overall_data.schema,
                    parent_layer=overall_data.layer,
                    parent_name=overall_data.name,
                    action=action)

            # 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_overall is not None:
                overall_data.data = utils.jsonpath_replace(
                    overall_data.data, from_overall, action_path)
            else:
                overall_data.data = utils.jsonpath_replace(
                    overall_data.data, from_child, action_path)
        elif method == self._REPLACE_ACTION:
            from_child = utils.jsonpath_parse(child_data.data, action_path)

            if from_child is None:
                raise errors.MissingDocumentKey(
                    child_schema=child_data.schema,
                    child_layer=child_data.layer,
                    child_name=child_data.name,
                    parent_schema=overall_data.schema,
                    parent_layer=overall_data.layer,
                    parent_name=overall_data.name,
                    action=action)

            overall_data.data = utils.jsonpath_replace(
                overall_data.data, from_child, action_path)

        return overall_data
Exemplo n.º 2
0
    def _apply_action(self, action, child_data, overall_data):
        """Apply actions to each layer that is rendered.

        Supported actions include:

            * `merge` - a "deep" merge that layers new and modified data onto
              existing data
            * `replace` - overwrite data at the specified path and replace it
              with the data given in this document
            * `delete` - remove the data at the specified path
        """
        method = action['method']
        if method not in self.SUPPORTED_METHODS:
            raise errors.UnsupportedActionMethod(action=action,
                                                 document=child_data)

        # Use copy prevent these data from being updated referentially.
        overall_data = copy.deepcopy(overall_data)
        child_data = copy.deepcopy(child_data)
        rendered_data = overall_data

        # Remove empty string paths and ensure that "data" is always present.
        path = action['path'].split('.')
        path = [p for p in path if p != '']
        path.insert(0, 'data')
        last_key = 'data' if not path[-1] else path[-1]

        for attr in path:
            if attr == path[-1]:
                break
            rendered_data = rendered_data.get(attr)
            child_data = child_data.get(attr)

        if method == 'delete':
            # If the entire document is passed (i.e. the dict including
            # metadata, data, schema, etc.) then reset data to an empty dict.
            if last_key == 'data':
                rendered_data['data'] = {}
            elif last_key in rendered_data:
                del rendered_data[last_key]
            elif last_key not in rendered_data:
                # If the key does not exist in `rendered_data`, this is a
                # validation error.
                raise errors.MissingDocumentKey(child=child_data,
                                                parent=rendered_data,
                                                key=last_key)
        elif method == 'merge':
            if last_key in rendered_data and last_key in child_data:
                # If both entries are dictionaries, do a deep merge. Otherwise
                # do a simple merge.
                if (isinstance(rendered_data[last_key], dict)
                        and isinstance(child_data[last_key], dict)):
                    engine_utils.deep_merge(rendered_data[last_key],
                                            child_data[last_key])
                else:
                    rendered_data.setdefault(last_key, child_data[last_key])
            elif last_key in child_data:
                rendered_data.setdefault(last_key, child_data[last_key])
            else:
                # If the key does not exist in the child document, this is a
                # validation error.
                raise errors.MissingDocumentKey(child=child_data,
                                                parent=rendered_data,
                                                key=last_key)
        elif method == 'replace':
            if last_key in rendered_data and last_key in child_data:
                rendered_data[last_key] = child_data[last_key]
            elif last_key in child_data:
                rendered_data.setdefault(last_key, child_data[last_key])
            elif last_key not in child_data:
                # If the key does not exist in the child document, this is a
                # validation error.
                raise errors.MissingDocumentKey(child=child_data,
                                                parent=rendered_data,
                                                key=last_key)

        return overall_data