object_patch 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|