happymapper-differ 0.1 → 0.1.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.
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