happymapper-differ 0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8762cf0e3b779fa85dad307850b73fff48980c87
4
- data.tar.gz: 5d77b1d3cae7582cf841387ec3c98473a46866ff
3
+ metadata.gz: 89743c03e9ed823e1418688575aca74ecd06dae6
4
+ data.tar.gz: c0fe578b444d252316621c029b869f7ccf483196
5
5
  SHA512:
6
- metadata.gz: e034ea0d0d7455438b6afe0cc6c60d870e78b85fe7afcc9c22a671a8af8e84546e222e64a51cad1d3223c1d3808250f52f78e48ea97dd8031923d69734dbfe1e
7
- data.tar.gz: a1d3704b0af7d435e5625404f1ac4c847af7a72463b2304815e55fb9c6fd1fd1a1c496c34c67bd63abf112c51a45f26441e3219381e9db2a6b36c067e53cc612
6
+ metadata.gz: 6a95ada3cb154e6e150080a0f4d3b21e76e3da9b3745ef89107ea7ee7557c2bea81900dfbf395afc982f7f195954958ae6754e204c1066845edae9ef32d8d819
7
+ data.tar.gz: ea4b1f0be3929ea7cca84f3bd7ffdd0ae7a705a455bff4d86ffe2e3cd7c5a3321e73fe9fdcda9ce56dbae32f4740634a4098582576225198f72e7f8226d55fca
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## Unreleased
2
+ ### Changed
3
+ - Removed cloning
@@ -1,132 +1,174 @@
1
+ require 'delegate'
2
+
1
3
  module HappyMapper
2
- class Differ
3
- VERSION = 0.1
4
+ # Differ compares the differences betwee two HappyMapper objects.
5
+ #
6
+ # Two step process
7
+ # First step is map all nodes into a DiffedItem
8
+ # Step two is present all the changes via DiffedItem.changes
9
+ class Differ
10
+ VERSION = "0.1.1"
4
11
 
5
12
  def initialize(left, right)
6
13
  @left = left
7
14
  @right = right
8
15
  end
9
16
 
10
- def changed?
11
- @left.to_xml == @right.to_xml
12
- end
13
-
14
- # Diff is a memory ineffecient and ugly method to find what elements and
15
- # attributes have changed and how.
16
- #
17
- # It extends each element and attribute with the DiffedItem module
18
- # and makes a clone of the original element for comparison.
17
+ # Diff is a method to find what elements and attributes have changed and
18
+ # how.It extends each element and attribute with the DiffedItem module
19
19
  def diff
20
- out = @left
21
- setup(@left, @right)
22
-
23
- all = out.class.attributes + out.class.elements
20
+ @left = DiffedItem.create(@left, @right)
24
21
 
25
22
  # setup for each element (has_one and has_many) and attribute
26
- all.map(&:name).compact.each do |name|
27
- value = out.send(name)
28
- if value.nil?
29
- value = NilLike.new
30
- out.send("#{name}=", value)
31
- end
23
+ all_items.each do |item|
24
+ lvalue = get_value(@left, item.name)
25
+ rvalue = get_value(@right, item.name)
26
+
27
+ # skip if both sides are nil
28
+ next if rvalue.nil? && lvalue.nil?
32
29
 
33
- if value.is_a?(Array)
34
- # Find the side with the most items
35
- # If the right has more, the left will be padded with NilLike instances
36
- count = [value.size, (@right.send(name) || []).size].max
30
+ if ! item.options[:single]
31
+ setup_element(lvalue, rvalue)
32
+ # Find the side with the most items. If the right has more, the left
33
+ # will be padded with UnExtendable instances
34
+ count = [lvalue.size, (rvalue || []).size].max
37
35
 
38
36
  count.times do |i|
39
- value[i] = NilLike.new if value[i].nil?
40
- setup_element(value[i], @right.send(name)[i])
37
+ lvalue[i] = setup_element(lvalue[i], (rvalue || [])[i])
41
38
  end
42
39
  else
43
- setup_element(value, @right.send(name))
40
+ lvalue = setup_element(lvalue, rvalue)
44
41
  end
42
+
43
+ @left.send("#{item.name}=", lvalue)
45
44
  end
46
45
 
47
- out
46
+ @left
48
47
  end
49
48
 
