Skip to content

Commit

Permalink
implements support for alernative JSON object containers
Browse files Browse the repository at this point in the history
  • Loading branch information
RayPlante committed Mar 1, 2017
1 parent 6398c35 commit 6878440
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 6 deletions.
52 changes: 51 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ objectMerge
(e.g. in *properties*, *patternProperties* or *additionalProperties*
schema keywords).

The *objclass* option allows one to request a different dictionary class
to be used to hold the JSON object. The possible values are names
that correspond to specific Python classes. Built-in names include
'OrderedDict', to use the collections.OrderedDict class, or 'default',
which uses whatever class was configured as the default class
(normally, dict). Note that additional classes can be configured in
via the Merger class.

version
Changes the type of the value to an array. New values are appended to the
array in the form of an object with a *value* property. This way all
Expand All @@ -191,8 +199,50 @@ If a merge strategy is not specified in the schema, *objectMerge* is used
to objects and *overwrite* for all other values.

You can implement your own strategies by making subclasses of
jsonmerge.strategies.Strategy and passing them to Merger() constructor.
jsonmerge.strategies.Strategy and passing them to Merger() constructor
(see below).

The Merger Class
----------------

The Merger class allows you to further customize the merging of JSON
data by allowing you to:

- set the schema containing the merge stategy configuration
- provide additional strategy implementations
- set a default class to use for holding JSON object data
- configure additional available JSON object classes

The Merger constructor takes the following arguments

`schema`
The JSON Schema that contains the merge strategy directives
provided as a JSON object. An empty dictionary should be provided
if strategy configuration is needed.

`strategies`
a dictionary mapping strategy names to instances of Strategy
classes. These will be combined with the built-in strategies
(overriding them with the instances having the same name).

`def_objclass`
the name of a supported dictionary-like class to use hold JSON
data by default in the merged result. The name must match a
built-in name or one provided in the `obj_cls_menu` parameter.
Built-in names include *OrderedDict*, which will cause the
`collections.OrderedDict` class to be used, and *default*, which will
use the configured default class. If the *default* name has not been
overridden by the `obj_cls_menu` parameter, which will be a vanilla
`dict`.

`obj_cls_menu`
a dictionary providing possible classes to use as JSON object
containers. The keys are names that can be used as values for the
*objectMerge* strategy's *objclass* option (in addition to the
built-in *OrderedDict* and *default*). Each value is a
function or class that produces an instance of the JSON object
container; it must support an optional dictionary-like object as a
parameter which initializes its contents.

Limitations
-----------
Expand Down
24 changes: 22 additions & 2 deletions jsonmerge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from jsonmerge import strategies
from jsonschema.validators import Draft4Validator, RefResolver
import logging
from collections import OrderedDict

log = logging.getLogger(name=__name__)

Expand Down Expand Up @@ -101,7 +102,7 @@ def work(self, strategy, schema, base, head, meta, **kwargs):
with self.head_resolver.resolving(head.ref) as resolved:
assert head.val == resolved

rv = strategy.merge(self, base, head, schema, meta, **kwargs)
rv = strategy.merge(self, base, head, schema, meta, obj_cls_menu=self.merger.obj_cls_menu, **kwargs)

assert isinstance(rv, JSONValue)
return rv
Expand Down Expand Up @@ -178,14 +179,27 @@ class Merger(object):
"arrayMergeById": strategies.ArrayMergeById()
}

def __init__(self, schema, strategies=()):
def __init__(self, schema, strategies=(), def_objclass='default', obj_cls_menu=None):
"""Create a new Merger object.
schema -- JSON schema to use when merging.
strategies -- Any additional merge strategies to use during merge.
def_objclass -- the name of a supported class to use to hold JSON
object data when one is not specified in the schema; must
be a built-in name or one in obj_cls_menu.
obj_cls_menu -- a dictionary that maps a string name to a
function or class that will return an empty dictionary-like
object to use as a JSON object. The function must accept
either no arguments or a dictionary-like object.
strategies argument should be a dict mapping strategy names to
instances of Strategy subclasses.
objclass names that are built-in include 'OrderedDict', which
uses collections.OrderedDict as an JSON object type, and
'default', which uses a vanilla dict. If def_objclass is not
set to 'default', the class associated with 'default' with the
class associated with that given name.
"""

self.schema = schema
Expand All @@ -194,6 +208,12 @@ def __init__(self, schema, strategies=()):
self.strategies = dict(self.STRATEGIES)
self.strategies.update(strategies)

self.obj_cls_menu = { 'default': dict, 'OrderedDict': OrderedDict }
if obj_cls_menu:
self.obj_cls_menu.update(obj_cls_menu)
if def_objclass is not None and def_objclass != 'default' and def_objclass in self.obj_cls_menu:
self.obj_cls_menu['default'] = self.obj_cls_menu[def_objclass]

