json_patch 0.2.1

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