json_patch 0.2.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,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use "ruby-1.9.2-p180@json_patch"
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -0,0 +1,10 @@
1
+ # json_patch
2
+
3
+ An implementation of the JSON patch spec.
4
+
5
+ http://tools.ietf.org/html/draft-pbryan-json-patch-01
6
+
7
+
8
+ Utilities for applying JSON patches to arbitary objects. To
9
+ participate in the patch protocol, classes can implement #apply_patch
10
+
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task default: :spec
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "json/patch/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "json_patch"
7
+ s.version = JSON::Patch::VERSION
8
+ s.authors = ["Travis Vachon"]
9
+ s.email = ["travis@copious.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{JSON patch implementation in ruby}
12
+ s.description = %q{An implementation of JSON patch in Ruby.
13
+
14
+ http://tools.ietf.org/html/draft-pbryan-json-patch-01
15
+
16
+ Utilities for applying JSON patches to arbitary objects. To
17
+ participate in the patch protocol, classes can implement #apply_patch
18
+ }
19
+
20
+ s.rubyforge_project = "json_patch"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
24
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.require_paths = ["lib"]
26
+
27
+ s.add_development_dependency "mocha"
28
+ s.add_development_dependency "rspec"
29
+ s.add_development_dependency "bson_ext"
30
+ s.add_development_dependency "mongoid"
31
+ s.add_development_dependency "gemfury"
32
+ end
@@ -0,0 +1,69 @@
1
+ require "json/patch/version"
2
+
3
+ module JSON
4
+ class Patch
5
+ attr_reader :hunks
6
+
7
+ def initialize(hunks)
8
+ @hunks = hunks.map {|h| Hunk.new(h)}
9
+ end
10
+
11
+ # Apply this patch to an object
12
+ #
13
+ # The object to be modified (which may be nested inside the object
14
+ # in the method signature) MUST implement #apply_patch.
15
+ #
16
+ # #apply_patch should return true if the patch can be succesfully
17
+ # applied and false otherwise.
18
+ #
19
+ # If any hunk in the patch cannot be applied successfully, the
20
+ # PATCH rfc dictates that the entire patch MUST not be
21
+ # applied. This atomicity must be handled by the #apply_patch
22
+ # implementation.
23
+ def apply_to(object)
24
+ object.apply_patch(self)# if object.respond_to?(:apply_patch)
25
+ end
26
+ end
27
+
28
+ class Hunk
29
+ attr_reader :op, :path, :value
30
+
31
+ def initialize(attributes={})
32
+ op = case
33
+ when attributes.has_key?('add') then 'add'
34
+ when attributes.has_key?('remove') then 'remove'
35
+ when attributes.has_key?('replace') then 'replace'
36
+ end
37
+ if op
38
+ @op = op.to_sym
39
+ else
40
+ raise ArgumentError
41
+ end
42
+ self.path = attributes[op]
43
+ @value = attributes['value']
44
+ end
45
+
46
+ def path=(path_string)
47
+ raise ArgumentError if path_string and not path_string[0] == '/'
48
+ path_string ||= ''
49
+ @path = path_string.split('/').drop(1).map {|section| section.to_sym}
50
+ end
51
+
52
+ # Given a root object, resolve the paths in this patch.
53
+ # Return a tuple of the object and element to be modified.
54
+ #
55
+ # For example (from the specs):
56
+ #
57
+ # OpenStruct.new(:foo => 1, :bar => OpenStruct.new(:baz => 2))
58
+ # JSON::Hunk.new('add' => '/bar/baz').resolve_path(obj).should == [obj.bar, :baz]
59
+ def resolve_path(object)
60
+ path_to_element = path[0..-2]
61
+ element = path[-1]
62
+ [path_to_element.inject(object) {|o, e| o.send(e) }, element]
63
+ end
64
+
65
+ def ==(other)
66
+ op == other.op and path == other.path and value = other.value
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,5 @@
1
+ module JSON
2
+ class Patch
3
+ VERSION = "0.2.1"
4
+ end
5
+ end
@@ -0,0 +1,110 @@
1
+ module Mongoid
2
+ module Patchable
3
+ # Apply a patch this this document.
4
+ #
5
+ # Currently assumes add_to_set and set operations will succeed.
6
+ #
7
+ # TODO: is there a better way to ensure atomicity of multiple
8
+ # operations while avoiding race conditions from multiple clients?
9
+ #
10
+ # it looks like
11
+ #
12
+ # Preferences.collection.update({'user_id' => 1}, {'$addToSet' => {'follow_suggest_blacklist' => 4}, '$set' => {'hams' => 2}})
13
+ #
14
+ # would do this, but a) it needs to deal with multiple hunks
15
+ # acting on the same attribute and b) it looks like mongoid
16
+ # doesn't support this ootb. more research needed...
17
+ #
18
+ # TODO: handle numeric path segments
19
+ def apply_patch(patch)
20
+ # compile operation information for verification
21
+ ops = patch.hunks.map do |hunk|
22
+ (obj, element) = hunk.resolve_path(self)
23
+ [hunk, obj, element, obj.fields[element.to_s]]
24
+ end
25
+
26
+ if patch_ops_valid?(ops)
27
+ # if something goes wrong here, raise an error to let the client
28
+ # know the patch may be partially applied
29
+ ops.each_with_index do |(hunk, obj, element, field), index|
30
+ value = hunk.value
31
+ case hunk.op
32
+ when :add
33
+ process_add(obj, field, element, value)
34
+ when :replace
35
+ process_replace(obj, field, element, value)
36
+ when :remove
37
+ process_remove(obj, field, element, value)
38
+ else
39
+ raise "Illegal operation #{hunk.op} in hunk #{index}"
40
+ end
41
+ end
42
+ true
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ def process_add(obj, field, element, value)
49
+ case field.type.name
50
+ when 'Array'
51
+ obj.add_to_set(element, (value.is_a?(Array) ? {'$each' => value} : value))
52
+ when 'Hash'
53
+ (key, val) = destructure_hash_value(value)
54
+ obj.send("#{element}=", {}) if obj.send(element).nil?
55
+ obj.send(element)[key] = val
56
+ obj.save(validate: false)
57
+ else
58
+ obj.write_attribute(element, value)
59
+ obj.save
60
+ end
61
+ end
62
+
63
+ def process_replace(obj, field, element, value)
64
+ case field.type.name
65
+ when 'Array'
66
+ obj.add_to_set(element, (value.is_a?(Array) ? {'$each' => value} : value))
67
+ when 'Hash'
68
+ (key, val) = destructure_hash_value(value)
69
+ obj.send("#{element}=", {}) if obj.send(element).nil?
70
+ obj.send(element)[key] = val
71
+ obj.save(validate: false)
72
+ else
73
+ obj.write_attribute(element, value)
74
+ obj.save
75
+ end
76
+ end
77
+
78
+ def process_remove(obj, field, element, value = nil)
79
+ case field.type.name
80
+ when 'Array'
81
+ obj.pull_all(element, (value.is_a?(Array) ? value : [value]))
82
+ when 'Hash'
83
+ (key, val) = destructure_hash_value(value)
84
+ if obj.send(element).nil?
85
+ obj.send("#{element}=", {})
86
+ else
87
+ obj.send(element).delete(key)
88
+ end
89
+ obj.save(validate: false)
90
+ else
91
+ obj.write_attribute(element, nil)
92
+ obj.save
93
+ end
94
+ end
95
+
96
+ def patch_ops_valid?(ops)
97
+ ops.inject(true) do |valid, (hunk, obj, element, field)|
98
+ return false unless valid && hunk && obj && element && field
99
+ return !hunk.value.nil? if [:add, :replace].include?(hunk.op)
100
+ return true if hunk.op == :remove
101
+ false
102
+ end
103
+ end
104
+
105
+ def destructure_hash_value(value)
106
+ (key, value) = value.split(/\=/, 2)
107
+ key.ends_with?('[]') ? [key.slice(0..-3), value.split(',')] : [key, value]
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+ require 'ostruct'
3
+
4
+ def os(a); OpenStruct.new(a) end
5
+
6
+ describe JSON::Patch do
7
+ let(:hunk) { { 'add' => '/foo/bar', 'value' => 'hams'} }
8
+ subject {JSON::Patch.new([hunk])}
9
+
10
+ describe '#initialize' do
11
+ its(:hunks) { should eq([JSON::Hunk.new(hunk)])}
12
+ end
13
+
14
+ describe '#apply_to' do
15
+ let(:obj) { os(:foo => 1, :bar => os(:baz => 2)) }
16
+ it 'should call apply_patch on the target' do
17
+ obj.expects(:apply_patch).with(subject).returns(true)
18
+ subject.apply_to(obj).should == true
19
+ end
20
+ end
21
+ end
22
+
23
+ describe JSON::Hunk do
24
+ subject {JSON::Hunk.new('add' => '/foo/bar', 'value' => 'hams')}
25
+
26
+ describe "#initialize" do
27
+ its(:op) { should eq(:add)}
28
+ its(:path) { should eq([:foo, :bar])}
29
+ its(:value) { should eq('hams')}
30
+ end
31
+
32
+ describe '#path=' do
33
+ it 'should split paths' do
34
+ subject.path = '/foo/bar'
35
+ subject.path.should == [:foo, :bar]
36
+ end
37
+
38
+ it 'should set paths to an empty list when given nil' do
39
+ subject.path = nil
40
+ subject.path.should == []
41
+ end
42
+
43
+ it 'should raise an argument error if path without starting slash is passed' do
44
+ lambda { subject.path = 'foo/bar' }.should raise_error(ArgumentError)
45
+ end
46
+ end
47
+
48
+ describe '#resolve_path' do
49
+ let(:obj) { os(:foo => 1, :bar => os(:baz => 2)) }
50
+ it "should resolve paths" do
51
+ JSON::Hunk.new('add' => '/foo').resolve_path(obj).should == [obj, :foo]
52
+ JSON::Hunk.new('add' => '/bar/baz').resolve_path(obj).should == [obj.bar, :baz]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+ require 'mongoid'
3
+ require 'mongoid/patchable'
4
+ require 'json/patch'
5
+
6
+ class PatchableDoc
7
+ include Mongoid::Document
8
+ include Mongoid::Patchable
9
+ field :foo, type: Array
10
+ field :bar, type: Hash
11
+ field :baz, type: Integer
12
+ end
13
+
14
+ describe Mongoid::Patchable do
15
+ subject { PatchableDoc.new }
16
+
17
+ describe "#apply_patch" do
18
+ it 'should call process_add' do
19
+ subject.expects(:process_add).with(subject, subject.fields['foo'], :foo, 12345)
20
+ subject.apply_patch(JSON::Patch.new([{ 'add' => '/foo', 'value' => 12345}])).should be_true
21
+ end
22
+
23
+ it 'should call process_replace' do
24
+ subject.expects(:process_replace).with(subject, subject.fields['foo'], :foo, 12345)
25
+ subject.apply_patch(JSON::Patch.new([{ 'replace' => '/foo', 'value' => 12345}])).should be_true
26
+ end
27
+
28
+ it 'should call process_remove' do
29
+ subject.expects(:process_remove).with(subject, subject.fields['foo'], :foo, nil)
30
+ subject.apply_patch(JSON::Patch.new([{ 'remove' => '/foo'}])).should be_true
31
+ end
32
+ end
33
+
34
+ describe "#process_add" do
35
+ it "should add a scalar element to an array" do
36
+ subject.expects(:add_to_set).with(:foo, 12345)
37
+ subject.send(:process_add, subject, subject.fields['foo'], :foo, 12345)
38
+ end
39
+
40
+ it "should add an array element to an array" do
41
+ subject.expects(:add_to_set).with(:foo, {'$each' => [12345]})
42
+ subject.send(:process_add, subject, subject.fields['foo'], :foo, [12345])
43
+ end
44
+
45
+ it "should add an element to a hash" do
46
+ subject.expects(:save)
47
+ subject.send(:process_add, subject, subject.fields['bar'], :bar, "rilo=kiley")
48
+ subject.bar.should == {'rilo' => 'kiley'}
49
+ end
50
+
51
+ it "should set a scalar" do
52
+ subject.expects(:write_attribute).with(:baz, 12345)
53
+ subject.expects(:save)
54
+ subject.send(:process_add, subject, subject.fields['baz'], :baz, 12345)
55
+ end
56
+ end
57
+
58
+ describe "#process_replace" do
59
+ it "should replace a scalar element in an array" do
60
+ subject.expects(:add_to_set).with(:foo, 12345)
61
+ subject.send(:process_replace, subject, subject.fields['foo'], :foo, 12345)
62
+ end
63
+
64
+ it "should replace an array element in an array" do
65
+ subject.expects(:add_to_set).with(:foo, {'$each' => [12345]})
66
+ subject.send(:process_replace, subject, subject.fields['foo'], :foo, [12345])
67
+ end
68
+
69
+ it "should replace an element in a hash" do
70
+ subject.bar = {'rilo' => 'yelik'}
71
+ subject.expects(:save)
72
+ subject.send(:process_replace, subject, subject.fields['bar'], :bar, "rilo=kiley")
73
+ subject.bar.should == {'rilo' => 'kiley'}
74
+ end
75
+
76
+ it "should replace a scalar" do
77
+ subject.baz = 54321
78
+ subject.expects(:write_attribute).with(:baz, 12345)
79
+ subject.expects(:save)
80
+ subject.send(:process_replace, subject, subject.fields['baz'], :baz, 12345)
81
+ end
82
+ end
83
+
84
+ describe "#process_remove" do
85
+ it "should remove a scalar element from an array" do
86
+ subject.expects(:pull_all).with(:foo, [12345])
87
+ subject.send(:process_remove, subject, subject.fields['foo'], :foo, 12345)
88
+ end
89
+
90
+ it "should remove a array element from an array" do
91
+ subject.expects(:pull_all).with(:foo, [12345])
92
+ subject.send(:process_remove, subject, subject.fields['foo'], :foo, [12345])
93
+ end
94
+
95
+ it "should remove an element from a hash" do
96
+ subject.bar = {'rilo' => 'kiley'}
97
+ subject.expects(:save)
98
+ subject.send(:process_remove, subject, subject.fields['bar'], :bar, "rilo")
99
+ subject.bar.should == {}
100
+ end
101
+
102
+ it "should nil a scalar" do
103
+ subject.baz = 54321
104
+ subject.expects(:write_attribute).with(:baz, nil)
105
+ subject.expects(:save)
106
+ subject.send(:process_remove, subject, subject.fields['baz'], :baz)
107
+ end
108
+ end
109
+
110
+ describe "#destructure_hash_value" do
111
+ it "should return array of key and value" do
112
+ subject.send(:destructure_hash_value, "foo=bar").should == ['foo', 'bar']
113
+ end
114
+
115
+ it "should return removed square brackets from key and array from csv string" do
116
+ subject.send(:destructure_hash_value, "foo[]=bar,baz").should == ['foo', ['bar', 'baz']]
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.setup
5
+
6
+ require 'rspec'
7
+ require 'mocha'
8
+ require 'json/patch'
9
+
10
+ RSpec.configure do |config|
11
+ config.mock_with :mocha
12
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_patch
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.1
6
+ platform: ruby
7
+ authors:
8
+ - Travis Vachon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-01-07 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mocha
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :development
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :development
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: bson_ext
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: mongoid
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ type: :development
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: gemfury
61
+ prerelease: false
62
+ requirement: &id005 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ type: :development
69
+ version_requirements: *id005
70
+ description: |
71
+ An implementation of JSON patch in Ruby.
72
+
73
+ http://tools.ietf.org/html/draft-pbryan-json-patch-01
74
+
75
+ Utilities for applying JSON patches to arbitary objects. To
76
+ participate in the patch protocol, classes can implement #apply_patch
77
+
78
+ email:
79
+ - travis@copious.com
80
+ executables: []
81
+
82
+ extensions: []
83
+
84
+ extra_rdoc_files: []
85
+
86
+ files:
87
+ - .gitignore
88
+ - .rspec
89
+ - .rvmrc
90
+ - Gemfile
91
+ - README.md
92
+ - Rakefile
93
+ - json_patch.gemspec
94
+ - lib/json/patch.rb
95
+ - lib/json/patch/version.rb
96
+ - lib/mongoid/patchable.rb
97
+ - spec/json/patch_spec.rb
98
+ - spec/mongoid/patchable_spec.rb
99
+ - spec/spec_helper.rb
100
+ homepage: ""
101
+ licenses: []
102
+
103
+ post_install_message:
104
+ rdoc_options: []
105
+
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: "0"
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: "0"
120
+ requirements: []
121
+
122
+ rubyforge_project: json_patch
123
+ rubygems_version: 1.8.11
124
+ signing_key:
125
+ specification_version: 3
126
+ summary: JSON patch implementation in ruby
127
+ test_files:
128
+ - spec/json/patch_spec.rb
129
+ - spec/mongoid/patchable_spec.rb
130
+ - spec/spec_helper.rb