object_patch 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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