50
- def handle_nil(name)
51
- out.send("#{name}=", NilLike.new)
49
+ protected
50
+
51
+ def get_value(side, name)
52
+ begin
53
+ side.send(name)
54
+ rescue
55
+ nil
56
+ end
52
57
  end
53
58
 
54
- def setup(item, compared)
55
- n = item.clone
56
- item.extend(DiffedItem)
57
- item.compared = compared
58
- item.original = n
59
+ # returns all the elements and attributes for the left class
60
+ def all_items
61
+ @left.class.attributes + @left.class.elements
59
62
  end
60
63
 
61
64
  def setup_element(item, compared)
62
- if(item.is_a?(HappyMapper))
65
+ if item.is_a?(HappyMapper)
63
66
  Differ.new(item, compared).diff
64
67
  else
65
- setup(item, compared)
68
+ DiffedItem.create(item, compared)
66
69
  end
67
70
  end
71
+ end
68
72
 
69
- # nil can't be cloned or extended
70
- # so this object behaves like nil
71
- class NilLike
72
- def nil?
73
- true
74
- end
75
-
76
- def to_s
77
- "nil"
78
- end
79
-
80
- def inspect
81
- nil.inspect
82
- end
73
+ # nil, Float, and other classes can't be extended
74
+ # so this object acts as wrapper
75
+ class UnExtendable < SimpleDelegator
76
+ def class
77
+ __getobj__.class
83
78
  end
84
79
  end
85
80
 
81
+ # DiffedItem is an extension which allows tracking changes between two
82
+ # HappyMapper objects.
86
83
  module DiffedItem
87
- attr_accessor :original
88
-
89
84
  # The object this item is being compared to
90
85
  attr_accessor :compared
91
- alias :was :compared
86
+ alias_method :was, :compared
87
+
88
+ def self.create(item, compared)
89
+ begin
90
+ # if the item can not be cloned, it will raise an exception
91
+ # do not extend objects which can not be cloned
92
+ item.clone
93
+ item.extend(DiffedItem)
94
+ rescue
95
+ # this item is a Float, Nil or other class that can not be extended
96
+ item = UnExtendable.new(item)
97
+ item.extend(DiffedItem)
98
+ end
99
+
100
+ item.compared = compared
101
+ item
102
+ end
92
103
 
93
104
  def changed?
94
- original != compared
105
+ if self.is_a?(HappyMapper)
106
+ ! changes.empty?
107
+ else
108
+ self != compared
109
+ end
95
110
  end
96
111
 
97
112
  def changes
98
- cs = {} # the changes
113
+ @changes ||= ChangeLister.new(self, compared).find_changes
114
+ end
115
+ end
99
116
 
100
- original.class.attributes.map(&:name).each do |attr|
101
- other_value = compared.send(attr)
102
- if original.send(attr) != other_value
103
- cs[attr] = other_value
104
- end
117
+ # ChangeLister creates a hash of all changes between the two objects
118
+ class ChangeLister
119
+ def initialize(current, compared)
120
+ @current = current
121
+ @compared = compared
122
+ @changes = {}
123
+ end
124
+
125
+ def eq(a,b)
126
+ if a.respond_to?(:to_xml) && b.respond_to?(:to_xml)
127
+ a.to_xml == b.to_xml
128
+ else
129
+ a == b
105
130
  end
131
+ end
106
132
 
107
- original.class.elements.map(&:name).each do |name|
108
- other_els = compared.send(name)
109
- this_els = original.send(name)
133
+ def find_changes
134
+ elements_and_attributes.map(&:name).each do |name|
135
+ el = @current.send(name)
110
136
 
111
- if this_els.is_a?(Array)
112
- this_els.each_with_index do |el, i|
113
- if el != other_els[i]
114
- cs[name] ||= []
115
- cs[name] << other_els[i]
116
- end
117
- end
137
+ if el.is_a?(Array)
138
+ many_changes(el, key: name)
118
139
  else
119
- if this_els != other_els
120
- cs[name] = other_els
140
+ other_el = get_compared_value(name)
141
+ if ! eq(el, other_el)
142
+ @changes[name] = other_el
121
143
  end
122
144
  end
123
145
  end
124
146
 
125
- cs
147
+ @changes
148
+ end
149
+
150
+ # Handle change for has_many elements
151
+ def many_changes(els, key:)
152
+ other_els = get_compared_value(key) || []
153
+
154
+ els.each_with_index do |el, i|
155
+ if ! eq(el, other_els[i])
156
+ @changes[key] ||= []
157
+ @changes[key] << other_els[i]
158
+ end
159
+ end
160
+ end
161
+
162
+ def get_compared_value(key)
163
+ if @compared.respond_to?(key)
164
+ @compared.send(key)
165
+ else
166
+ nil
167
+ end
126
168
  end
