json-patch 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.gitmodules +3 -0
- data/.travis.yml +9 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +82 -0
- data/Rakefile +11 -0
- data/json-patch.gemspec +23 -0
- data/lib/json/patch.rb +187 -0
- data/lib/json/patch/railtie.rb +14 -0
- data/lib/json/patch/version.rb +5 -0
- data/test/ietf_spec_test.rb +77 -0
- data/test/ietf_test.rb +71 -0
- data/test/json-patch_test.rb +355 -0
- data/test/ruby_ietf_spec_test.rb +0 -0
- data/test/test_helper.rb +11 -0
- metadata +100 -0
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Guille Carlos
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# JSON::Patch (Version 1 Coming Soon)
|
2
|
+
[![Build Status](https://travis-ci.org/guillec/json-patch.png)](https://travis-ci.org/guillec/json-patch)
|
3
|
+
[![Code Climate](https://codeclimate.com/github/guillec/json-patch.png)](https://codeclimate.com/github/guillec/json-patch)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/guillec/json-patch/badge.png)](https://coveralls.io/r/guillec/json-patch)
|
5
|
+
|
6
|
+
|
7
|
+
This gem augments Ruby's built-in JSON library to support JSON Patch
|
8
|
+
(identified by the json-patch+json media type). http://tools.ietf.org/html/rfc6902
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
gem 'json-patch'
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
|
22
|
+
$ gem install json-patch
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Then, use it:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
# The example from http://tools.ietf.org/html/rfc6902#appendix-A
|
30
|
+
|
31
|
+
# Add Object Member
|
32
|
+
target_document = <<-JSON
|
33
|
+
{ "foo": "bar"}
|
34
|
+
JSON
|
35
|
+
|
36
|
+
operations_document = <<-JSON
|
37
|
+
[
|
38
|
+
{ "op": "add", "path": "/baz", "value": "qux" }
|
39
|
+
]
|
40
|
+
JSON
|
41
|
+
|
42
|
+
JSON.patch(target_document, operations_document)
|
43
|
+
# =>
|
44
|
+
{ "baz": "qux", "foo": "bar" }
|
45
|
+
|
46
|
+
|
47
|
+
# Add Array Element
|
48
|
+
target_document = <<-JSON
|
49
|
+
{ "foo": [ "bar", "baz" ] }
|
50
|
+
JSON
|
51
|
+
|
52
|
+
operations_document = <<-JSON
|
53
|
+
[
|
54
|
+
{ "op": "add", "path": "/foo/1", "value": "qux" }
|
55
|
+
]
|
56
|
+
JSON
|
57
|
+
|
58
|
+
JSON.patch(target_document, operations_document)
|
59
|
+
# =>
|
60
|
+
{ "foo": [ "bar", "qux", "baz" ] }
|
61
|
+
```
|
62
|
+
|
63
|
+
If you'd prefer to operate on pure Ruby objects rather than JSON
|
64
|
+
strings, you can construct a JSON::Patch object instead.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
target_document = { "foo" => [ "bar", "baz" ] }
|
68
|
+
operations_document = [{ "op" => "add", "path" => "/foo/1", "value" => "qux" }]
|
69
|
+
|
70
|
+
JSON::Patch.new(target_document, operations_document).call
|
71
|
+
# =>
|
72
|
+
{ "foo" => [ "bar", "qux", "baz" ] }
|
73
|
+
```
|
74
|
+
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
|
78
|
+
1. Fork it
|
79
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
80
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
81
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
82
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/json-patch.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'json/patch/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "json-patch"
|
8
|
+
spec.version = Json::Patch::VERSION
|
9
|
+
spec.authors = ["Guille Carlos"]
|
10
|
+
spec.email = ["guille@bitpop.in"]
|
11
|
+
spec.description = %q{An implementation of RFC 6902: JSON Patch.}
|
12
|
+
spec.summary = %q{An implementation of RFC 6902: JSON Patch.}
|
13
|
+
spec.homepage = "https://github.com/guillec/json-patch"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
data/lib/json/patch.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'json/patch/railtie' if defined?(Rails)
|
3
|
+
|
4
|
+
module JSON
|
5
|
+
PatchError = Class.new(StandardError)
|
6
|
+
PatchOutOfBoundException = Class.new(StandardError)
|
7
|
+
PatchObjectOperationOnArrayException = Class.new(StandardError)
|
8
|
+
|
9
|
+
def self.patch(target_doc, operations_doc)
|
10
|
+
target_doc = JSON.parse(target_doc)
|
11
|
+
operations_doc = JSON.parse(operations_doc)
|
12
|
+
result_doc = JSON::Patch.new(target_doc, operations_doc).call
|
13
|
+
JSON.dump(result_doc)
|
14
|
+
end
|
15
|
+
|
16
|
+
class Patch
|
17
|
+
|
18
|
+
def initialize(target_doc, operations_doc)
|
19
|
+
@target_doc = target_doc
|
20
|
+
@operations_doc = operations_doc
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
return @target_doc if @operations_doc.empty?
|
25
|
+
@operations_doc.each do |operation|
|
26
|
+
operation = operation.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
27
|
+
if allowed?(operation)
|
28
|
+
@target_doc = send(operation[:op].to_sym, @target_doc, operation)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
return @target_doc
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def allowed?(operation)
|
36
|
+
operation.fetch(:op) { raise JSON::PatchError }
|
37
|
+
raise JSON::PatchError unless ["add","remove","replace","move","copy","test"].include?(operation[:op])
|
38
|
+
operation.fetch(:path) { raise JSON::PatchError }
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(target_doc, operation_doc)
|
43
|
+
path = operation_doc[:path]
|
44
|
+
value = operation_doc.fetch(:value) { raise JSON::PatchError }
|
45
|
+
|
46
|
+
add_operation(target_doc, path, value)
|
47
|
+
target_doc
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove(target_doc, operation_doc)
|
51
|
+
path = operation_doc.fetch(:path) { raise JSON::PatchError }
|
52
|
+
|
53
|
+
remove_operation(target_doc, path)
|
54
|
+
target_doc
|
55
|
+
end
|
56
|
+
|
57
|
+
def replace(target_doc, operation_doc)
|
58
|
+
remove(target_doc, operation_doc)
|
59
|
+
add(target_doc, operation_doc)
|
60
|
+
target_doc
|
61
|
+
end
|
62
|
+
|
63
|
+
def move(target_doc, operation_doc)
|
64
|
+
src = operation_doc.fetch(:from) { raise JSON::PatchError }
|
65
|
+
dest = operation_doc[:path]
|
66
|
+
value = remove_operation(target_doc, src)
|
67
|
+
|
68
|
+
add_operation(target_doc, dest, value)
|
69
|
+
target_doc
|
70
|
+
end
|
71
|
+
|
72
|
+
def copy(target_doc, operation_doc)
|
73
|
+
src = operation_doc.fetch(:from) { raise JSON::PatchError }
|
74
|
+
dest = operation_doc[:path]
|
75
|
+
value = find_value(target_doc, operation_doc, src)
|
76
|
+
|
77
|
+
add_operation(target_doc, dest, value)
|
78
|
+
target_doc
|
79
|
+
end
|
80
|
+
|
81
|
+
def test(target_doc, operation_doc)
|
82
|
+
path = operation_doc[:path]
|
83
|
+
value = find_value(target_doc, operation_doc, path)
|
84
|
+
test_value = operation_doc.fetch(:value) { raise JSON::PatchError }
|
85
|
+
|
86
|
+
raise JSON::PatchError if value != test_value
|
87
|
+
target_doc if value == test_value
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_operation(target_doc, path, value)
|
91
|
+
path_array = split_path(path)
|
92
|
+
ref_token = path_array.pop
|
93
|
+
target_item = build_target_array(path_array, target_doc)
|
94
|
+
|
95
|
+
add_array(target_doc, path_array, target_item, ref_token, value) if target_item.kind_of? Array
|
96
|
+
add_object(target_doc, target_item, ref_token, value) unless target_item.kind_of? Array
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_object(target_doc, target_item, ref_token, value)
|
100
|
+
raise JSON::PatchError if target_item.nil?
|
101
|
+
if ref_token.nil?
|
102
|
+
target_doc.replace(value)
|
103
|
+
else
|
104
|
+
target_item[ref_token] = value
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_array(doc, path_array, target_item, ref_token, value)
|
109
|
+
return unless valid_index?(target_item, ref_token)
|
110
|
+
if ref_token == "-"
|
111
|
+
new_array = target_item << value
|
112
|
+
else
|
113
|
+
new_array = target_item.insert ref_token.to_i, value
|
114
|
+
end
|
115
|
+
add_to_target_document(doc, path_array, target_item, new_array)
|
116
|
+
end
|
117
|
+
|
118
|
+
def valid_index?(item_array, index)
|
119
|
+
raise JSON::PatchObjectOperationOnArrayException unless index =~ /\A-?\d+\Z/ || index == "-"
|
120
|
+
index = index == "-" ? item_array.length : index.to_i
|
121
|
+
raise JSON::PatchOutOfBoundException if index.to_i > item_array.length || index.to_i < 0
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
def remove_operation(target_doc, path)
|
126
|
+
path_array = split_path(path)
|
127
|
+
ref_token = path_array.pop
|
128
|
+
target_item = build_target_array(path_array, target_doc)
|
129
|
+
raise JSON::PatchObjectOperationOnArrayException if target_item.nil?
|
130
|
+
|
131
|
+
if Array === target_item
|
132
|
+
target_item.delete_at ref_token.to_i if valid_index?(target_item, ref_token)
|
133
|
+
else
|
134
|
+
target_item.delete ref_token
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def find_value(target_doc, operation_doc, path)
|
139
|
+
path_array = split_path(path)
|
140
|
+
ref_token = path_array.pop
|
141
|
+
target_item = build_target_array(path_array, target_doc)
|
142
|
+
if Array === target_item
|
143
|
+
if is_a_number?(ref_token)
|
144
|
+
target_item.at ref_token.to_i
|
145
|
+
else
|
146
|
+
raise JSON::PatchObjectOperationOnArrayException
|
147
|
+
end
|
148
|
+
else
|
149
|
+
target_item[ref_token]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def is_a_number?(s)
|
154
|
+
s.to_s.match(/\A[+-]?\d+?(\.\d+)?\Z/) == nil ? false : true
|
155
|
+
end
|
156
|
+
|
157
|
+
def build_target_array(path_array, target_doc)
|
158
|
+
path_array.inject(target_doc) do |doc, item|
|
159
|
+
key = (doc.kind_of?(Array) ? item.to_i : item)
|
160
|
+
if doc.kind_of?(Array)
|
161
|
+
doc[key]
|
162
|
+
else
|
163
|
+
doc.has_key?(key) ? doc[key] : doc[key.to_sym] unless doc.kind_of?(Array)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def add_to_target_document(doc, path, target_item, array)
|
169
|
+
path.inject(doc) do |obj, part|
|
170
|
+
key = (Array === doc ? part.to_i : part)
|
171
|
+
doc[key]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def split_path(path)
|
176
|
+
escape_characters = {'^/' => '/', '^^' => '^', '~0' => '~', '~1' => '/'}
|
177
|
+
if path == '/'
|
178
|
+
['']
|
179
|
+
else
|
180
|
+
path.sub(/^\//, '').split(/(?<!\^)\//).map! { |part|
|
181
|
+
part.gsub!(/\^[\/^]|~[01]/) { |m| escape_characters[m] }
|
182
|
+
part
|
183
|
+
}
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module JSON
|
4
|
+
class Patch
|
5
|
+
# This class registers our gem with Rails.
|
6
|
+
class Railtie < ::Rails::Railtie
|
7
|
+
# When the application loads, this will cause Rails to know
|
8
|
+
# # how to serve up the proper type.
|
9
|
+
initializer 'json-patch' do
|
10
|
+
Mime::Type.register 'application/json-patch+json', :json_patch
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'json/patch'
|
3
|
+
|
4
|
+
describe "IETF JSON Patch Test" do
|
5
|
+
|
6
|
+
TESTDIR = File.dirname File.expand_path __FILE__
|
7
|
+
spec_json = File.read File.join TESTDIR, 'json-patch-tests', 'spec_tests.json'
|
8
|
+
specs = JSON.load spec_json
|
9
|
+
|
10
|
+
describe "JSON.patch" do
|
11
|
+
specs.each_with_index do |spec, index|
|
12
|
+
next unless spec['doc']
|
13
|
+
|
14
|
+
|
15
|
+
#This test I am skipping
|
16
|
+
#Because it test if a operations object has two op members
|
17
|
+
#Since the first step in the process is to convert to hash
|
18
|
+
#it gets rid of the similar keys.
|
19
|
+
next if spec['comment'] == 'A.13 Invalid JSON Patch Document'
|
20
|
+
|
21
|
+
|
22
|
+
comment = spec['comment']
|
23
|
+
unless spec['disabled']
|
24
|
+
|
25
|
+
describe "A JSON String " do
|
26
|
+
it "#{comment || spec['error'] || index}" do
|
27
|
+
|
28
|
+
target_doc = JSON.dump(spec['doc']) if spec['doc']
|
29
|
+
operations_doc = JSON.dump(spec['patch']) if spec['patch']
|
30
|
+
expected_doc = JSON.dump(spec['expected']) if spec['expected']
|
31
|
+
|
32
|
+
if spec['error']
|
33
|
+
assert_raises(ex(spec['error'])) do
|
34
|
+
JSON.patch(target_doc, operations_doc)
|
35
|
+
end
|
36
|
+
else
|
37
|
+
result_doc = JSON.patch(target_doc, operations_doc)
|
38
|
+
assert_equal JSON.parse(expected_doc || target_doc), JSON.parse(result_doc)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "JSON::Patch.new" do
|
44
|
+
it "#{comment || spec['error'] || index}" do
|
45
|
+
|
46
|
+
target_doc = eval(spec['doc'].to_s) if spec['doc']
|
47
|
+
operations_doc = eval(spec['patch'].to_s) if spec['patch']
|
48
|
+
expected_doc = eval(spec['expected'].to_s) if spec['expected']
|
49
|
+
|
50
|
+
if spec['error']
|
51
|
+
assert_raises(ex(spec['error'])) do
|
52
|
+
JSON::Patch.new(target_doc, operations_doc).call
|
53
|
+
end
|
54
|
+
else
|
55
|
+
result_doc = JSON::Patch.new(target_doc, operations_doc).call
|
56
|
+
assert_equal (expected_doc || target_doc), result_doc
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def ex msg
|
67
|
+
case msg
|
68
|
+
when /Out of bounds/i then
|
69
|
+
JSON::PatchOutOfBoundException
|
70
|
+
when /Object operation on array target/ then
|
71
|
+
JSON::PatchObjectOperationOnArrayException
|
72
|
+
else
|
73
|
+
JSON::PatchError
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
data/test/ietf_test.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'json/patch'
|
3
|
+
|
4
|
+
describe "IETF JSON Patch Test" do
|
5
|
+
|
6
|
+
TESTDIR = File.dirname File.expand_path __FILE__
|
7
|
+
spec_json = File.read File.join TESTDIR, 'json-patch-tests', 'tests.json'
|
8
|
+
specs = JSON.load spec_json
|
9
|
+
|
10
|
+
describe "Test JSON File" do
|
11
|
+
specs.each_with_index do |spec, index|
|
12
|
+
next unless spec['doc']
|
13
|
+
comment = spec['comment']
|
14
|
+
unless spec['disabled']
|
15
|
+
|
16
|
+
describe "JSON.patch" do
|
17
|
+
it "#{comment || spec['error'] || index}" do
|
18
|
+
|
19
|
+
target_doc = JSON.dump(spec['doc']) if spec['doc']
|
20
|
+
operations_doc = JSON.dump(spec['patch']) if spec['patch']
|
21
|
+
expected_doc = JSON.dump(spec['expected']) if spec['expected']
|
22
|
+
|
23
|
+
if spec['error']
|
24
|
+
assert_raises(ex(spec['error'])) do
|
25
|
+
JSON.patch(target_doc, operations_doc)
|
26
|
+
end
|
27
|
+
else
|
28
|
+
result_doc = JSON.patch(target_doc, operations_doc)
|
29
|
+
assert_equal JSON.parse(expected_doc || target_doc), JSON.parse(result_doc)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "JSON::Patch.new" do
|
35
|
+
it "#{comment || spec['error'] || index}" do
|
36
|
+
|
37
|
+
target_doc = spec['doc'] if spec['doc']
|
38
|
+
operations_doc = spec['patch'] if spec['patch']
|
39
|
+
expected_doc = spec['expected'] if spec['expected']
|
40
|
+
|
41
|
+
if spec['error']
|
42
|
+
assert_raises(ex(spec['error'])) do
|
43
|
+
JSON::Patch.new(target_doc, operations_doc).call
|
44
|
+
end
|
45
|
+
else
|
46
|
+
result_doc = JSON::Patch.new(target_doc, operations_doc).call
|
47
|
+
assert_equal (expected_doc || target_doc), result_doc
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def ex msg
|
57
|
+
case msg
|
58
|
+
when /Out of bounds/i then
|
59
|
+
JSON::PatchOutOfBoundException
|
60
|
+
when /Object operation on array target/i then
|
61
|
+
JSON::PatchObjectOperationOnArrayException
|
62
|
+
when /with bad number/i then
|
63
|
+
JSON::PatchObjectOperationOnArrayException
|
64
|
+
when /get array element 1/ then
|
65
|
+
JSON::PatchObjectOperationOnArrayException
|
66
|
+
else
|
67
|
+
JSON::PatchError
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,355 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'json/patch'
|
3
|
+
|
4
|
+
describe "Section 4: Operation objects" do
|
5
|
+
|
6
|
+
describe "Operation objects MUST have at least one 'op' member" do
|
7
|
+
let(:target_document) { %q'{}' }
|
8
|
+
let(:operation_document) { %q'[{"path":"/a/b/c"}]' }
|
9
|
+
|
10
|
+
it "will raise exception when no 'op' member exist" do
|
11
|
+
assert_raises(JSON::PatchError) do
|
12
|
+
JSON.patch(target_document, operation_document)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "Operation objects 'op' member MUST be one of the correct values" do
|
18
|
+
let(:target_document) { %q'{ "foo":["bar","baz"] }' }
|
19
|
+
let(:add_operation_document) { %q'[{"op":"add","path":"/foo/1","value":"qux"}]' }
|
20
|
+
let(:remove_operation_document) { %q'[{ "op": "remove", "path": "/baz" }]' }
|
21
|
+
let(:replace_operation_document) { %q'[{"op":"replace","path":"/foo/1","value":"qux"}]' }
|
22
|
+
let(:move_operation_document) { %q'[{"op":"replace","from":"foo","path":"/foo/1","value":"qux"}]' }
|
23
|
+
let(:copy_operation_document) { %q'[{"op":"replace","from":"foo","path":"/foo/1","value":"qux"}]' }
|
24
|
+
let(:test_operation_document) { %q'[{"op":"test", "path":"/foo/1","value":"baz"}]' }
|
25
|
+
let(:error_operation_document) { %q'[{"op": "hammer time"}]' }
|
26
|
+
|
27
|
+
it "can contain a 'add' value" do
|
28
|
+
assert JSON.patch(target_document, add_operation_document)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can contain a 'remove' value" do
|
32
|
+
assert JSON.patch(target_document, remove_operation_document)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can contain a 'replace' value" do
|
36
|
+
assert JSON.patch(target_document, replace_operation_document)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can contain a 'move' value" do
|
40
|
+
assert JSON.patch(target_document, move_operation_document)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "can contain a 'copy' value" do
|
44
|
+
assert JSON.patch(target_document, copy_operation_document)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can contain a 'test' value" do
|
48
|
+
assert JSON.patch(target_document, test_operation_document)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "will raise exception when 'op' member contains invalid 'hammer time' value" do
|
52
|
+
assert_raises(JSON::PatchError) do
|
53
|
+
JSON.patch(target_document, error_operation_document)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "Operation objects MUST have at least one path member" do
|
59
|
+
let(:target_document) { %q'{ "foo":["bar","baz"] }' }
|
60
|
+
let(:operation_document) { %q'[{"op":"add", "value":"qux"}]' }
|
61
|
+
|
62
|
+
it "will raise exception when no 'path' member exist" do
|
63
|
+
assert_raises(JSON::PatchError) do
|
64
|
+
JSON.patch(target_document, operation_document)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "Operation members not define by the action MUST be ignored" do
|
70
|
+
let(:target_document) { %q'{ "foo":["bar","baz"] }' }
|
71
|
+
let(:operation_document) { %q'[{"op":"add","path":"/foo/1","value":"qux", "ignore":"This please"}]' }
|
72
|
+
|
73
|
+
it "ignores the 'ignore' member of the add operation_document" do
|
74
|
+
expected = %q'{"foo":["bar","qux","baz"]}'
|
75
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "Section 4.1: The add operation" do
|
82
|
+
|
83
|
+
describe "If the target location specifies an array index" do
|
84
|
+
let(:target_document) { %q'{ "foo":["bar","baz"] }' }
|
85
|
+
let(:operation_document) { %q'[{"op":"add","path":"/foo/1","value":"qux"}]' }
|
86
|
+
|
87
|
+
it "inserts the value into the array at specified index" do
|
88
|
+
expected = %q'{"foo":["bar","qux","baz"]}'
|
89
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "If the target location species a object member that does not exist" do
|
94
|
+
let(:target_document) { %q'{"foo":"bar"}' }
|
95
|
+
let(:operation_document) { %q'[{ "op": "add", "path": "/baz", "value": "qux" }]' }
|
96
|
+
|
97
|
+
it "it will add the object to the target_document" do
|
98
|
+
expected = %q'{"foo":"bar","baz":"qux"}'
|
99
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "If the target location species a member that does exist" do
|
104
|
+
let(:target_document) { %q'{"foo":"bar","baz":"wat"}' }
|
105
|
+
let(:operation_document) { %q'[{ "op": "add", "path": "/baz", "value": "qux" }]' }
|
106
|
+
|
107
|
+
it "it replaces the value" do
|
108
|
+
expected = %q'{"foo":"bar","baz":"qux"}'
|
109
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "The add operation MUST contina a 'value' member" do
|
114
|
+
let(:target_document) { %q'{"foo":"bar","baz":"wat"}' }
|
115
|
+
let(:operation_document) { %q'[{ "op": "add", "path": "/baz" }]' }
|
116
|
+
|
117
|
+
it "will raise exception if no 'value' member" do
|
118
|
+
assert_raises(JSON::PatchError) do
|
119
|
+
JSON.patch(target_document, operation_document)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
=begin
|
125
|
+
TODO
|
126
|
+
When the operation is applied, the target location MUST reference one of:
|
127
|
+
|
128
|
+
1. The root of the target document - whereupon the specified value
|
129
|
+
becomes the entire content of the target document.
|
130
|
+
|
131
|
+
2. A member to add to an existing object - whereupon the supplied
|
132
|
+
value is added to that object at the indicated location. If the
|
133
|
+
member already exists, it is replaced by the specified value.
|
134
|
+
|
135
|
+
3. An element to add to an existing array - whereupon the supplied
|
136
|
+
value is added to the array at the indicated location. Any
|
137
|
+
elements at or above the specified index are shifted one position
|
138
|
+
to the right. The specified index MUST NOT be greater than the
|
139
|
+
number of elements in the array. If the "-" character is used to
|
140
|
+
index the end of the array (see [RFC6901]), this has the effect of
|
141
|
+
appending the value to the array.
|
142
|
+
=end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "Section 4.2: The remove operation" do
|
147
|
+
|
148
|
+
describe "Removing a object member" do
|
149
|
+
let(:target_document) { %q'{"foo":"bar","baz":"qux"}' }
|
150
|
+
let(:operation_document) { %q'[{ "op": "remove", "path": "/baz" }]' }
|
151
|
+
|
152
|
+
it "will remove memeber of object at the target location" do
|
153
|
+
expected = %q'{"foo":"bar"}'
|
154
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "Removing a array element" do
|
159
|
+
let(:target_document) { %q'{"foo":["bar","qux","baz"]}' }
|
160
|
+
let(:operation_document) { %q'[{ "op": "remove", "path": "/foo/1" }]' }
|
161
|
+
|
162
|
+
it "will remove object in array at the target location" do
|
163
|
+
expected = %q'{"foo":["bar","baz"]}'
|
164
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe "Target location MUST exist for the remove operation" do
|
169
|
+
let(:target_document) { %q'{"foo":["bar","qux","baz"]}' }
|
170
|
+
let(:operation_document) { %q'[{ "op": "remove"}]' }
|
171
|
+
|
172
|
+
it "will raise an exception if no target is specified" do
|
173
|
+
assert_raises(JSON::PatchError) do
|
174
|
+
JSON.patch(target_document, operation_document)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
describe "Section 4.3: The replace operation" do
|
182
|
+
|
183
|
+
describe "Replacing a value" do
|
184
|
+
let(:target_document) { %q'{"foo":"bar","baz":"qux"}' }
|
185
|
+
let(:operation_document) { %q'[{ "op": "replace", "path": "/baz", "value": "boo" }]' }
|
186
|
+
|
187
|
+
it "will replace old value with a new value at target location" do
|
188
|
+
expected = %q'{"foo":"bar","baz":"boo"}'
|
189
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "The replace operation document MUST contain a 'value' member" do
|
194
|
+
let(:target_document) { %q'{"foo":"bar","baz":"qux"}' }
|
195
|
+
let(:operation_document) { %q'[{ "op": "replace", "path": "/baz" }]' }
|
196
|
+
|
197
|
+
it "will raise an exception if no 'value' is specified" do
|
198
|
+
assert_raises(JSON::PatchError) do
|
199
|
+
JSON.patch(target_document, operation_document)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe "The replace operation MUST have a target location" do
|
205
|
+
let(:target_document) { %q'{"foo":"bar","baz":"qux"}' }
|
206
|
+
let(:operation_document) { %q'[{ "op": "replace", "value": "boo" }]' }
|
207
|
+
|
208
|
+
it "will raise an exception if no target is specified" do
|
209
|
+
assert_raises(JSON::PatchError) do
|
210
|
+
JSON.patch(target_document, operation_document)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe "Section 4.4: The move operation" do
|
217
|
+
|
218
|
+
describe "The move operation" do
|
219
|
+
let(:target_document) { %q'{"foo":{"bar":"baz","waldo":"fred"},"qux":{"corge":"grault"}}' }
|
220
|
+
let(:operation_document) { %q'[{ "op": "move", "from":"/foo/waldo", "path": "/qux/thud" }]' }
|
221
|
+
|
222
|
+
it "will remove the value at a specified location and add it to the target location" do
|
223
|
+
expected = %q'{"foo":{"bar":"baz"},"qux":{"corge":"grault","thud":"fred"}}'
|
224
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe "The move operation" do
|
229
|
+
let(:target_document) { %q'{"foo":["add","grass","cows","eat"]}' }
|
230
|
+
let(:operation_document) { %q'[{ "op": "move", "from":"/foo/1", "path": "/foo/3" }]' }
|
231
|
+
|
232
|
+
it "will move a array element to new location" do
|
233
|
+
expected = %q'{"foo":["add","cows","eat","grass"]}'
|
234
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe "The move operation MUST hav a 'from' memeber" do
|
239
|
+
let(:target_document) { %q'{"foo":"bar","baz":"qux"}' }
|
240
|
+
let(:operation_document) { %q'[{ "op": "move", "value": "boo" }]' }
|
241
|
+
|
242
|
+
it "will raise an exception if no from 'from' location is specified" do
|
243
|
+
assert_raises(JSON::PatchError) do
|
244
|
+
JSON.patch(target_document, operation_document)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
=begin
|
250
|
+
TODO The "from" location MUST NOT be a proper prefix of the "path"
|
251
|
+
location; i.e., a location cannot be moved into one of its children.
|
252
|
+
=end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
describe "Section 4.5: The copy operation" do
|
257
|
+
|
258
|
+
describe "The copy operation" do
|
259
|
+
let(:target_document) { %q'{"foo":{"bar":"baz","waldo":"fred"},"qux":{"corge":"grault"}}' }
|
260
|
+
let(:operation_document) { %q'[{ "op": "copy", "from":"/foo/waldo", "path": "/qux/waldo" }]' }
|
261
|
+
|
262
|
+
it "will copy a value from a specified location to the target location" do
|
263
|
+
expected = %q'{"foo":{"bar":"baz","waldo":"fred"},"qux":{"corge":"grault","waldo":"fred"}}'
|
264
|
+
assert_equal expected, JSON.patch(target_document, operation_document)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
describe "The copy operation MUST have a 'from' member" do
|
269
|
+
let(:target_document) { %q'{"foo":"bar","baz":"qux"}' }
|
270
|
+
let(:operation_document) { %q'[{ "op": "copy", "path": "/foo" }]' }
|
271
|
+
|
272
|
+
it "will raise an exception if no 'from' location is specified" do
|
273
|
+
assert_raises(JSON::PatchError) do
|
274
|
+
JSON.patch(target_document, operation_document)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
280
|
+
|
281
|
+
describe "Section 4.6: The test operation" do
|
282
|
+
|
283
|
+
#The "test" operation tests that a value at the target location is equal to a specified value.
|
284
|
+
|
285
|
+
describe "The test operation MUST contain a 'value' member" do
|
286
|
+
let(:target_document) { %q'{"baz":"qux","foo":["a",2,"c"]}' }
|
287
|
+
let(:operation_document) { %q'[{ "op": "test", "path": "/baz"}, {"op": "test", "path": "/foo/1"}]' }
|
288
|
+
|
289
|
+
it "will raise a exception if no 'value' is specified" do
|
290
|
+
assert_raises(JSON::PatchError) do
|
291
|
+
JSON.patch(target_document, operation_document)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
describe "Testing that strings have the same number of Unicode characters and their code points are byte-to-byte equal" do
|
297
|
+
let(:target_document) { %q'{"baz":"qux","foo":["a",2,"c"]}' }
|
298
|
+
let(:operation_document) { %q'[{ "op": "test", "path": "/baz", "value": "qux"}]' }
|
299
|
+
|
300
|
+
it "will return true since the strings are equal" do
|
301
|
+
assert JSON.patch(target_document, operation_document)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
describe "Testing that numbers are equal if their values are numerically equal" do
|
306
|
+
let(:target_document) { %q'{"baz": 1,"foo":["a",2,"c"]}' }
|
307
|
+
let(:operation_document) { %q'[{ "op": "test", "path": "/baz", "value": 1}]' }
|
308
|
+
|
309
|
+
it "will return true since the numbers are equal" do
|
310
|
+
assert JSON.patch(target_document, operation_document)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe "Testing that arrays are equal if they contain then same number of values and these values are equal" do
|
315
|
+
let(:target_document) { %q'{"baz": 1,"foo":["a",2,"c"]}' }
|
316
|
+
let(:operation_document) { %q'[{"op": "test", "path": "/foo", "value": ["a",2,"c"]}]' }
|
317
|
+
|
318
|
+
it "will return true since arrays and values are equal" do
|
319
|
+
assert JSON.patch(target_document, operation_document)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
describe "Testing that objects are equal if they contain then same number of members and each member has same keys and values" do
|
324
|
+
let(:target_document) { %q'{"baz": 1,"foo":{"foo": "bar","hammer": "time"}}' }
|
325
|
+
let(:operation_document) { %q'[{"op": "test", "path": "/foo", "value": {"foo": "bar", "hammer":"time"}}]' }
|
326
|
+
|
327
|
+
it "will return true since objects equal" do
|
328
|
+
assert JSON.patch(target_document, operation_document)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
=begin
|
333
|
+
TODO
|
334
|
+
5 literals (false, true, and null): are considered equal if they are
|
335
|
+
the same.
|
336
|
+
|
337
|
+
Also, note that ordering of the serialization of object members is
|
338
|
+
not significant.
|
339
|
+
=end
|
340
|
+
|
341
|
+
end
|
342
|
+
|
343
|
+
describe "JSON::Patch object" do
|
344
|
+
|
345
|
+
describe "JSON::Patch.new " do
|
346
|
+
let(:target_document) { {"foo" => { "bar" => "baz", "waldo" => "fred" }, "qux" => { "corge" => "grault" } } }
|
347
|
+
let(:operation_document) { [{ "op"=> "copy", "from" => "/foo/waldo", "path" => "/qux/waldo" }] }
|
348
|
+
|
349
|
+
it "can handle plain ruby objects" do
|
350
|
+
expected = {"foo"=>{"bar"=>"baz","waldo"=>"fred"},"qux"=>{"corge"=>"grault","waldo"=>"fred"}}
|
351
|
+
assert_equal expected, JSON::Patch.new(target_document, operation_document).call
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
File without changes
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json-patch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Guille Carlos
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: ! 'An implementation of RFC 6902: JSON Patch.'
|
47
|
+
email:
|
48
|
+
- guille@bitpop.in
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- .gitmodules
|
55
|
+
- .travis.yml
|
56
|
+
- Gemfile
|
57
|
+
- LICENSE.txt
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- json-patch.gemspec
|
61
|
+
- lib/json/patch.rb
|
62
|
+
- lib/json/patch/railtie.rb
|
63
|
+
- lib/json/patch/version.rb
|
64
|
+
- test/ietf_spec_test.rb
|
65
|
+
- test/ietf_test.rb
|
66
|
+
- test/json-patch_test.rb
|
67
|
+
- test/ruby_ietf_spec_test.rb
|
68
|
+
- test/test_helper.rb
|
69
|
+
homepage: https://github.com/guillec/json-patch
|
70
|
+
licenses:
|
71
|
+
- MIT
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.8.25
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: ! 'An implementation of RFC 6902: JSON Patch.'
|
94
|
+
test_files:
|
95
|
+
- test/ietf_spec_test.rb
|
96
|
+
- test/ietf_test.rb
|
97
|
+
- test/json-patch_test.rb
|
98
|
+
- test/ruby_ietf_spec_test.rb
|
99
|
+
- test/test_helper.rb
|
100
|
+
has_rdoc:
|