def cache_schema(self, schema, uri=None):
"""Cache an external schema reference.
Expand Down
44 changes: 41 additions & 3 deletions jsonmerge/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,55 @@ def get_schema(self, walk, schema, meta, **kwargs):


class ObjectMerge(Strategy):
def merge(self, walk, base, head, schema, meta, **kwargs):
"""A Strategy for merging objects.
Resulting objects have properties from both base and head. Any
properties that are present both in base and head are merged based
on the strategy specified further down in the hierarchy (e.g. in
properties, patternProperties or additionalProperties schema
keywords).
walk -- WalkInstance object for the current context.
base -- JSONValue being merged into.
head -- JSONValue being merged.
schema -- Schema used for merging (also JSONValue)
meta -- Meta data, as passed to the Merger.merge() method.
obj_cls_menu -- a dictionary that maps a string name to a
function or class that will return an empty dictionary-like
object to use as a JSON object. The function must accept
either no arguments or a dictionary-like object. The name
'default' represents the default object to use if not
overridden by the options.
kwargs -- Dict with any extra options given in the 'mergeOptions'
keyword
One mergeOption is supported:
objclass -- a name for the dictionary class to use as a JSON
object in the output. This name must correspond to a
defined class provided in the def_obj_cls.
"""
def merge(self, walk, base, head, schema, meta, obj_cls_menu=None, objclass='default', **kwargs):
if not walk.is_type(head, "object"):
raise HeadInstanceError("Head for an 'object' merge strategy is not an object")

if not obj_cls_menu:
obj_cls_menu = { 'default': dict }
elif not hasattr(obj_cls_menu, 'get'):
raise TypeError("ObjectMerge: obj_cls_menu: not a dictionary-like object: " + repr(obj_cls_menu))
objcls = obj_cls_menu.get(objclass)
if not objcls:
if objclass == 'default':
objcls = dict
else:
raise SchemaError("ObjectMerge: objclass not recognized: " + objclass)

if base.is_undef():
base = JSONValue({}, base.ref)
base = JSONValue(objcls(), base.ref)
else:
if not walk.is_type(base, "object"):
raise BaseInstanceError("Base for an 'object' merge strategy is not an object")

base = JSONValue(dict(base.val), base.ref)
base = JSONValue(objcls(base.val), base.ref)

for k, v in head.items():

Expand Down
60 changes: 60 additions & 0 deletions tests/test_jsonmerge.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# vim:ts=4 sw=4 expandtab softtabstop=4
import unittest
import pdb
import jsonmerge
import jsonmerge.strategies
from jsonmerge.exceptions import (
Expand Down Expand Up @@ -210,6 +211,61 @@ def test_merge_overwrite(self):

self.assertEqual(base, {'a': "b"})

def test_merge_objclass(self):

from collections import OrderedDict
schema = {'mergeStrategy': 'objectMerge', 'mergeOptions': { 'objclass': 'OrderedDict'}}

merger = jsonmerge.Merger(schema)

base = None
base = merger.merge(base, OrderedDict([('c', "a"), ('a', "a")]), schema)
self.assertIsInstance(base, OrderedDict)
self.assertEquals(base.keys(), ['c', 'a'])

base = merger.merge(base, {'a': "b"}, schema)
self.assertIsInstance(base, OrderedDict)
self.assertEquals(base.keys(), ['c', 'a'])

self.assertEqual(base, {'a': "b", 'c': "a"})

def test_merge_def_objclass(self):

from collections import OrderedDict
schema = {'mergeStrategy': 'objectMerge'}
menu = { 'default': OrderedDict }

merger = jsonmerge.Merger(schema, obj_cls_menu=menu)

base = None
base = merger.merge(base, OrderedDict([('c', "a"), ('a', "a")]), schema)
self.assertIsInstance(base, OrderedDict)
self.assertEquals(base.keys(), ['c', 'a'])

base = merger.merge(base, {'a': "b"}, schema)
self.assertIsInstance(base, OrderedDict)
self.assertEquals(base.keys(), ['c', 'a'])

self.assertEqual(base, {'a': "b", 'c': "a"})

def test_merge_def_objclass2(self):

from collections import OrderedDict
schema = {'mergeStrategy': 'objectMerge'}

merger = jsonmerge.Merger(schema, def_objclass='OrderedDict')

base = None
base = merger.merge(base, OrderedDict([('c', "a"), ('a', "a")]), schema)
self.assertIsInstance(base, OrderedDict)
self.assertEquals(base.keys(), ['c', 'a'])

base = merger.merge(base, {'a': "b"}, schema)
self.assertIsInstance(base, OrderedDict)
self.assertEquals(base.keys(), ['c', 'a'])

self.assertEqual(base, {'a': "b", 'c': "a"})

def test_merge_append(self):

schema = {'mergeStrategy': 'objectMerge',
Expand Down Expand Up @@ -1436,3 +1492,7 @@ def test_merge_append_additional(self):
schema2 = merger.get_schema()

self.assertEqual(schema2, expected)

if __name__ == '__main__':
unittest.main()

0 comments on commit 6878440

Please sign in to comment.