127
169
 
128
- def ==(other)
129
- original == other
170
+ def elements_and_attributes
171
+ @current.class.attributes + @current.class.elements
130
172
  end
131
173
  end
132
174
  end
@@ -1,11 +1,7 @@
1
1
  require 'happymapper'
2
2
 
3
+ # HappyMapper module imported from the nokogiri-happymapper gem
3
4
  module HappyMapper
4
5
  require 'happymapper/differ'
5
-
6
- # equality based on the underlying XML
7
- def ==(other)
8
- self.to_xml == other.to_xml
9
- end
10
6
  end
11
7
 
@@ -0,0 +1,55 @@
1
+ require 'test_helper'
2
+
3
+ describe HappyMapper::ChangeLister do
4
+ it "should find now changes between two equal objects" do
5
+ a = TParent.parse(sample_a)
6
+ b = TParent.parse(sample_a)
7
+
8
+ d = HappyMapper::ChangeLister.new(a,b).find_changes
9
+
10
+ assert_equal({}, d)
11
+ end
12
+
13
+ it "should find the changes between two simple objects" do
14
+ a = TParent.parse(sample_a)
15
+ b = TParent.parse(sample_b)
16
+
17
+ d = HappyMapper::ChangeLister.new(a,b).find_changes
18
+
19
+ assert_equal ["name", "children"], d.keys
20
+ assert_equal "Vlad", d["name"]
21
+ assert_equal [b.children[2]], d["children"]
22
+ end
23
+
24
+ it "should find changes in nested objects" do
25
+ a = TParent.parse(sample_a)
26
+ b = TParent.parse(sample_a)
27
+
28
+ b.children.first.address = TAddress.new
29
+ b.children.first.address.street = "123 Maple"
30
+
31
+ d = HappyMapper::ChangeLister.new(a,b).find_changes
32
+
33
+ assert_equal [b.children.first], d["children"]
34
+ end
35
+
36
+ def sample_a
37
+ <<-XML
38
+ <parent name="Roz">
39
+ <child name="Joe"/>
40
+ <child name="Jane"/>
41
+ <child name="Jason"/>
42
+ </parent>
43
+ XML
44
+ end
45
+
46
+ def sample_b
47
+ <<-XML
48
+ <parent name="Vlad">
49
+ <child name="Joe"/>
50
+ <child name="Jane"/>
51
+ <child name="Alex"/>
52
+ </parent>
53
+ XML
54
+ end
55
+ end
@@ -0,0 +1,59 @@
1
+ require 'test_helper'
2
+
3
+ class DIPerson
4
+ include HappyMapper
5
+ tag 'person'
6
+
7
+ attribute 'name', String
8
+ has_one :child, DIPerson
9
+ end
10
+
11
+ describe HappyMapper::DiffedItem do
12
+ describe "HappyMapper objects" do
13
+ let(:a) { TAddress.parse("<address><street>Maple</street></address>") }
14
+ let(:b) { TAddress.parse("<address><street>Main</street></address>") }
15
+
16
+ describe "changed" do
17
+ it "is true when the values are not equal" do
18
+ di = HappyMapper::DiffedItem.create(a,b)
19
+ assert_equal true, di.changed?
20
+ end
21
+
22
+ it "is false when the objects are the same" do
23
+ di = HappyMapper::DiffedItem.create(a,a)
24
+ assert_equal false, di.changed?
25
+ end
26
+ end
27
+ end
28
+
29
+ describe "non HappyMapper Objects" do
30
+ describe "changed" do
31
+ it "is true when the values are note equal" do
32
+ di = HappyMapper::DiffedItem.create("A","B")
33
+ assert_equal true, di.changed?
34
+ assert_equal "B", di.was
35
+
36
+ di = HappyMapper::DiffedItem.create(1,2)
37
+ assert_equal true, di.changed?
38
+ assert_equal 2, di.was
39
+
40
+ di = HappyMapper::DiffedItem.create(1,1)
41
+ assert_equal false, di.changed?
42
+ assert_equal 1, di.was
43
+ end
44
+ end
45
+ end
46
+
47
+ describe "with nil objects" do
48
+ # BUG: When there are two nil objects, the second replaces the first.
49
+ it "should keep the correct state" do
50
+ a = HappyMapper::DiffedItem.create(nil, 'A')
51
+ b = HappyMapper::DiffedItem.create(nil, 'B')
52
+
53
+ assert_equal 'A', a.was
54
+ assert_equal 'B', b.was
55
+ end
56
+ end
57
+ end
58
+
59
+
@@ -4,17 +4,20 @@ describe "HappyMapper with Comparable" do
4
4
  let(:left) { TParent.parse(sample_a) }
