object_patch 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.gitmodules +4 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/.yardopts +3 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +77 -0
- data/Rakefile +18 -0
- data/TODO.md +7 -0
- data/lib/object_patch.rb +46 -0
- data/lib/object_patch/exceptions.rb +51 -0
- data/lib/object_patch/generator.rb +114 -0
- data/lib/object_patch/operation_factory.rb +25 -0
- data/lib/object_patch/operations.rb +79 -0
- data/lib/object_patch/operations/add.rb +57 -0
- data/lib/object_patch/operations/copy.rb +69 -0
- data/lib/object_patch/operations/move.rb +63 -0
- data/lib/object_patch/operations/remove.rb +49 -0
- data/lib/object_patch/operations/replace.rb +59 -0
- data/lib/object_patch/operations/test.rb +47 -0
- data/lib/object_patch/pointer.rb +90 -0
- data/lib/object_patch/version.rb +4 -0
- data/object_patch.gemspec +28 -0
- data/spec/fixtures/tenderlove_tests.json +36 -0
- data/spec/helpers/json_test_loader.rb +19 -0
- data/spec/object_patch/fixture_tests_spec.rb +62 -0
- data/spec/object_patch/object_patch_spec.rb +9 -0
- data/spec/object_patch/pointer_spec.rb +52 -0
- data/spec/spec_helper.rb +24 -0
- metadata +181 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch::Operations
|
3
|
+
|
4
|
+
# A representation of a JSON pointer add operation.
|
5
|
+
class Add
|
6
|
+
|
7
|
+
# Apply this operation to the provided document and return the updated
|
8
|
+
# document. Please note that the changes will be reflected not only in the
|
9
|
+
# returned value but the original document that was passed in as well.
|
10
|
+
#
|
11
|
+
# @param [Object] target_doc The document that will be modified by this
|
12
|
+
# patch.
|
13
|
+
# @return [Object] The modified document
|
14
|
+
def apply(target_doc)
|
15
|
+
key = processed_path.last
|
16
|
+
inner_obj = ObjectPatch::Pointer.eval(processed_path[0...-1], target_doc)
|
17
|
+
|
18
|
+
raise MissingTargetException, @path unless inner_obj
|
19
|
+
|
20
|
+
if key
|
21
|
+
ObjectPatch::Operations.add_op(inner_obj, key, @value)
|
22
|
+
else
|
23
|
+
inner_obj.replace(@value)
|
24
|
+
end
|
25
|
+
|
26
|
+
target_doc
|
27
|
+
end
|
28
|
+
|
29
|
+
# Setup the add operation with any required arguments.
|
30
|
+
#
|
31
|
+
# @param [Hash] patch_data Parameters necessary to build the operation.
|
32
|
+
# @option patch_data [String] path The location in the target document to
|
33
|
+
# add.
|
34
|
+
# @option patch_data [Object] value The value to insert into the target.
|
35
|
+
# @return [void]
|
36
|
+
def initialize(patch_data)
|
37
|
+
@path = patch_data.fetch('path')
|
38
|
+
@value = patch_data.fetch('value')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the path after being expanded by the JSON pointer semantics.
|
42
|
+
#
|
43
|
+
# @return [Array<String>] Expanded pointer path
|
44
|
+
def processed_path
|
45
|
+
ObjectPatch::Pointer.parse(@path)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Covert this operation to a format that can be built into a full on JSON
|
49
|
+
# patch.
|
50
|
+
#
|
51
|
+
# @return [Hash<String => String>] JSON patch add operation
|
52
|
+
def to_patch
|
53
|
+
{ 'op' => 'add', 'path' => @path, 'value' => @value }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch::Operations
|
3
|
+
|
4
|
+
# A representation of a JSON pointer copy operation.
|
5
|
+
class Copy
|
6
|
+
|
7
|
+
# Apply this operation to the provided document and return the updated
|
8
|
+
# document. Please note that the changes will be reflected not only in the
|
9
|
+
# returned value but the original document that was passed in as well.
|
10
|
+
#
|
11
|
+
# @param [Object] target_doc The document that will be modified by this
|
12
|
+
# patch.
|
13
|
+
# @return [Object] The modified document
|
14
|
+
def apply(target_doc)
|
15
|
+
src_key = processed_from.last
|
16
|
+
dst_key = processed_path.last
|
17
|
+
|
18
|
+
src_obj = ObjectPatch::Pointer.eval(processed_from[0...-1], target_doc)
|
19
|
+
dst_obj = ObjectPatch::Pointer.eval(processed_path[0...-1], target_doc)
|
20
|
+
|
21
|
+
if src_obj.is_a?(Array)
|
22
|
+
raise ObjectPatch::InvalidIndexError unless src_key =~ /\A\d+\Z/
|
23
|
+
copied_obj = src_obj.fetch(src_key.to_i)
|
24
|
+
else
|
25
|
+
copied_obj = src_obj.fetch(src_key)
|
26
|
+
end
|
27
|
+
|
28
|
+
ObjectPatch::Operations.add_op(dst_obj, dst_key, copied_obj)
|
29
|
+
|
30
|
+
target_doc
|
31
|
+
end
|
32
|
+
|
33
|
+
# Setup the replace operation with any required arguments.
|
34
|
+
#
|
35
|
+
# @param [Hash] patch_data Parameters necessary to build the operation.
|
36
|
+
# @option patch_data [String] path The location in the target document to
|
37
|
+
# duplicate the data to.
|
38
|
+
# @option patch_data [String] from The source data that will be copied into
|
39
|
+
# the new location.
|
40
|
+
# @return [void]
|
41
|
+
def initialize(patch_data)
|
42
|
+
@from = patch_data.fetch('from')
|
43
|
+
@path = patch_data.fetch('path')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the from field after being expanded by the JSON pointer semantics.
|
47
|
+
#
|
48
|
+
# @return [Array<String>] Expanded pointer path
|
49
|
+
def processed_from
|
50
|
+
ObjectPatch::Pointer.parse(@from)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the path after being expanded by the JSON pointer semantics.
|
54
|
+
#
|
55
|
+
# @return [Array<String>] Expanded pointer path
|
56
|
+
def processed_path
|
57
|
+
ObjectPatch::Pointer.parse(@path)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Covert this operation to a format that can be built into a full on JSON
|
61
|
+
# patch.
|
62
|
+
#
|
63
|
+
# @return [Hash<String => String>] JSON patch copy operation
|
64
|
+
def to_patch
|
65
|
+
{ 'op' => 'copy', 'from' => @from, 'path' => @path }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch::Operations
|
3
|
+
|
4
|
+
# A representation of a JSON pointer move operation.
|
5
|
+
class Move
|
6
|
+
|
7
|
+
# Apply this operation to the provided document and return the updated
|
8
|
+
# document. Please note that the changes will be reflected not only in the
|
9
|
+
# returned value but the original document that was passed in as well.
|
10
|
+
#
|
11
|
+
# @param [Object] target_doc The document that will be modified by this
|
12
|
+
# patch.
|
13
|
+
# @return [Object] The modified document
|
14
|
+
def apply(target_doc)
|
15
|
+
src_key = processed_from.last
|
16
|
+
dst_key = processed_path.last
|
17
|
+
|
18
|
+
src_obj = ObjectPatch::Pointer.eval(processed_from[0...-1], target_doc)
|
19
|
+
dst_obj = ObjectPatch::Pointer.eval(processed_path[0...-1], target_doc)
|
20
|
+
|
21
|
+
moved_value = ObjectPatch::Operations.rm_op(src_obj, src_key)
|
22
|
+
ObjectPatch::Operations.add_op(dst_obj, dst_key, moved_value)
|
23
|
+
|
24
|
+
target_doc
|
25
|
+
end
|
26
|
+
|
27
|
+
# Setup the replace operation with any required arguments.
|
28
|
+
#
|
29
|
+
# @param [Hash] patch_data Parameters necessary to build the operation.
|
30
|
+
# @option patch_data [String] path The location in the target document to
|
31
|
+
# place moved data.
|
32
|
+
# @option patch_data [String] from The location that will be moved to a new
|
33
|
+
# path.
|
34
|
+
# @return [void]
|
35
|
+
def initialize(patch_data)
|
36
|
+
@from = patch_data.fetch('from')
|
37
|
+
@path = patch_data.fetch('path')
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the from field after being expanded by the JSON pointer semantics.
|
41
|
+
#
|
42
|
+
# @return [Array<String>] Expanded pointer path
|
43
|
+
def processed_from
|
44
|
+
ObjectPatch::Pointer.parse(@from)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the path after being expanded by the JSON pointer semantics.
|
48
|
+
#
|
49
|
+
# @return [Array<String>] Expanded pointer path
|
50
|
+
def processed_path
|
51
|
+
ObjectPatch::Pointer.parse(@path)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Covert this operation to a format that can be built into a full on JSON
|
55
|
+
# patch.
|
56
|
+
#
|
57
|
+
# @return [Hash<String => String>] JSON patch move operation
|
58
|
+
def to_patch
|
59
|
+
{ 'op' => 'move', 'from' => @from, 'path' => @path }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch::Operations
|
3
|
+
|
4
|
+
# A representation of a JSON pointer remove operation.
|
5
|
+
class Remove
|
6
|
+
|
7
|
+
# Apply this operation to the provided document and return the updated
|
8
|
+
# document. Please note that the changes will be reflected not only in the
|
9
|
+
# returned value but the original document that was passed in as well.
|
10
|
+
#
|
11
|
+
# @param [Object] target_doc The document that will be modified by this
|
12
|
+
# patch.
|
13
|
+
# @return [Object] The modified document
|
14
|
+
def apply(target_doc)
|
15
|
+
key = processed_path.last
|
16
|
+
inner_obj = ObjectPatch::Pointer.eval(processed_path[0...-1], target_doc)
|
17
|
+
|
18
|
+
ObjectPatch::Operations.rm_op(inner_obj, key)
|
19
|
+
|
20
|
+
target_doc
|
21
|
+
end
|
22
|
+
|
23
|
+
# Setup the remove operation with any required arguments.
|
24
|
+
#
|
25
|
+
# @param [Hash] patch_data Parameters necessary to build the operation.
|
26
|
+
# @option patch_data [String] path The location in the target document to
|
27
|
+
# remove.
|
28
|
+
# @return [void]
|
29
|
+
def initialize(patch_data)
|
30
|
+
@path = patch_data.fetch('path')
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the path after being expanded by the JSON pointer semantics.
|
34
|
+
#
|
35
|
+
# @return [Array<String>] Expanded pointer path
|
36
|
+
def processed_path
|
37
|
+
ObjectPatch::Pointer.parse(@path)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Covert this operation to a format that can be built into a full on JSON
|
41
|
+
# patch.
|
42
|
+
#
|
43
|
+
# @return [Hash<String => String>] JSON patch remove operation
|
44
|
+
def to_patch
|
45
|
+
{ 'op' => 'remove', 'path' => @path }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch::Operations
|
3
|
+
|
4
|
+
# A representation of a JSON pointer replace operation.
|
5
|
+
class Replace
|
6
|
+
|
7
|
+
# Apply this operation to the provided document and return the updated
|
8
|
+
# document. Please note that the changes will be reflected not only in the
|
9
|
+
# returned value but the original document that was passed in as well.
|
10
|
+
#
|
11
|
+
# @param [Object] target_doc The document that will be modified by this
|
12
|
+
# patch.
|
13
|
+
# @return [Object] The modified document
|
14
|
+
def apply(target_doc)
|
15
|
+
return @value if processed_path.empty?
|
16
|
+
|
17
|
+
key = processed_path.last
|
18
|
+
inner_obj = ObjectPatch::Pointer.eval(processed_path[0...-1], target_doc)
|
19
|
+
|
20
|
+
if inner_obj.is_a?(Array)
|
21
|
+
raise ObjectPatch::InvalidIndexError unless key =~ /\A\d+\Z/
|
22
|
+
inner_obj[key.to_i] = @value
|
23
|
+
else
|
24
|
+
inner_obj[key] = @value
|
25
|
+
end
|
26
|
+
|
27
|
+
target_doc
|
28
|
+
end
|
29
|
+
|
30
|
+
# Setup the replace operation with any required arguments.
|
31
|
+
#
|
32
|
+
# @param [Hash] patch_data Parameters necessary to build the operation.
|
33
|
+
# @option patch_data [String] path The location in the target document to
|
34
|
+
# replace with the provided data.
|
35
|
+
# @option patch_data [String] value The value that should be written over
|
36
|
+
# whatever is at the provided path.
|
37
|
+
# @return [void]
|
38
|
+
def initialize(patch_data)
|
39
|
+
@path = patch_data.fetch('path')
|
40
|
+
@value = patch_data.fetch('value')
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the path after being expanded by the JSON pointer semantics.
|
44
|
+
#
|
45
|
+
# @return [Array<String>] Expanded pointer path
|
46
|
+
def processed_path
|
47
|
+
ObjectPatch::Pointer.parse(@path)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Covert this operation to a format that can be built into a full on JSON
|
51
|
+
# patch.
|
52
|
+
#
|
53
|
+
# @return [Hash<String => String>] JSON patch replace operation
|
54
|
+
def to_patch
|
55
|
+
{ 'op' => 'replace', 'path' => @path, 'value' => @value }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch::Operations
|
3
|
+
|
4
|
+
# An implementation of the JSON patch test operation.
|
5
|
+
class Test
|
6
|
+
|
7
|
+
# A simple test to validate the value at the expected location matches the
|
8
|
+
# value in the patch information. Will raise an error if the test fails.
|
9
|
+
#
|
10
|
+
# @param [Object] target_doc
|
11
|
+
# @return [Object] Unmodified version of the document.
|
12
|
+
def apply(target_doc)
|
13
|
+
unless @value == ObjectPatch::Pointer.eval(processed_path, target_doc)
|
14
|
+
raise ObjectPatch::FailedTestException.new(@value, @path)
|
15
|
+
end
|
16
|
+
|
17
|
+
target_doc
|
18
|
+
end
|
19
|
+
|
20
|
+
# Setup the test operation with any required arguments.
|
21
|
+
#
|
22
|
+
# @param [Hash] patch_data Parameters necessary to build the operation.
|
23
|
+
# @option patch_data [String] path The location in the target document to
|
24
|
+
# test.
|
25
|
+
# @return [void]
|
26
|
+
def initialize(patch_data)
|
27
|
+
@path = patch_data.fetch('path')
|
28
|
+
@value = patch_data.fetch('value')
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the path after being expanded by the JSON pointer semantics.
|
32
|
+
#
|
33
|
+
# @return [Array<String>] Expanded pointer path
|
34
|
+
def processed_path
|
35
|
+
ObjectPatch::Pointer.parse(@path)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Covert this operation to a format that can be built into a full on JSON
|
39
|
+
# patch.
|
40
|
+
#
|
41
|
+
# @return [Hash<String => String>] JSON patch test operation
|
42
|
+
def to_patch
|
43
|
+
{ 'op' => 'test', 'path' => @path, 'value' => @value }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,90 @@
|
|
1
|
+
|
2
|
+
module ObjectPatch
|
3
|
+
|
4
|
+
# This module contains the code to convert between an JSON pointer path
|
5
|
+
# representation and the keys required to traverse an array. It can make use
|
6
|
+
# of an a path and evaluate it against a provided (potentially deeply nested)
|
7
|
+
# array or hash.
|
8
|
+
#
|
9
|
+
# This is mostly compliant with RFC6901, however, a few small exceptions have
|
10
|
+
# been made, though they shouldn't break compatibility with pure
|
11
|
+
# implementations.
|
12
|
+
module Pointer
|
13
|
+
|
14
|
+
# Given a parsed path and an object, get the nested value within the object.
|
15
|
+
#
|
16
|
+
# @param [Array<String,Fixnum>] path Key path to traverse to get the value.
|
17
|
+
# @param [Hash,Array] obj The document to traverse.
|
18
|
+
# @return [Object] The value at the provided path.
|
19
|
+
def eval(path, obj)
|
20
|
+
path.inject(obj) do |o, p|
|
21
|
+
if o.is_a?(Hash)
|
22
|
+
raise MissingTargetException unless o.keys.include?(p)
|
23
|
+
o[p]
|
24
|
+
elsif o.is_a?(Array)
|
25
|
+
# The last element +1 is technically how this is interpretted. This
|
26
|
+
# will always trigger the index error so it may not be valuable to
|
27
|
+
# set...
|
28
|
+
p = o.size if p == "-1"
|
29
|
+
# Technically a violation of the RFC to allow reverse access to the
|
30
|
+
# array but I'll allow it...
|
31
|
+
raise ObjectOperationOnArrayException unless p.to_s.match(/\A-?\d+\Z/)
|
32
|
+
raise InvalidIndexError unless p.to_i.abs < o.size
|
33
|
+
o[p.to_i]
|
34
|
+
else
|
35
|
+
# We received a Scalar value from the prior iteration... we can't do
|
36
|
+
# anything with this...
|
37
|
+
raise TraverseScalarException
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Given an array of keys this will provide a properly escaped JSONPointer
|
43
|
+
# path.
|
44
|
+
#
|
45
|
+
# @param [Array<String,Fixnum>] ary_path
|
46
|
+
# @return [String]
|
47
|
+
def encode(ary_path)
|
48
|
+
ary_path = Array(ary_path).map { |p| p.is_a?(String) ? escape(p) : p }
|
49
|
+
"/" << ary_path.join("/")
|
50
|
+
end
|
51
|
+
|
52
|
+
# Escapes reserved characters as defined by RFC6901. This is intended to
|
53
|
+
# escape individual segments of the pointer and thus should not be run on an
|
54
|
+
# already generated path.
|
55
|
+
#
|
56
|
+
# @see [Pointer#unescape]
|
57
|
+
# @param [String] str
|
58
|
+
# @return [String]
|
59
|
+
def escape(str)
|
60
|
+
str.gsub(/~|\//, { '~' => '~0', '/' => '~1' })
|
61
|
+
end
|
62
|
+
|
63
|
+
# Convert a JSON pointer into an array of keys that can be used to traverse
|
64
|
+
# a parsed JSON document.
|
65
|
+
#
|
66
|
+
# @param [String] path
|
67
|
+
# @return [Array<String,Fixnum>]
|
68
|
+
def parse(path)
|
69
|
+
# I'm pretty sure this isn't quite valid but it's a holdover from
|
70
|
+
# tenderlove's code. Once the operations are refactored I believe this
|
71
|
+
# won't be necessary.
|
72
|
+
return [""] if path == "/"
|
73
|
+
# Strip off the leading slash
|
74
|
+
path = path.sub(/^\//, '')
|
75
|
+
path.split("/").map { |p| unescape(p) }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Unescapes any reserved characters within a JSON pointer segment.
|
79
|
+
#
|
80
|
+
# @see [Pointer#escape]
|
81
|
+
# @param [String] str
|
82
|
+
# @return [String]
|
83
|
+
def unescape(str)
|
84
|
+
str.gsub(/~[01]/, { '~0' => '~', '~1' => '/' })
|
85
|
+
end
|
86
|
+
|
87
|
+
module_function :eval, :encode, :escape, :parse, :unescape
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|