diffable 0.0.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,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