5
5
  let(:right) { TParent.parse(sample_b) }
6
6
 
7
- it "sez two identical documents should be equal" do
8
- assert_equal left, TParent.parse(sample_a)
9
- end
10
-
11
- it "sez two different documents should not be equal" do
12
- refute_equal left, right
13
- end
14
-
15
7
  describe HappyMapper::Differ do
16
8
  let(:result) { HappyMapper::Differ.new(left, right).diff }
17
9
 
10
+ it "finds no changes for identical documents" do
11
+ result = HappyMapper::Differ.new(
12
+ left,
13
+ TParent.parse(sample_a)
14
+ ).diff
15
+
16
+ assert ! result.changed?
17
+ assert ! result.name.changed?
18
+ assert_equal({}, result.changes)
19
+ end
20
+
18
21
  it "finds attribute changes" do
19
22
  result = HappyMapper::Differ.new(
20
23
  TParent.parse("<parent name='Roz'/>"),
@@ -35,20 +38,26 @@ describe "HappyMapper with Comparable" do
35
38
  end
36
39
 
37
40
  it "finds changes to has_many elements" do
41
+ assert result.children.changed?
38
42
  assert ! result.children[0].changed?
39
43
  assert ! result.children[1].changed?
40
44
  assert result.children[2].changed?
45
+
46
+
41
47
  assert_equal({"name" => "Vlad", "children" => [right.children[2]]}, result.changes)
42
48
  assert_equal({}, result.children[1].changes)
43
49
  assert_equal({"name" => "Alex"}, result.children[2].changes)
44
50
  end
45
51
 
46
- it "finds changes to nested data" do
52
+ it "finds changes to nested data" do
47
53
  result = HappyMapper::Differ.new(
48
54
  TParent.parse(nested_a),
49
55
  TParent.parse(nested_b),
50
56
  ).diff
51
57
 
58
+ # why is the name being injected
59
+ assert_equal result.children.last.address.to_xml, result.children.last.address.was.to_xml
60
+
52
61
  assert result.changed?
53
62
  assert result.children[0].changed?
54
63
  assert result.children[0].address.changed?
@@ -59,15 +68,15 @@ describe "HappyMapper with Comparable" do
59
68
  assert_equal("789 Maple St", result.children[0].address.street.compared)
60
69
  end
61
70
 
62
- it "find changes when the value is nil" do
71
+ it "find changes when the value is nil XX" do
63
72
  result = HappyMapper::Differ.new(
64
73
  TParent.parse("<parent/>"),
65
- TParent.parse("<parent name='Alex'/>"),
74
+ TParent.parse("<parent name='Justin'/>"),
66
75
  ).diff
67
76
 
68
77
  assert result.changed?
69
78
  assert result.name.changed?
70
- assert_equal 'Alex', result.name.compared
79
+ assert_equal 'Justin', result.name.compared
71
80
  end
72
81
 
73
82
  it "finds changes when the left side element count is less than the right" do
@@ -79,6 +88,46 @@ describe "HappyMapper with Comparable" do
79
88
  assert result.changed?
80
89
  assert result.children[3].changed?
81
90
  end
91
+
92
+ it "handles a variet of types" do
93
+ result = HappyMapper::Differ.new(
94
+ TTypes.parse(types_a),
95
+ TTypes.parse(types_b)
96
+ ).diff
97
+
98
+ assert result.changed?
99
+ assert_equal Float, result.float.class
100
+ assert_equal 1.1, result.float
101
+ assert_equal 11.1, result.float.was
102
+ end
103
+
104
+ it "gracefully handles mismatched objects" do
105
+ result = HappyMapper::Differ.new(
106
+ TParent.parse(sample_a),
107
+ TParent.parse("<parent/>"),
108
+ ).diff
109
+
110
+ assert result.changed?
111
+ assert result.changes
112
+ end
113
+
114
+ it "handles nil right side" do
115
+ di = HappyMapper::Differ.new(left,nil).diff
116
+ assert_equal true, di.changed?
117
+ assert_equal ["name","children"], di.changes.keys
118
+
119
+ p = TParent.parse("<parent><child/><child/></parent>")
120
+ di = HappyMapper::Differ.new(p,nil).diff
121
+ assert_equal true, di.changed?
122
+ assert_equal ["children"], di.changes.keys
123
+ end
124
+
125
+ it "errors if the left is nil" do
126
+ assert_raises NoMethodError do
127
+ di = HappyMapper::Differ.new(nil,right).diff
128
+ assert_equal false, di.changed?
129
+ end
130
+ end
82
131
  end
83
132
 
84
133
  def sample_a
@@ -143,7 +192,7 @@ describe "HappyMapper with Comparable" do
143
192
  </child>
144
193
  <child name="Jane">
145
194
  <address>
146
- <street>567 Olice St</street>
195
+ <street>567 Olive St</street>
147
196
  <city>Brooklyn</city>
148
197
  </address>
149
198
  </child>
@@ -151,6 +200,14 @@ describe "HappyMapper with Comparable" do
151
200
  XML
152
201
  end
153
202
 
203
+ def addy
204
+ <<-XML
205
+ <address>
206
+ <street>567 Olive St</street>
207
+ <city>Brooklyn</city>
208
+ </address>
209
+ XML
210
+ end
154
211
  # Joe's address changed
155
212
  def nested_b
156
213
  <<-XML
@@ -163,11 +220,31 @@ describe "HappyMapper with Comparable" do
163
220
  </child>
164
221
  <child name="Jane">
165
222
  <address>
166
- <street>567 Olice St</street>
223
+ <street>567 Olive St</street>
167
224
  <city>Brooklyn</city>
168
225
  </address>
169
226
  </child>
170
227
  </parent>
171
228
  XML
172
229
  end
230
+
231
+ def types_a
232
+ <<-XML
233
+ <types
234
+ float="1.1"
235
+ int="2"
236
+ bool="true"
237
+ />
238
+ XML
239
+ end
240
+
241
+ def types_b
242
+ <<-XML
243
+ <types
244
+ float="11.1"
245
+ int="12"
246
+ bool="false"
247
+ />
248
+ XML
249
+ end
173
250
  end
data/test/test_helper.rb CHANGED
@@ -9,6 +9,10 @@ class TParent
9
9
  tag 'parent'
10
10
 
11
11
  attribute :name, String
12
+
13
+ # always nil, never used
14
+ has_one :address, "TAddress", tag: 'pAddress'
15
+
12
16
  has_many :children, "TChild", tag: 'child'
13
17
  end
14
18
 
@@ -28,3 +32,11 @@ class TAddress
28
32
  has_one :street, String
29
33
  has_one :city, String
30
34
  end
35
+
36
+ class TTypes
37
+ include HappyMapper
38
+ tag 'types'
39
+
40
+ attribute :float, Float
41
+ attribute :int, Integer
42
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: happymapper-differ
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Weir
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-06 00:00:00.000000000 Z
11
+ date: 2015-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri-happymapper
@@ -77,6 +77,7 @@ executables: []
77
77
  extensions: []
78
78
  extra_rdoc_files: []
79
79
  files:
80
+ - CHANGELOG.md
80
81
  - Gemfile
81
82
  - Gemfile.lock
82
83
  - LICENSE
@@ -85,6 +86,8 @@ files:
85
86
  - happymapper-differ.gemspec
86
87
  - lib/happymapper/differ.rb
87
88
  - lib/happymapper_differ.rb
89
+ - test/happymapper/change_lister_test.rb
90
+ - test/happymapper/diffed_item_test.rb
88
91
  - test/happymapper_differ_test.rb
89
92
  - test/test_helper.rb
90
93
  homepage: https://github.com/pharos-ei/happymapper-differ
@@ -112,5 +115,7 @@ signing_key:
112
115
  specification_version: 4
113
116
  summary: Find changes between two like HappyMapper objects
114
117
  test_files:
118
+ - test/happymapper/change_lister_test.rb
119
+ - test/happymapper/diffed_item_test.rb
115
120
  - test/happymapper_differ_test.rb
116
121
  - test/test_helper.rb