44from __future__ import unicode_literals
55
66import json
7+ import decimal
78import doctest
89import unittest
910import jsonpatch
@@ -278,6 +279,34 @@ def test_str(self):
278279 self .assertEqual (json .dumps (patch_obj ), patch .to_string ())
279280
280281
282+ def custom_types_dumps (obj ):
283+ def default (obj ):
284+ if isinstance (obj , decimal .Decimal ):
285+ return {'__decimal__' : str (obj )}
286+ raise TypeError ('Unknown type' )
287+
288+ return json .dumps (obj , default = default )
289+
290+
291+ def custom_types_loads (obj ):
292+ def as_decimal (dct ):
293+ if '__decimal__' in dct :
294+ return decimal .Decimal (dct ['__decimal__' ])
295+ return dct
296+
297+ return json .loads (obj , object_hook = as_decimal )
298+
299+
300+ class CustomTypesJsonPatch (jsonpatch .JsonPatch ):
301+ @staticmethod
302+ def json_dumper (obj ):
303+ return custom_types_dumps (obj )
304+
305+ @staticmethod
306+ def json_loader (obj ):
307+ return custom_types_loads (obj )
308+
309+
281310class MakePatchTestCase (unittest .TestCase ):
282311
283312 def test_apply_patch_to_copy (self ):
@@ -456,6 +485,35 @@ def test_issue103(self):
456485 self .assertEqual (res , dst )
457486 self .assertIsInstance (res ['A' ], float )
458487
488+ def test_custom_types_diff (self ):
489+ old = {'value' : decimal .Decimal ('1.0' )}
490+ new = {'value' : decimal .Decimal ('1.00' )}
491+ generated_patch = jsonpatch .JsonPatch .from_diff (
492+ old , new , dumps = custom_types_dumps )
493+ str_patch = generated_patch .to_string (dumps = custom_types_dumps )
494+ loaded_patch = jsonpatch .JsonPatch .from_string (
495+ str_patch , loads = custom_types_loads )
496+ self .assertEqual (generated_patch , loaded_patch )
497+ new_from_patch = jsonpatch .apply_patch (old , generated_patch )
498+ self .assertEqual (new , new_from_patch )
499+
500+ def test_custom_types_subclass (self ):
501+ old = {'value' : decimal .Decimal ('1.0' )}
502+ new = {'value' : decimal .Decimal ('1.00' )}
503+ generated_patch = CustomTypesJsonPatch .from_diff (old , new )
504+ str_patch = generated_patch .to_string ()
505+ loaded_patch = CustomTypesJsonPatch .from_string (str_patch )
506+ self .assertEqual (generated_patch , loaded_patch )
507+ new_from_patch = jsonpatch .apply_patch (old , loaded_patch )
508+ self .assertEqual (new , new_from_patch )
509+
510+ def test_custom_types_subclass_load (self ):
511+ old = {'value' : decimal .Decimal ('1.0' )}
512+ new = {'value' : decimal .Decimal ('1.00' )}
513+ patch = CustomTypesJsonPatch .from_string (
514+ '[{"op": "replace", "path": "/value", "value": {"__decimal__": "1.00"}}]' )
515+ new_from_patch = jsonpatch .apply_patch (old , patch )
516+ self .assertEqual (new , new_from_patch )
459517
460518
461519class OptimizationTests (unittest .TestCase ):
@@ -671,6 +729,86 @@ def test_create_with_pointer(self):
671729 self .assertEqual (result , expected )
672730
673731
732+ class JsonPatchCreationTest (unittest .TestCase ):
733+
734+ def test_creation_fails_with_invalid_patch (self ):
735+ invalid_patches = [
736+ { 'path' : '/foo' , 'value' : 'bar' },
737+ {'op' : 0xADD , 'path' : '/foo' , 'value' : 'bar' },
738+ {'op' : 'boo' , 'path' : '/foo' , 'value' : 'bar' },
739+ {'op' : 'add' , 'value' : 'bar' },
740+ ]
741+ for patch in invalid_patches :
742+ with self .assertRaises (jsonpatch .InvalidJsonPatch ):
743+ jsonpatch .JsonPatch ([patch ])
744+
745+ with self .assertRaises (jsonpointer .JsonPointerException ):
746+ jsonpatch .JsonPatch ([{'op' : 'add' , 'path' : 'foo' , 'value' : 'bar' }])
747+
748+
749+ class UtilityMethodTests (unittest .TestCase ):
750+
751+ def test_boolean_coercion (self ):
752+ empty_patch = jsonpatch .JsonPatch ([])
753+ self .assertFalse (empty_patch )
754+
755+ def test_patch_equality (self ):
756+ p = jsonpatch .JsonPatch ([{'op' : 'add' , 'path' : '/foo' , 'value' : 'bar' }])
757+ q = jsonpatch .JsonPatch ([{'op' : 'add' , 'path' : '/foo' , 'value' : 'bar' }])
758+ different_op = jsonpatch .JsonPatch ([{'op' : 'remove' , 'path' : '/foo' }])
759+ different_path = jsonpatch .JsonPatch ([{'op' : 'add' , 'path' : '/bar' , 'value' : 'bar' }])
760+ different_value = jsonpatch .JsonPatch ([{'op' : 'add' , 'path' : '/foo' , 'value' : 'foo' }])
761+ self .assertNotEqual (p , different_op )
762+ self .assertNotEqual (p , different_path )
763+ self .assertNotEqual (p , different_value )
764+ self .assertEqual (p , q )
765+
766+ def test_operation_equality (self ):
767+ add = jsonpatch .AddOperation ({'path' : '/new-element' , 'value' : 'new-value' })
768+ add2 = jsonpatch .AddOperation ({'path' : '/new-element' , 'value' : 'new-value' })
769+ rm = jsonpatch .RemoveOperation ({'path' : '/target' })
770+ self .assertEqual (add , add2 )
771+ self .assertNotEqual (add , rm )
772+
773+ def test_add_operation_structure (self ):
774+ with self .assertRaises (jsonpatch .InvalidJsonPatch ):
775+ jsonpatch .AddOperation ({'path' : '/' }).apply ({})
776+
777+ def test_replace_operation_structure (self ):
778+ with self .assertRaises (jsonpatch .InvalidJsonPatch ):
779+ jsonpatch .ReplaceOperation ({'path' : '/' }).apply ({})
780+
781+ with self .assertRaises (jsonpatch .InvalidJsonPatch ):
782+ jsonpatch .ReplaceOperation ({'path' : '/top/-' , 'value' : 'foo' }).apply ({'top' : {'inner' : 'value' }})
783+
784+ with self .assertRaises (jsonpatch .JsonPatchConflict ):
785+ jsonpatch .ReplaceOperation ({'path' : '/top/missing' , 'value' : 'foo' }).apply ({'top' : {'inner' : 'value' }})
786+
787+ def test_move_operation_structure (self ):
788+ with self .assertRaises (jsonpatch .InvalidJsonPatch ):
789+ jsonpatch .MoveOperation ({'path' : '/target' }).apply ({})
790+
791+ with self .assertRaises (jsonpatch .JsonPatchConflict ):
792+ jsonpatch .MoveOperation ({'from' : '/source' , 'path' : '/target' }).apply ({})
793+
794+ def test_test_operation_structure (self ):
795+ with self .assertRaises (jsonpatch .JsonPatchTestFailed ):
796+ jsonpatch .TestOperation ({'path' : '/target' }).apply ({})
797+
798+ with self .assertRaises (jsonpatch .InvalidJsonPatch ):
799+ jsonpatch .TestOperation ({'path' : '/target' }).apply ({'target' : 'value' })
800+
801+ def test_copy_operation_structure (self ):
802+ with self .assertRaises (jsonpatch .InvalidJsonPatch ):
803+ jsonpatch .CopyOperation ({'path' : '/target' }).apply ({})
804+
805+ with self .assertRaises (jsonpatch .JsonPatchConflict ):
806+ jsonpatch .CopyOperation ({'path' : '/target' , 'from' : '/source' }).apply ({})
807+
808+ with self .assertRaises (jsonpatch .JsonPatchConflict ):
809+ jsonpatch .CopyOperation ({'path' : '/target' , 'from' : '/source' }).apply ({})
810+
811+
674812class CustomJsonPointer (jsonpointer .JsonPointer ):
675813 pass
676814
@@ -690,7 +828,7 @@ def test_json_patch_from_string(self):
690828 self .assertEqual (res .pointer_cls , CustomJsonPointer )
691829
692830 def test_json_patch_from_object (self ):
693- patch = [{'op' : 'add' , 'path' : '/baz' , 'value' : 'qux' }],
831+ patch = [{'op' : 'add' , 'path' : '/baz' , 'value' : 'qux' }]
694832 res = jsonpatch .JsonPatch (
695833 patch , pointer_cls = CustomJsonPointer ,
696834 )
@@ -815,6 +953,8 @@ def get_suite():
815953 suite .addTest (unittest .makeSuite (ConflictTests ))
816954 suite .addTest (unittest .makeSuite (OptimizationTests ))
817955 suite .addTest (unittest .makeSuite (JsonPointerTests ))
956+ suite .addTest (unittest .makeSuite (JsonPatchCreationTest ))
957+ suite .addTest (unittest .makeSuite (UtilityMethodTests ))
818958 suite .addTest (unittest .makeSuite (CustomJsonPointerTests ))
819959 return suite
820960
0 commit comments