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.
@@ -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
+
@@ -0,0 +1,4 @@
1
+
2
+ module ObjectPatch
3
+ VERSION = "0.8.1" # The current gem version.
4
+ end