diffable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in diffable.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Liz Conlan
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.
@@ -0,0 +1,112 @@
1
+ # Diffable
2
+
3
+ Diffable provides a mixin that can be used to extend any ActiveRecord object to provide diff
4
+ functionality. Calling the diff method compares the receiver against another
5
+ object and returns a Hash of differences found (presented as a description of
6
+ the changes between the second object and the receiver - as if trying to restore
7
+ the calling object from its replacement).
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'diffable'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install diffable
22
+
23
+ ## Usage
24
+
25
+ Require the gem in the source file that includes the relevant model code:
26
+
27
+ require 'diffable'
28
+
29
+ Use the include statement to add Diffable support to each model that needs it:
30
+
31
+ class ModelA < ActiveRecord::Base
32
+ include Diffable
33
+ end
34
+
35
+ You can then call the `diff` method on any instance of that model class:
36
+
37
+ object1 = ModelA.new(:name => "test")
38
+ object2 = ModelA.new(:name => "test2")
39
+ difference = object1.diff(object2)
40
+
41
+ ## Behaviour
42
+
43
+ There are 3 different types of change that can be returned by the diff method:
44
+ modified, new and deleted. These are indicated using a `:change_type`
45
+ key/value within the results Hash.
46
+
47
+ When an object is flagged as **modified**, its identifier and any of the altered
48
+ fields are returned, e.g.:
49
+
50
+ {:change_type => "modified", :id => 42, :name => "test1"}
51
+
52
+ When an object is flagged as **new**, only its identifier is returned. Sample output:
53
+
54
+ {:change_type => "new", :id => 42}
55
+
56
+ When an object is flagged as **deleted**, all of its attributes are returned in
57
+ the diff Hash. Sample output:
58
+
59
+ {:change_type => "deleted", :id => 42, :name => "test", :desc => "db test"}
60
+
61
+ ### Excluding fields
62
+
63
+ If there are any database fields that should not be returned as part of
64
+ a **deleted** object's data, they can be excluded at the model level using
65
+ `set_excluded_fields`:
66
+
67
+ class ModelB < ActiveRecord::Base
68
+ include Diffable
69
+ set_excluded_fields :ignore_me
70
+ end
71
+
72
+ ### Conditional fields
73
+
74
+ If a field value should always be included as part of a modified object's
75
+ data, this can be set at the model level using `set_conditional_fields`:
76
+
77
+ class ModelC < ActiveRecord::Base
78
+ include Diffable
79
+ set_conditional_fields :metadata, :history
80
+ end
81
+
82
+ ### Using with related tables
83
+
84
+ If your model uses `has_many` or `has_one`, the changes to these dependent
85
+ objects can also be captured by the diff method, provided that their model
86
+ definitions also use the `include Diffable` statement (otherwise they will
87
+ be ignored). However, you will also need to inform the model which field can
88
+ be used to uniquely identify records within the set returned for a particular
89
+ parent object using `set_unique_within_group`. This should be a generated
90
+ field as `:id` is unlikely to be suitable.
91
+
92
+ class ModelD < ActiveRecord::Base
93
+ include Diffable
94
+ has_many :catalogue_entries
95
+ end
96
+
97
+ class CatalogueEntries < ActiveRecord::Base
98
+ include Diffable
99
+ belongs_to :model_d
100
+ set_unique_within_group :generated_identifier
101
+
102
+ ...
103
+
104
+ end
105
+
106
+ ## Contributing
107
+
108
+ 1. Fork it
109
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
110
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
111
+ 4. Push to the branch (`git push origin my-new-feature`)
112
+ 5. Create new Pull Request
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
8
+
9
+ desc "Run the rake spec task"
10
+ task :test => [:spec]
11
+
12
+ task :default => :test
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'diffable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "diffable"
8
+ spec.version = Diffable::VERSION
9
+ spec.authors = ["Liz Conlan"]
10
+ spec.email = ["lizconlan@gmail.com"]
11
+ spec.description = %q{Facilitates Active::Record object diffing}
12
+ spec.summary = %q{Adds ability to compare 2 Active::Record objects; returns the differences as a hash}
13
+ spec.homepage = "https://github.com/lizconlan/diffable"
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_dependency "activerecord", ">= 3.2"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "activerecord-nulldb-adapter"
27
+ end
@@ -0,0 +1,302 @@
1
+ require "diffable/version"
2
+
3
+ # :main: README.rdoc
4
+
5
+ ##
6
+ # Diffable provides a mixin that can be used to extend any ActiveRecord object
7
+ # to provide diff functionality. Calling the diff method compares the receiver
8
+ # against another object and returns a Hash of differences found (presented as
9
+ # a description of the changes between the second object and the receiver - as
10
+ # if trying to restore the calling object from its replacement).
11
+
12
+ module Diffable
13
+ def self.included base # :nodoc:
14
+ base.send :include, InstanceMethods
15
+ base.extend ClassMethods
16
+ end
17
+
18
+ module InstanceMethods
19
+ ##
20
+ # Produces a Hash containing the differences between the calling object
21
+ # and the object passed in as a parameter
22
+ def diff(other)
23
+ check_class_compatibility(self, other)
24
+
25
+ self_attribs = self.get_attributes(self.class.excluded_fields)
26
+ other_attribs = other.get_attributes(other.class.excluded_fields)
27
+
28
+ change = compare_objects(self_attribs, other_attribs, self, other)
29
+
30
+ #the last bit - no change, no report; simples
31
+ if other.class.conditional_fields
32
+ other.class.conditional_fields.each do |key|
33
+ change[key.to_sym] = eval("other.#{key}") unless change.empty?
34
+ end
35
+ end
36
+ change
37
+ end
38
+
39
+ ##
40
+ # Fetches the attributes of the calling object, exluding the +id+ field
41
+ # and any fields specified passed as an array of symbols via the +excluded+
42
+ # parameter
43
+ def get_attributes(excluded)
44
+ attribs = attributes.dup
45
+ attribs.delete_if { |key, value|
46
+ (!excluded.nil? and excluded.include?(key)) or key == "id" }
47
+ end
48
+
49
+ ##
50
+ # Uses reflection to fetch the eligible associated objects for the current
51
+ # object, excluding parent objects and child objects that do not include
52
+ # the Diffable mixin
53
+ def reflected_names(obj)
54
+ classes = obj.reflections
55
+ class_names = []
56
+ classes.each do |key, cl|
57
+ if eval(cl.class_name).respond_to?("diffable") \
58
+ and cl.association_class != ActiveRecord::Associations::BelongsToAssociation
59
+ class_names << key
60
+ end
61
+ end
62
+ class_names
63
+ end
64
+
65
+ private
66
+
67
+ def check_class_compatibility(current, other)
68
+ current_super = current.class.superclass
69
+ other_super = other.class.superclass
70
+ if current_super == ActiveRecord::Base || other_super == ActiveRecord::Base
71
+ if other.class != current.class && other.class != current_super && other_super != current.class
72
+ raise "Unable to compare #{current.class} to #{other.class}"
73
+ end
74
+ else
75
+ if current.class != other.class && other.class.superclass != current.class.superclass
76
+ raise "Unable to compare #{current.class} to #{other.class}"
77
+ end
78
+ end
79
+ end
80
+
81
+ def find_in_array_by_ident(arr, value)
82
+ arr.select { |x| eval(%Q|x.#{x.class.unique_within_group}|) == value }.first
83
+ end
84
+
85
+ def map_obj_idents(obj)
86
+ obj.map { |x| x.attributes[x.class.unique_within_group] }
87
+ end
88
+
89
+ def ident_in_list?(ident, ident_list)
90
+ return true if ident_list.include?(ident)
91
+ false
92
+ end
93
+
94
+ def compare_current_subs(current_obj_idents, previous_obj_idents, current_subs, previous_subs)
95
+ objects = []
96
+ current_obj_idents.each do |idnt|
97
+ current_sub = find_in_array_by_ident(current_subs, idnt)
98
+ previous_sub = find_in_array_by_ident(previous_subs, idnt)
99
+
100
+ if ident_in_list?(idnt, previous_obj_idents)
101
+ #pre-existing thing, compare the differences...
102
+ current_attribs = current_sub.get_attributes(current_sub.class.excluded_fields)
103
+ previous_attribs = previous_sub.get_attributes(previous_sub.class.excluded_fields)
104
+
105
+ obj = compare_objects(current_attribs, previous_attribs, current_sub, previous_sub, obj)
106
+
107
+ #...and only store if something's changed
108
+ unless obj.empty?
109
+ obj[:change_type] = "modified"
110
+ objects << obj
111
+ end
112
+ else
113
+ #a new thing, just need to note its arrival
114
+ unique_field = current_sub.class.unique_within_group
115
+ objects << {unique_field.to_sym => eval("current_sub.#{unique_field}"), :change_type => "new"}
116
+ end
117
+ end
118
+ objects
119
+ end
120
+
121
+ def preserve_deleted_by_ident(deleted_idents, previous_subs, previous_obj, sub)
122
+ objects = []
123
+ deleted_idents.each do |ident|
124
+ previous_sub = find_in_array_by_ident(eval("previous_obj.#{sub}.to_a"), ident)
125
+ obj = preserve_deleted_obj(previous_sub)
126
+ objects << obj
127
+ end
128
+ objects
129
+ end
130
+
131
+ def compare_attributes(current, previous, current_obj, change={})
132
+ previous.each do |key, value|
133
+ change[key.to_sym] = value if value != current[key]
134
+ end
135
+ unless change.empty?
136
+ if current_obj.class.unique_within_group
137
+ unique_field = current_obj.class.unique_within_group
138
+ change[unique_field.to_sym] = eval("current_obj.#{unique_field}")
139
+ end
140
+ end
141
+ change
142
+ end
143
+
144
+ def analyze_subobjects(current_obj, previous_obj, change={})
145
+ #need both - comparable objects need not have the same reflections
146
+ current_subs = reflected_names(current_obj)
147
+ previous_subs = reflected_names(previous_obj)
148
+
149
+ #things that are available to the current object
150
+ current_subs.each do |sub|
151
+ objects = []
152
+ current_objects = current_obj.association(sub).target
153
+ previous_objects = previous_obj.respond_to?(sub) ? eval("previous_obj.#{sub}.to_a") : []
154
+ current_obj_idents = map_obj_idents(current_objects)
155
+ previous_obj_idents = map_obj_idents(previous_objects)
156
+
157
+ #loop through the ids in the current block
158
+ objects += compare_current_subs(current_obj_idents, previous_obj_idents, current_objects, previous_objects)
159
+
160
+ #look for ids that only exist in the previous block
161
+ objects += preserve_deleted_by_ident((previous_obj_idents - current_obj_idents), previous_subs, previous_obj, sub)
162
+
163
+ #update time_blocks if any changes were found
164
+ change[sub] = objects unless objects.empty?
165
+ end
166
+
167
+ #things that are only available to the previous object
168
+ (previous_subs - current_subs).each do |sub|
169
+ objects = []
170
+ previous_obj_idents = map_obj_idents(previous_obj)
171
+ objects += preserve_deleted_by_ident(previous_obj_idents, (previous_subs - current_subs), previous_obj, sub)
172
+ change[sub] = objects unless objects.empty?
173
+ end
174
+ change
175
+ end
176
+
177
+ def preserve_deleted_obj(deleted, excluded_fields=self.class.excluded_fields)
178
+ obj = {}
179
+ #get attributes of object marked for deletion...
180
+ attribs = deleted.get_attributes(deleted.class.excluded_fields)
181
+ #...and copy them for preservation
182
+ attribs.keys.each do |att|
183
+ value = nil
184
+ if deleted.respond_to?(att)
185
+ value = eval("deleted.#{att}")
186
+ end
187
+
188
+ obj[att.to_sym] = value unless value.nil?
189
+ end
190
+
191
+ #look to see if our target object has sub-objects of its own
192
+ previous_sub_keys = reflected_names(deleted)
193
+
194
+ #preserve subs
195
+ obj = preserve_deleted_subs(previous_sub_keys, deleted, obj)
196
+
197
+ unless obj.empty?
198
+ if deleted.class.conditional_fields
199
+ deleted.class.conditional_fields.each do |key|
200
+ obj[key.to_sym] = eval("deleted.#{key}") unless obj.empty?
201
+ end
202
+ end
203
+ obj[:change_type] = "deleted"
204
+ end
205
+ obj
206
+ end
207
+
208
+ def compare_objects(current_attribs, other_attribs, current, other, change={})
209
+ #compare the simple values
210
+ change = compare_attributes(current_attribs, other_attribs, current)
211
+
212
+ #analyse the subobjects
213
+ change = analyze_subobjects(current, other, change)
214
+
215
+ if other.class.conditional_fields
216
+ other.class.conditional_fields.each do |key|
217
+ change[key.to_sym] = eval("other.#{key}") unless change.empty?
218
+ end
219
+ end
220
+ change
221
+ end
222
+
223
+ def preserve_deleted_subs(keys, deleted, change={})
224
+ keys.each do |sub|
225
+ subs = []
226
+ previous_subs = deleted.respond_to?(sub) ? eval("deleted.#{sub}.to_a") : []
227
+ previous_subs.each do |deleted_sub|
228
+ preserved = preserve_deleted_obj(deleted_sub)
229
+ subs << preserved
230
+ end
231
+ change[sub] = subs unless subs.empty?
232
+ end
233
+ change
234
+ end
235
+ end
236
+
237
+ module ClassMethods
238
+ ##
239
+ # Holds an array of excluded fields which will not be used
240
+ # for comparison tests or when copying deleted values
241
+ attr_reader :excluded_fields
242
+
243
+ ##
244
+ # String value corresponding to the field that uniquely identifies
245
+ # a child record from among its siblings
246
+ # (should not be id unless id is being generated)
247
+ attr_reader :unique_within_group
248
+
249
+ ##
250
+ # Holds an array of fields which will be added to a modified change Hash
251
+ # (regardless of whether its value has changed or not) unless there are
252
+ # no other changes
253
+ attr_reader :conditional_fields
254
+
255
+ ##
256
+ # A shortcut, used to quickly check whether a class implements Diffable
257
+ attr_reader :diffable
258
+
259
+ @diffable = true
260
+
261
+ ##
262
+ # Sets the class's conditional_fields values.
263
+ #
264
+ # If required, should be placed in the model definition code:
265
+ #
266
+ # class ModelA < ActiveRecord::Base
267
+ # include Diffable
268
+ # set_conditional_fields :meta
269
+ # end
270
+ def set_conditional_fields(*h)
271
+ @conditional_fields = []
272
+ h.each { |key| eval(%Q|@conditional_fields << "#{key.to_s}"|) }
273
+ end
274
+
275
+ ##
276
+ # Sets the class's excluded_fields values.
277
+ #
278
+ # If required, should be placed in the model definition code:
279
+ #
280
+ # class ModelB < ActiveRecord::Base
281
+ # include Diffable
282
+ # set_excluded_fields :ignore_me, :test
283
+ # end
284
+ def set_excluded_fields(*h)
285
+ @excluded_fields = []
286
+ h.each { |key| eval(%Q|@excluded_fields << "#{key.to_s}"|) }
287
+ end
288
+
289
+ ##
290
+ # Sets the class's unique_within_group value
291
+ #
292
+ # If required, should be placed in the model definition code:
293
+ #
294
+ # class ModelC < ActiveRecord::Base
295
+ # include Diffable
296
+ # set_unique_within_set :generated_identifier
297
+ # end
298
+ def set_unique_within_group(value)
299
+ eval(%Q|@unique_within_group = "#{value.to_s}"|)
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,6 @@
1
+ module Diffable
2
+ ##
3
+ # Diffable version you are using
4
+
5
+ VERSION = "0.0.1"
6
+ end
@@ -0,0 +1,35 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table(:notdiff) do |t|
3
+ t.string :name
4
+ end
5
+
6
+ create_table(:hazdiffs) do |t|
7
+ t.string :name
8
+ t.integer :price
9
+ end
10
+
11
+ create_table(:multidiffs) do |t|
12
+ t.string :name
13
+ t.integer :price
14
+ end
15
+
16
+ create_table(:subrecs) do |t|
17
+ t.integer :hazdiff_id
18
+ t.string :name
19
+ t.string :ident
20
+ end
21
+
22
+ create_table(:alt_subrecs) do |t|
23
+ t.integer :hazdiff_id
24
+ t.string :name
25
+ t.string :ident
26
+ t.string :ignore_me
27
+ t.string :tracker
28
+ end
29
+
30
+ create_table(:subrec_no_diffs) do |t|
31
+ t.integer :hazdiff_id
32
+ t.string :name
33
+ t.string :ident
34
+ end
35
+ end
@@ -0,0 +1,240 @@
1
+ require 'active_record'
2
+ require 'rspec'
3
+ require 'nulldb_rspec'
4
+
5
+ require "./lib/diffable"
6
+
7
+ class Hazdiff < ActiveRecord::Base
8
+ include Diffable
9
+ end
10
+
11
+ class Notdiff < ActiveRecord::Base
12
+ end
13
+
14
+ class Multidiff < ActiveRecord::Base
15
+ include Diffable
16
+ has_many :subrecs
17
+ has_many :alt_subrecs
18
+ has_many :subrec_no_diffs
19
+ end
20
+
21
+ class Subrec < ActiveRecord::Base
22
+ include Diffable
23
+ set_unique_within_group :ident
24
+ belongs_to :multidiff
25
+ end
26
+
27
+ class SubrecNoDiff < ActiveRecord::Base
28
+ belongs_to :multidiff
29
+ end
30
+
31
+ class AltSubrec < ActiveRecord::Base
32
+ include Diffable
33
+ set_unique_within_group :ident
34
+ set_excluded_fields :ignore_me
35
+ set_conditional_fields :tracker
36
+ end
37
+
38
+ class Altdiff < Hazdiff
39
+ end
40
+
41
+ class AltdiffToo < Hazdiff
42
+ end
43
+
44
+ describe Diffable do
45
+ before(:all) do
46
+ NullDB.configure {|ndb| ndb.project_root = './spec/'}
47
+ ActiveRecord::Base.establish_connection :adapter => :nulldb
48
+ ActiveRecord::Migration.verbose = false
49
+ end
50
+
51
+ def should_have_column(klass, col_name, col_type)
52
+ col = klass.columns_hash[col_name.to_s]
53
+ col.should_not be_nil
54
+ col.type.should == col_type
55
+ end
56
+
57
+ context "sanity checks" do
58
+ it "should remember columns defined in migrations" do
59
+ should_have_column(Hazdiff, :name, :string)
60
+ end
61
+
62
+ it "should be defined" do
63
+ Diffable::VERSION.is_a?(String)
64
+ !Diffable::VERSION.empty?
65
+ end
66
+ end
67
+
68
+ context "checking class compatibility" do
69
+ it "should be able to compare 2 things of the same class" do
70
+ hd1 = Hazdiff.new()
71
+ hd2 = Hazdiff.new()
72
+ lambda { hd1.diff(hd2) }.should_not raise_error
73
+ end
74
+
75
+ it "should not be able to compare 2 things with different base classes" do
76
+ hd = Hazdiff.new
77
+ md = Multidiff.new
78
+ lambda { hd.diff(md) }.should raise_error
79
+ lambda { md.diff(hd) }.should raise_error
80
+ end
81
+
82
+ it "should be able to compare 2 things with the same base class" do
83
+ ad1 = Altdiff.new
84
+ ad2 = AltdiffToo.new
85
+ lambda { ad1.diff(ad2) }.should_not raise_error
86
+ end
87
+
88
+ it "should be able to compare base object with inherited object" do
89
+ hd = Hazdiff.new
90
+ ad = Altdiff.new
91
+ lambda { hd.diff(ad) }.should_not raise_error
92
+ lambda { ad.diff(hd) }.should_not raise_error
93
+ end
94
+ end
95
+
96
+ context "an object not including Diffable" do
97
+ it "should not inherit the instance methods" do
98
+ object = Notdiff.new
99
+ object.respond_to?(:diff).should be_false
100
+ object.respond_to?(:get_attributes).should be_false
101
+ object.respond_to?(:reflected_names).should be_false
102
+ object.class.respond_to?(:diffable).should be_false
103
+ end
104
+ end
105
+
106
+ context "a simple object including Diffable" do
107
+ it "should inherit the expected instance methods" do
108
+ object = Hazdiff.new(:name => "meep")
109
+ object.respond_to?(:diff).should be_true
110
+ object.respond_to?(:get_attributes).should be_true
111
+ object.respond_to?(:reflected_names).should be_true
112
+ object.class.respond_to?(:diffable).should be_true
113
+ end
114
+
115
+ describe "when asked for diff" do
116
+ describe "and given an object with identical attribute values" do
117
+ it "should return a blank hash" do
118
+ obj1 = Hazdiff.new(:name => "test1", :price => 0)
119
+ obj2 = Hazdiff.new(:name => "test1", :price => 0)
120
+ obj1.diff(obj2).should eq({})
121
+ end
122
+ end
123
+
124
+ describe "and given an object with different attribute values" do
125
+ it "should return a hash of differences" do
126
+ obj1 = Hazdiff.new(:name => "test1", :price => 0)
127
+ obj2 = Hazdiff.new(:name => "test2", :price => 0)
128
+ obj3 = Hazdiff.new(:name => "test2", :price => 1)
129
+ obj1.diff(obj2).should eq({:name => "test2"})
130
+ obj2.diff(obj1).should eq({:name => "test1"})
131
+ obj1.diff(obj3).should eq({:name => "test2", :price => 1})
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ context "an object including Diffable that has subobjects" do
138
+ describe "when asked for diff" do
139
+ describe "and the subobjects do not include Diffable" do
140
+ it "should ignore the ineligible subobjects" do
141
+ obj1 = Multidiff.new(:name => "test1")
142
+ sub1 = SubrecNoDiff.new(:name => "sub1")
143
+
144
+ obj2 = Multidiff.new(:name => "test1")
145
+ sub2 = SubrecNoDiff.new(:name => "sub2")
146
+
147
+ obj1.diff(obj2).should eq({})
148
+ end
149
+ end
150
+
151
+ describe "and there are no difference between the subobjects" do
152
+ it "should return a blank hash" do
153
+ obj1 = Multidiff.new(:name => "test1")
154
+ sub1 = Subrec.new(:name => "sub1")
155
+ obj1.subrecs << sub1
156
+ obj2 = Multidiff.new(:name => "test1")
157
+ obj2.subrecs << sub1.dup
158
+
159
+ obj1.diff(obj2).should eq({})
160
+ end
161
+ end
162
+
163
+ describe "and there are 2 different sets of subobjects" do
164
+ it "should return 1 deleted subobject and 1 new one" do
165
+ obj1 = Multidiff.new(:name => "test1")
166
+ sub1 = Subrec.new(:name => "sub1", :ident => "s1")
167
+ obj1.subrecs << sub1
168
+
169
+ obj2 = Multidiff.new(:name => "test1")
170
+ sub2 = Subrec.new(:name => "sub2", :ident => "s2")
171
+ obj2.subrecs << sub2
172
+
173
+ obj1.diff(obj2).should eq({:subrecs=>[{:ident=>"s1", :change_type=>"new"}, {:name=>"sub2", :ident=>"s2", :change_type=>"deleted"}]})
174
+ end
175
+ end
176
+
177
+ describe "and there are changes to the subobject" do
178
+ it "should return the subobject changes" do
179
+ obj1 = Multidiff.new(:name => "test1")
180
+ sub1 = Subrec.new(:name => "sub1", :ident => "s1")
181
+ obj1.subrecs << sub1
182
+
183
+ obj2 = Multidiff.new(:name => "test1")
184
+ sub2 = Subrec.new(:name => "sub01", :ident => "s1")
185
+ obj2.subrecs << sub2
186
+
187
+ obj1.diff(obj2).should eq({:subrecs=>[{:name=>"sub01", :ident=>"s1", :change_type=>"modified"}]})
188
+ end
189
+
190
+ it "should include conditional fields where there has been a change" do
191
+ obj1 = Multidiff.new(:name => "test1")
192
+ sub1 = AltSubrec.new(:name => "sub1", :ident => "s1", :tracker => "t")
193
+ obj1.alt_subrecs << sub1
194
+
195
+ obj2 = Multidiff.new(:name => "test1")
196
+ sub2 = AltSubrec.new(:name => "sub01", :ident => "s1", :tracker => "t")
197
+ obj2.alt_subrecs << sub2
198
+
199
+ obj1.diff(obj2).should eq({:alt_subrecs=>[{:name=>"sub01", :ident=>"s1", :tracker => "t", :change_type=>"modified"}]})
200
+ end
201
+ end
202
+
203
+ describe "and the subobject has been removed" do
204
+ #diffs are retrospective, this is where things get a bit weird
205
+ it "should return a new subobject" do
206
+ obj1 = Multidiff.new(:name => "test1")
207
+ sub1 = Subrec.new(:name => "sub1", :ident => "s1")
208
+ obj1.subrecs << sub1
209
+
210
+ obj2 = Multidiff.new(:name => "test1")
211
+
212
+ obj1.diff(obj2).should eq({:subrecs=>[{:ident=>"s1", :change_type=>"new"}]})
213
+ end
214
+ end
215
+
216
+ describe "and a subobject has been added" do
217
+ #diffs are retrospective, this is where things get a bit weird
218
+ it "should return a deleted subobject" do
219
+ obj1 = Multidiff.new(:name => "test1")
220
+ obj2 = Multidiff.new(:name => "test1")
221
+ sub1 = Subrec.new(:name => "sub1", :ident => "s1")
222
+ obj2.subrecs << sub1
223
+
224
+ obj1.diff(obj2).should eq({:subrecs=>[{:ident=>"s1", :name => "sub1", :change_type=>"deleted"}]})
225
+ end
226
+
227
+ it "should not return excluded fields" do
228
+ obj1 = Multidiff.new(:name => "test1")
229
+ obj2 = Multidiff.new(:name => "test1")
230
+ sub1 = AltSubrec.new(:name => "sub1", :ident => "s1", :tracker => "t", :ignore_me => "??")
231
+ obj2.alt_subrecs << sub1
232
+
233
+ obj1.diff(obj2).should eq({:alt_subrecs=>[{:name => "sub1", :ident=>"s1", :tracker => "t", :change_type=>"deleted"}]})
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ ActiveRecord::Base.configurations['test'] = {'adapter' => 'nulldb'}
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: diffable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Liz Conlan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-11-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '3.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '3.2'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
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: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: activerecord-nulldb-adapter
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Facilitates Active::Record object diffing
95
+ email:
96
+ - lizconlan@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - Gemfile
103
+ - LICENSE.txt
104
+ - README.md
105
+ - Rakefile
106
+ - diffable.gemspec
107
+ - lib/diffable.rb
108
+ - lib/diffable/version.rb
109
+ - spec/db/schema.rb
110
+ - spec/lib/diffable_spec.rb
111
+ homepage: https://github.com/lizconlan/diffable
112
+ licenses:
113
+ - MIT
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ segments:
125
+ - 0
126
+ hash: -907652915963392506
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ segments:
134
+ - 0
135
+ hash: -907652915963392506
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 1.8.24
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Adds ability to compare 2 Active::Record objects; returns the differences
142
+ as a hash
143
+ test_files:
144
+ - spec/db/schema.rb
145
+ - spec/lib/diffable_spec.rb