hash_deep_diff 0.5.0 → 0.8.0

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
  SHA256:
3
- metadata.gz: 3aab07a4c397dbc84845d5f0faff052bbae89eb8be4bf24a5ed9df0d5129c3da
4
- data.tar.gz: 2ea89b301d808655625e0fa6a9c5028b9d194bba3c4dc54cf1f5c6859c59fc6f
3
+ metadata.gz: 0e7ceeb1c2e935cc4b6c9d27e6d718f63f627d3adeba269df367c37464f3e9c0
4
+ data.tar.gz: 00053a3c87a26ea7dff3513594c6fb85e1235be3c341bc58f30d4934faa3de57
5
5
  SHA512:
6
- metadata.gz: f0a05ee023dbbd4d8551b5065a57a65b645ca3b0a864eace0de91b10ec08da77b0a15df6422af0f5ce0afd4459ca141e227398f9560198f6d350963fac1a99f0
7
- data.tar.gz: 279185ca8464992a65f2fff626b6ee90f44ac7b7e98706e34443390ad24457961b642e5b2ebee10c4c8e3e5c72b7bfee454dd644f8f7ef68db5872c426137dc7
6
+ metadata.gz: 612c398c02060fd908f449ac44893a8a81d33f3c1f13ec86bb3042d748034f18a20fbd99559c211af58e5f70d77d0b107fda0fae6cb2be3a1f94e1e0299c0f2e
7
+ data.tar.gz: c63d6e0ddad970dce331a5460981916b821805a8a3fe8b95f5ab6ea73a029942091acdf6b9681da533630fd6da38440749419d25436979e07cd15da5a2020658
data/.rubocop.yml CHANGED
@@ -10,6 +10,7 @@ AllCops:
10
10
  - '.git/**/*'
11
11
  - '.bundle/**/*'
12
12
  - 'bin/*'
13
+ TargetRubyVersion: 2.6.1
13
14
 
14
15
  Style/RedundantReturn:
15
16
  Enabled: false
@@ -18,3 +19,9 @@ Metrics/BlockLength:
18
19
  Exclude:
19
20
  - 'test/**/test_*.rb'
20
21
  - '*.gemspec'
22
+ Style/YodaCondition:
23
+ EnforcedStyle: require_for_all_comparison_operators
24
+
25
+ Minitest/AssertEmptyLiteral:
26
+ Exclude:
27
+ - 'test/unit/test_change_key.rb'
data/CHANGELOG.md CHANGED
@@ -0,0 +1 @@
1
+ Changes will be recorded for version 1.0.0 and later
data/Guardfile CHANGED
@@ -25,8 +25,8 @@ end
25
25
  guard :minitest do
26
26
  # with Minitest::Unit
27
27
  watch(%r{^test/(.*)/?test_(.*)\.rb$})
28
- # watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
29
- watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { 'test' }
28
+ watch(%r{^lib/hash_deep_diff/(.*/)?([^/]+)\.rb$}) { |m| "test/unit/#{m[1]}test_#{m[2]}.rb" }
29
+ # watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { 'test' }
30
30
  watch(%r{^test/test_helper\.rb$}) { 'test' }
31
31
  watch(%r{^test/support/.*$}) { 'test' }
32
32
  end
data/README.md CHANGED
@@ -5,7 +5,9 @@ Status](https://img.shields.io/github/workflow/status/bpohoriletz/hash_deep_diff
5
5
  ![GitHub](https://img.shields.io/github/license/bpohoriletz/hash_deep_diff)
6
6
 
7
7
 
8
- Find the exact difference between two Hash objects and build a report to visualize it
8
+ Find the exact difference between two Hash objects and build a report to visualize it. Works for other objects too but why would you do that :/
9
+
10
+ Alternative solutions [hashdiff](https://github.com/liufengyun/hashdiff) by liufengyun and [hash_diff](https://github.com/CodingZeal/hash_diff) by CodingZeal
9
11
 
10
12
  ## Installation
11
13
 
@@ -24,20 +26,103 @@ Or install it yourself as:
24
26
  $ gem install hash_deep_diff
25
27
 
26
28
  ## Usage
27
- Basic example
29
+ ### Basic
28
30
 
29
31
  ```ruby
30
32
  left = { a: :a }
31
33
  right = { a: :b }
32
34
 
33
- HashDeepDiff::Comparison.new(left, right).report
35
+ print HashDeepDiff::Comparison.new(left, right).report
34
36
  ```
35
37
  ```diff
36
38
  - left[a] = a
37
39
  + left[a] = b
38
40
  ```
39
- please see [Documentation](https://rdoc.info/gems/hash_deep_diff) for
40
- more info
41
+ ### Arrays
42
+ ```ruby
43
+ left = [1, 2, { a: :a }]
44
+ right = [2, { a: :b }, 3]
45
+
46
+ print HashDeepDiff::Comparison.new(left, right).report
47
+ ```
48
+ ```diff
49
+ -left[...] = [1]
50
+ +left[...] = [3]
51
+ -left[{}][a] = a
52
+ +left[{}][a] = b
53
+ ```
54
+ ### Nesting
55
+ ```ruby
56
+ left = { a: [1, 2, { a: :a } ], b: { c: [1, 2, { d: :e } ] } }
57
+ right = { a: [2, { a: :b }, 3], b: { c: { f: { g: :h } } } }
58
+
59
+ print HashDeepDiff::Comparison.new(left, right).report
60
+ ```
61
+ ```diff
62
+ -left[a][...] = [1]
63
+ +left[a][...] = [3]
64
+ -left[a][{}][a] = a
65
+ +left[a][{}][a] = b
66
+ +left[b][c][...][f] = {}
67
+ +left[b][c][...][f][g] = h
68
+ -left[b][c][...][f] = [1, 2]
69
+ -left[b][c][{}][d] = e
70
+ ```
71
+ ### Reporting Engines
72
+ You can choose from the default diff-like reporting engine (examples are above) and YML reporting engine
73
+
74
+ ```ruby
75
+ left = { a: [1, 2, { a: :a } ], b: { c: [1, 2, { d: :e } ] } }
76
+ right = { a: [2, { a: :b }, 3], b: { c: { f: { g: :h } } } }
77
+ ```
78
+ #### Raw Report
79
+
80
+ ```ruby
81
+ print HashDeepDiff::Comparison.new(left, right, reporting_engine: HashDeepDiff::Reports::Yml).raw_report
82
+ => {"additions"=>{:a=>[3, {:a=>:b}], :b=>{:c=>[{:f=>{:g=>:h}}]}},
83
+ "deletions"=>{:a=>[1, {:a=>:a}], :b=>{:c=>[1, 2, {:d=>:e}]}}}
84
+ ```
85
+
86
+ #### YML Report
87
+
88
+ ```ruby
89
+ print HashDeepDiff::Comparison.new(left, right, reporting_engine: HashDeepDiff::Reports::Yml).report
90
+
91
+ ---
92
+ additions:
93
+ :a:
94
+ - 3
95
+ - :a: :b
96
+ :b:
97
+ :c:
98
+ - :f:
99
+ :g: :h
100
+ deletions:
101
+ :a:
102
+ - 1
103
+ - :a: :a
104
+ :b:
105
+ :c:
106
+ - 1
107
+ - 2
108
+ - :d: :e
109
+ ```
110
+
111
+ please see [Documentation](https://rdoc.info/gems/hash_deep_diff/HashDeepDiff/Comparison) for
112
+ more information or [Reporting test](https://github.com/bpohoriletz/hash_deep_diff/blob/a525d239189b0310aec3741dfc4862834805252d/test/integration/locales/test_uk_ru.rb#L59)
113
+
114
+
115
+
116
+ ## Customization
117
+
118
+ You can implement and use your own reporting engines with the default `HashDeepDiff::Delta` objects as a source of the report. In order to do so implement your own version of the reporting engine (example can be found [here](https://github.com/bpohoriletz/hash_deep_diff/tree/main/lib/hash_deep_diff/reports)) and inject it into a `Comparison`
119
+
120
+ ```ruby
121
+ left = { a: :a }
122
+ right = { a: :b }
123
+
124
+ HashDeepDiff::Comparison.new(left, right, reporting_engine: CustomEngine).report
125
+ ```
41
126
 
42
127
  ## Contributing
43
128
 
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
23
23
 
24
24
  spec.metadata['homepage_uri'] = spec.homepage
25
+ spec.metadata['documentation_uri'] = 'https://rdoc.info/gems/hash_deep_diff'
25
26
  spec.metadata['source_code_uri'] = 'https://github.com/bpohoriletz/hash_deep_diff'
26
27
  spec.metadata['changelog_uri'] = 'https://github.com/bpohoriletz/hash_deep_diff/blob/main/CHANGELOG.md'
27
28
  spec.metadata['rubygems_mfa_required'] = 'true'
@@ -47,8 +48,12 @@ Gem::Specification.new do |spec|
47
48
  spec.add_development_dependency 'minitest', '~> 5.15.0'
48
49
  spec.add_development_dependency 'minitest-focus', '~> 1.3.1'
49
50
  spec.add_development_dependency 'minitest-reporters', '~> 1.5.0'
51
+ spec.add_development_dependency 'naught', '~> 1.1.0'
52
+ spec.add_development_dependency 'pry-byebug', '~> 3.9.0'
50
53
  spec.add_development_dependency 'rake', '~> 10.5.0'
51
54
  spec.add_development_dependency 'rubocop', '~> 1.26.1'
52
55
  spec.add_development_dependency 'rubocop-minitest', '~> 0.18.0'
56
+ spec.add_development_dependency 'rubocop-performance', '~> 1.13.3'
53
57
  spec.add_development_dependency 'rubocop-rake', '~> 0.6.0'
58
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.10.0'
54
59
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module HashDeepDiff
6
+ # Key for a compared value inside Array or Hash
7
+ class ChangeKey
8
+ extend Forwardable
9
+ def_delegators :to_ary, :[], :+, :map, :==, :first, :shift, :empty?
10
+ # element that indicates nested Hash
11
+ NESTED_HASH = '{}'
12
+ # element that indicates Array value
13
+ ARRAY_VALUE = '...'
14
+
15
+ # based on the first element of the key returns the initial object
16
+ # to buld the change representation
17
+ # @return [Array, Hash]
18
+ def self.initial_object(values:)
19
+ return [] if values.size.positive? && [NESTED_HASH, ARRAY_VALUE].include?(values[0][0][0])
20
+
21
+ {}
22
+ end
23
+
24
+ # set the value inside Hash based on the change_key
25
+ # @return [Array, Hash]
26
+ # TOFIX; check if @path are mutated
27
+ def set(obj, value, clone_keys = path.clone)
28
+ # 1. Fetch key
29
+ current_key = clone_keys.shift
30
+ # 2. Prepare object for further processing
31
+ init_value(current_key, obj, clone_keys)
32
+ init_nesting(current_key, obj, clone_keys)
33
+ # 3. Set value - directly or recursively
34
+ set_value(current_key, obj, value, clone_keys)
35
+ recursive_set(current_key, obj, value, clone_keys)
36
+
37
+ return obj
38
+ end
39
+
40
+ # see {#to_ary}
41
+ # @return [Array]
42
+ def to_a
43
+ to_ary
44
+ end
45
+
46
+ # array with keysused to initialize the object
47
+ # @return [Array]
48
+ def to_ary
49
+ path
50
+ end
51
+
52
+ # see {#to_str}
53
+ # @return [String]
54
+ def to_s
55
+ to_str
56
+ end
57
+
58
+ # visual representation of the change key
59
+ # @return [String]
60
+ def to_str
61
+ path.map { |key| "[#{key}]" }.join
62
+ end
63
+
64
+ private
65
+
66
+ attr_reader :path
67
+
68
+ def initialize(path:)
69
+ @path = path.to_ary
70
+ end
71
+
72
+ # prepare an object before further processing
73
+ def init_value(current_key, obj, clone_keys)
74
+ if [ARRAY_VALUE] == clone_keys || NESTED_HASH == clone_keys.first
75
+ obj[current_key] ||= []
76
+ elsif !clone_keys.empty? && ![ARRAY_VALUE, NESTED_HASH].include?(current_key)
77
+ obj[current_key] ||= {}
78
+ end
79
+ end
80
+
81
+ # prepare nesting before further processing
82
+ def init_nesting(current_key, obj, clone_keys)
83
+ element = if NESTED_HASH == current_key
84
+ obj
85
+ elsif NESTED_HASH == clone_keys.first
86
+ obj[current_key]
87
+ end
88
+ element << {} unless element.nil? || element.last.respond_to?(:to_hash)
89
+ end
90
+
91
+ # no more nesting - set value inside object
92
+ def set_value(current_key, obj, value, clone_keys)
93
+ if ARRAY_VALUE == current_key
94
+ obj.prepend(*value)
95
+ clone_keys.pop
96
+ elsif clone_keys.empty?
97
+ obj[current_key] = value
98
+ clone_keys.pop
99
+ elsif [ARRAY_VALUE] == clone_keys
100
+ obj[current_key] = value + obj[current_key]
101
+ clone_keys.pop
102
+ end
103
+ end
104
+
105
+ # recursion for deeply nested values
106
+ def recursive_set(current_key, obj, value, clone_keys)
107
+ if NESTED_HASH == current_key
108
+ set(obj.last, value, clone_keys)
109
+ elsif NESTED_HASH == clone_keys.first
110
+ set(obj[current_key].last, value, clone_keys[1..])
111
+ elsif !clone_keys.empty?
112
+ set(obj[current_key], value, clone_keys)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'delta'
3
+ require_relative 'factories/comparison'
4
4
 
5
5
  module HashDeepDiff
6
6
  # Representation of the recursive difference between two hashes
@@ -11,94 +11,133 @@ module HashDeepDiff
11
11
  #
12
12
  # Examples:
13
13
  # - { one: :a } compared with { one: :b } does not have nesting so we compare keys and values
14
- # - { one: { two: :a, zero: z } } compared with { one: { two: :b, three: :c } } has nesting, so is represented as
14
+ # - { one: { two: :a, zero: :z } } compared with { one: { two: :b, three: :c } } has nesting, so is represented as
15
15
  # - { two: :a } compared with { two: :b, three: :c }, as there is no more nesting we compare keys and values
16
16
  # and have the following comparisons
17
17
  # { one: { two: :a } } compared to { one: { two: :b } } - value was changed
18
18
  # i.e :a vas replaced with :b on path [:one, :two]
19
- # { one: { zero: z } } compared to NO_VALUE - value was deleted
19
+ # { one: { zero: :z } } compared to NO_VALUE - value was deleted
20
20
  # i.e :z vas replaced with NO_VALUE on path [:one, :zero]
21
21
  # NO_VALUE compared to { one: { three: :c } } compared - value was added
22
22
  # i.e NO_VALUE vas replaced with :c on path [:one, :three]
23
23
  # [
24
- # #<HashDeepDiff::Delta
25
- # @delta={:two=>{:left=>:a, :right=>:b}},
26
- # @prefix=[:one],
24
+ # #<HashDeepDiff::Delta:0x00007fc7bc8a6e58
25
+ # @change_key=#<HashDeepDiff::ChangeKey:0x00007fc7bc8a6d40 @path=[:one, :two]>,
27
26
  # @value={:left=>:a, :right=>:b}>,
28
- # #<HashDeepDiff::Delta
29
- # @delta={:zero=>{:left=>:z, :right=>HashDeepDiff::NO_VALUE}},
30
- # @prefix=[:one],
27
+ # #<HashDeepDiff::Delta:0x00007fc7bc8a6b60
28
+ # @change_key=#<HashDeepDiff::ChangeKey:0x00007fc7bc8a6a48 @path=[:one, :zero]>,
31
29
  # @value={:left=>:z, :right=>HashDeepDiff::NO_VALUE}>,
32
- # #<HashDeepDiff::Delta
33
- # @delta={:three=>{:left=>HashDeepDiff::NO_VALUE, :right=>:c}},
34
- # @prefix=[:one],
35
- # @value={:left=>HashDeepDiff::NO_VALUE, :right=>:c}>
30
+ # #<HashDeepDiff::Delta:0x00007fc7bc8a6930
31
+ # @change_key=#<HashDeepDiff::ChangeKey:0x00007fc7bc8a6818 @path=[:one, :three]>,
32
+ # @value={:left=>HashDeepDiff::NO_VALUE, :right=>:c}>]
36
33
  # ]
37
34
  class Comparison
38
- # @!attribute [r] left
39
- # @return [Hash] original version of the Hash
40
- # @!attribute [r] right
41
- # @return [Hash] Hash that the original is compared to
42
- # @!attribute [r] path
43
- # @return [Array<Object>] to a compared Hashes (is empty for top-level comparison)
44
- attr_reader :left, :right, :path
35
+ extend Forwardable
36
+ attr_reader :reporting_engine, :delta_engine
45
37
 
46
- # @return [String]
47
- def report
48
- diff.join("\n")
49
- end
38
+ def_delegators :comparison_factory, :comparison
39
+ def_delegators :report_engine_factory, :report, :raw_report
50
40
 
51
41
  # @return [Array<HashDeepDiff::Delta>]
52
42
  def diff
53
- comparison.flat_map do |delta|
54
- # if there are nested hashes we need to compare them furter
55
- # if no we return difference between values (HashDeepDiff::Delta)
56
- delta.complex? ? self.class.new(delta.left, delta.right, delta.path).diff : delta
57
- end
43
+ return [] if left == right
44
+
45
+ deltas.flat_map { |new_delta| new_delta.simple? ? new_delta : inward_comparison(new_delta) }
46
+ end
47
+
48
+ # @param [Object] key the key which value we're currently comparing
49
+ def left(key = NO_VALUE)
50
+ return NO_VALUE if @left == NO_VALUE
51
+ return @left if key == NO_VALUE
52
+ return @left unless left.respond_to?(:to_hash)
53
+
54
+ @left[key] || NO_VALUE
55
+ end
56
+
57
+ # @param [Object] key the key which value we're currently comparing
58
+ def right(key = NO_VALUE)
59
+ return NO_VALUE if @right == NO_VALUE
60
+ return @right if key == NO_VALUE
61
+ return @right unless right.respond_to?(:to_hash)
62
+
63
+ @right[key] || NO_VALUE
58
64
  end
59
65
 
60
66
  private
61
67
 
62
- # @param [Hash] left original version of the hash
63
- # @param [Hash] right new version of the hash
68
+ # @!attribute [r] path
69
+ # @return [Array<Object>] subset of keys from original Hashes to fetch compared values
70
+ # (is empty for top-level comparison)
71
+ attr_reader :path
72
+
73
+ # @param [Object] original original version
74
+ # @param [Object] changed new version
64
75
  # @param [Array] prefix keys to fetch current comparison (not empty for nested comparisons)
65
- def initialize(left, right, prefix = [])
66
- @left = left.to_hash
67
- @right = right.to_hash
76
+ def initialize(original, changed, prefix = [], reporting_engine: Reports::Diff, delta_engine: Delta)
77
+ @left = original
78
+ @right = changed
68
79
  @path = prefix.to_ary
80
+ @reporting_engine = reporting_engine
81
+ @delta_engine = delta_engine
69
82
  end
70
83
 
84
+ # {Comparison} broken down into array of {Delta}
71
85
  # @return [Array<HashDeepDiff::Delta>]
72
- def comparison
86
+ def deltas
87
+ return [delta] if common_keys.empty?
88
+
73
89
  common_keys.each_with_object([]) do |key, memo|
74
90
  next if values_equal?(key)
75
91
 
76
- memo << Delta.new(path: path + [key], value: { left: value_left(key), right: value_right(key) })
92
+ memo.append(delta(key: key))
93
+ end.flatten
94
+ end
95
+
96
+ # depending on circumstances will return necessary comparisons
97
+ # @return [Array<HashDeepDiff::Delta>]
98
+ def inward_comparison(complex_delta)
99
+ if complex_delta.partial?
100
+ complex_delta.placebo +
101
+ comparison(delta: complex_delta, modifier: :addition).map(&:diff).flatten +
102
+ comparison(delta: complex_delta, modifier: :deletion).map(&:diff).flatten
103
+ else
104
+ comparison(delta: complex_delta).map(&:diff).flatten
77
105
  end
78
106
  end
79
107
 
80
108
  # @param [Object] key the key which value we're currently comparing
81
109
  # @return [Bool]
82
110
  def values_equal?(key)
83
- value_right(key).instance_of?(value_left(key).class) && (value_right(key) == value_left(key))
111
+ right(key).instance_of?(left(key).class) && (right(key) == left(key))
84
112
  end
85
113
 
86
- # Original value
87
- # @param [Object] key the key which value we're currently comparing
88
- def value_left(key)
89
- left[key] || NO_VALUE
114
+ # All keys from both original and compared objects
115
+ # @return [Array]
116
+ def common_keys
117
+ keys = []
118
+ keys += left.keys if left.respond_to?(:keys)
119
+ keys += right.keys if right.respond_to?(:keys)
120
+
121
+ keys.uniq
90
122
  end
91
123
 
92
- # Value we compare to
93
- # @param [Object] key the key which value we're currently comparing
94
- def value_right(key)
95
- right[key] || NO_VALUE
124
+ # factory function
125
+ # @return [HashDeepDiff::Delta]
126
+ def delta(key: NO_VALUE)
127
+ keys = path
128
+ keys += [key] unless key == NO_VALUE
129
+
130
+ HashDeepDiff::Delta.new(path: keys, value: { left: left(key), right: right(key) })
96
131
  end
97
132
 
98
- # All keys from both original and compared objects
99
- # @return [Array]
100
- def common_keys
101
- (left.keys + right.keys).uniq
133
+ # @return [HashDeepDiff::Factories::Comparison]
134
+ def comparison_factory
135
+ HashDeepDiff::Factories::Comparison.new(reporting_engine: reporting_engine)
136
+ end
137
+
138
+ # @return [HashDeepDiff::Reports::Base]
139
+ def report_engine_factory
140
+ reporting_engine.new(diff: diff)
102
141
  end
103
142
  end
104
143
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'acts_as_hash'
4
- require_relative 'report'
3
+ require 'forwardable'
5
4
 
6
5
  module HashDeepDiff
7
6
  # Representation of the diff of two values
@@ -10,74 +9,127 @@ module HashDeepDiff
10
9
  # - diff of { a: a } and { a: b } is { a: { left: a, right: b } }
11
10
  # - diff of {} and { a: b } is { a: { left: HashDeepDiff::NO_VALUE, right: b } }
12
11
  class Delta
13
- include ActsAsHash
12
+ extend Forwardable
14
13
 
15
- # Visual representation of additions and deletiond at given +path+
16
- # @return [String]
17
- def to_str
18
- [deletion, addition].compact.join("\n")
14
+ def_delegators :to_hash, :==, :each_with_object, :each_key, :[],
15
+ :to_a, :empty?, :keys
16
+ attr_reader :change_key
17
+
18
+ # an indication that nested Hash was deleted/added
19
+ # @return [Array<HashDeepDiff::Delta>]
20
+ def placebo
21
+ placebo = simple_left? ? { left: NO_VALUE, right: placebo_elment } : { left: placebo_elment, right: NO_VALUE }
22
+
23
+ [self.class.new(path: change_key, value: placebo)]
19
24
  end
20
25
 
21
- # Returns true if we have nested Hashes
22
- # @return [Bool]
26
+ # true if any value is an +Array+ with hashes
27
+ # @return [TrueClass, FalseClass]
23
28
  def complex?
24
- left.respond_to?(:to_hash) && right.respond_to?(:to_hash)
29
+ complex_left? || complex_right?
30
+ end
31
+
32
+ # true if right part is an +Array+ with hashes
33
+ # @return [TrueClass, FalseClass]
34
+ def complex_right?
35
+ right.respond_to?(:to_ary) && right.any? { |el| el.respond_to?(:to_hash) }
36
+ end
37
+
38
+ # true if left part is an +Array+ with hashes
39
+ # @return [TrueClass, FalseClass]
40
+ def complex_left?
41
+ left.respond_to?(:to_ary) && left.any? { |el| el.respond_to?(:to_hash) }
42
+ end
43
+
44
+ # true if at least one of the values is a Hash
45
+ # @return [TrueClass, FalseClass]
46
+ def partial?
47
+ !composite? && !simple? && !complex_left? && !complex_right?
48
+ end
49
+
50
+ # true if both valus are Hashes
51
+ # @return [TrueClass, FalseClass]
52
+ def composite?
53
+ !simple_left? && !simple_right?
25
54
  end
26
55
 
27
- # Keys needed to fetch values that we're comparing
28
- # @return [Array]
29
- def path
30
- @prefix + [@delta.keys.first]
56
+ # true if none of the values is a Hash
57
+ # @return [TrueClass, FalseClass]
58
+ def simple?
59
+ simple_left? && simple_right?
60
+ end
61
+
62
+ # removed element(s)
63
+ def deletion
64
+ return left unless array_with_array?
65
+
66
+ return left - right
31
67
  end
32
68
 
33
69
  # Original value
34
70
  def left
35
- @value[:left]
71
+ value[:left]
72
+ end
73
+
74
+ # added element(s)
75
+ def addition
76
+ return right unless array_with_array?
77
+
78
+ return right - left
36
79
  end
37
80
 
38
81
  # Value we compare to
39
82
  def right
40
- @value[:right]
83
+ value[:right]
41
84
  end
42
85
 
43
- # See {#to_str}
44
- def to_s
45
- to_str
86
+ # see {#to_hash}
87
+ # @return [Hash]
88
+ def to_h
89
+ to_hash
90
+ end
91
+
92
+ # @return [Hash]
93
+ def to_hash
94
+ { change_key[-1] => value }
46
95
  end
47
96
 
48
97
  private
49
98
 
50
- # @param [Array, Object] path list of keys to fetch values we're comparing
99
+ attr_reader :value
100
+
101
+ # @param [Array] path list of keys to fetch values we're comparing
51
102
  # @param [Hash<(:left, :right), Object>] value +Hash+ object with two keys - :left and :right,
52
103
  # that represents compared original value (at :left) and value we compare to (at :right)
53
104
  def initialize(path:, value:)
54
- # TOFIX this may prohibit usage of hashes with Array keys
55
- # TOFIX extract path to a separate object
56
- if path.respond_to?(:to_ary)
57
- @delta = { path[-1] => value }
58
- @value = value
59
- @prefix = path[0..-2]
60
- else
61
- @delta = { path => value }
62
- @value = value
63
- @prefix = []
64
- end
65
- end
66
-
67
- # Visual representation of additions
68
- # @return [NillClass, String]
69
- def deletion
70
- return nil if left == NO_VALUE
105
+ @value = value
106
+ @change_key = HashDeepDiff::ChangeKey.new(path: path)
107
+ end
71
108
 
72
- Report.new(path: path, value: left, mode: Report::Mode::DELETION)
109
+ # an indication of added/removed nested Hash
110
+ # @return [Array, Hash]
111
+ def placebo_elment
112
+ return [{}] if complex_left? || complex_right?
113
+
114
+ return {}
73
115
  end
74
116
 
75
- # Visual representation of deletions
76
- # @return [NillClass, String]
77
- def addition
78
- return nil if right == NO_VALUE
117
+ # true if left value has no nested Hashes
118
+ # @return [TrueClass, FalseClass]
119
+ def simple_left?
120
+ !left.respond_to?(:to_hash) && !complex_left?
121
+ end
122
+
123
+ # true if right value has no nested Hashes
124
+ # @return [TrueClass, FalseClass]
125
+ def simple_right?
126
+ !right.respond_to?(:to_hash) && !complex_right?
127
+ end
79
128
 
80
- Report.new(path: path, value: right)
129
+ # true if both left and right are arrays
130
+ # @return [TrueClass, FalseClass]
131
+ def array_with_array?
132
+ left.respond_to?(:to_ary) && right.respond_to?(:to_ary)
81
133
  end
82
134
  end
83
135
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module HashDeepDiff
6
+ # factories
7
+ module Factories
8
+ # Factory for {HashDeepDiff::Comparison}
9
+ class Comparison
10
+ extend Forwardable
11
+
12
+ def_delegators :delta, :left, :right, :change_key, :complex?, :complex_left?, :complex_right?
13
+
14
+ # factory function
15
+ # @return [HashDeepDiff::Comparison]
16
+ def comparison(delta:, modifier: :change)
17
+ @delta = delta
18
+
19
+ fragments(modifier).map do |(left, right, change_key)|
20
+ HashDeepDiff::Comparison.new(left, right, change_key,
21
+ delta_engine: delta.class,
22
+ reporting_engine: reporting_engine)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ # @!attribute [r] reporting_engine
29
+ # @return [HashDeepDiff::Reports::Base] descendant of
30
+ # @!attribute [r] delta
31
+ # @return [HashDeepDiff::Delta]
32
+ attr_reader :reporting_engine, :delta
33
+
34
+ def initialize(reporting_engine:)
35
+ @reporting_engine = reporting_engine
36
+ end
37
+
38
+ # entities for further comparison
39
+ # @return [Array]
40
+ def fragments(mode)
41
+ case mode
42
+ when :change
43
+ return [[value_left, value_right, change_key]] unless complex?
44
+
45
+ [[value_left, value_right, change_key + [ChangeKey::ARRAY_VALUE]],
46
+ [nesting_left, nesting_right, change_key + [ChangeKey::NESTED_HASH]]]
47
+ when :deletion
48
+ [[value_left, NO_VALUE, change_key]]
49
+ when :addition
50
+ [[NO_VALUE, value_right, change_key]]
51
+ end
52
+ end
53
+
54
+ # original value without nested hashes
55
+ # @return [Object]
56
+ def value_left
57
+ return NO_VALUE if left.respond_to?(:to_hash) && right.respond_to?(:to_ary)
58
+ return left unless left.respond_to?(:to_ary)
59
+
60
+ left.reject { |el| el.respond_to?(:to_hash) }
61
+ end
62
+
63
+ # changed value without nested hashes
64
+ # @return [Object]
65
+ def value_right
66
+ return NO_VALUE if right.respond_to?(:to_hash) && left.respond_to?(:to_ary)
67
+ return right unless right.respond_to?(:to_ary)
68
+
69
+ right.reject { |el| el.respond_to?(:to_hash) }
70
+ end
71
+
72
+ # nested hashes from original value
73
+ # @return [Array<Hash>]
74
+ def nesting_left
75
+ return left if left.respond_to?(:to_hash)
76
+ return NO_VALUE unless complex_left?
77
+
78
+ left
79
+ .select { |el| el.respond_to?(:to_hash) }
80
+ .each_with_object({}) { |el, memo| memo.merge!(el) }
81
+ end
82
+
83
+ # nested hashes from changed value
84
+ # @return [Array<Hash>]
85
+ def nesting_right
86
+ return right if right.respond_to?(:to_hash)
87
+ return NO_VALUE unless complex_right?
88
+
89
+ right
90
+ .select { |el| el.respond_to?(:to_hash) }
91
+ .each_with_object({}) { |el, memo| memo.merge!(el) }
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HashDeepDiff
4
+ # Different reporting enjines for {Delta}
5
+ module Reports
6
+ # Abstract Class
7
+ class Base
8
+ # raw data for {#report}
9
+ def raw_report
10
+ raise AbstractMethodError
11
+ end
12
+
13
+ # see {#to_str}
14
+ # @return [String]
15
+ def to_s
16
+ to_str
17
+ end
18
+
19
+ # see {#report}
20
+ # @return [String]
21
+ def to_str
22
+ report
23
+ end
24
+
25
+ # A report on additions and deletions
26
+ # @return [String]
27
+ def report
28
+ raise AbstractMethodError
29
+ end
30
+
31
+ private
32
+
33
+ # @!attribute [r] diff
34
+ # @return [Array<HashDeepDiff::Delta>] set of deltas from Comparison of two objects
35
+ attr_reader :diff
36
+
37
+ # @param [Array<HashDeepDiff::Delta>] diff comparison data to report
38
+ def initialize(diff:, change_key_engine: HashDeepDiff::ChangeKey)
39
+ @diff = diff.to_ary
40
+ @change_key = change_key_engine
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module HashDeepDiff
6
+ # Different reporting enjines for {Delta}
7
+ module Reports
8
+ # Visual representation of the {Delta} as diff
9
+ class Diff < Base
10
+ # additiond and deletions represented as diff
11
+ # @return [String]
12
+ def report
13
+ raw_report.map { |delta| original(delta) + replacement(delta) }.join
14
+ end
15
+
16
+ # additiond and deletions raw
17
+ # @return [Array<HashDeepDiff::Delta>]
18
+ def raw_report
19
+ diff
20
+ end
21
+
22
+ private
23
+
24
+ # line of the report with deleted value
25
+ # @return [String]
26
+ def original(delta)
27
+ return '' if delta.left == NO_VALUE
28
+ return "#{deletion}#{delta.change_key} = #{delta.left}\n" unless array_to_array?(delta)
29
+ return '' if array_deletion(delta).empty?
30
+
31
+ "#{deletion}#{delta.change_key} = #{array_deletion(delta)}\n"
32
+ end
33
+
34
+ # line of the report with added value
35
+ # @return [String]
36
+ def replacement(delta)
37
+ return '' if delta.right == NO_VALUE
38
+ return "#{addition}#{delta.change_key} = #{delta.right}\n" unless array_to_array?(delta)
39
+ return '' if array_addition(delta).empty?
40
+
41
+ "#{addition}#{delta.change_key} = #{array_addition(delta)}\n"
42
+ end
43
+
44
+ # returns true if original value and replacement are instances of +Array+
45
+ # @return Bool
46
+ # TOFIX drop
47
+ def array_to_array?(delta)
48
+ delta.left.instance_of?(Array) && delta.right.instance_of?(Array)
49
+ end
50
+
51
+ # added elemnts of array
52
+ # @return [Array]
53
+ def array_addition(delta)
54
+ delta.right - delta.left
55
+ end
56
+
57
+ # added elemnts of array
58
+ # @return [Array]
59
+ def array_deletion(delta)
60
+ delta.left - delta.right
61
+ end
62
+
63
+ # visual indication of addition
64
+ # @return [String]
65
+ def addition
66
+ '+left'
67
+ end
68
+
69
+ # visual indication of deletion
70
+ # @return [String]
71
+ def deletion
72
+ '-left'
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'forwardable'
5
+ require 'yaml'
6
+
7
+ module HashDeepDiff
8
+ # Different reporting enjines for {Delta}
9
+ module Reports
10
+ # Visual representation of the {Delta} as diff
11
+ class Yml < Base
12
+ extend Forwardable
13
+ def_delegators :@change_key, :initial_object, :dig_set
14
+
15
+ # additions and deletions represented as YAML
16
+ # @return [String]
17
+ def report
18
+ YAML.dump(raw_report)
19
+ end
20
+
21
+ # additions and deletiond represented as Hash
22
+ # @return [Hash]
23
+ def raw_report
24
+ @raw = { 'additions' => initial_object(values: additions), 'deletions' => initial_object(values: deletions) }
25
+
26
+ additions.each { |(change_key, addition)| change_key.set(raw['additions'], addition) }
27
+ deletions.each { |(change_key, deletion)| change_key.set(raw['deletions'], deletion) }
28
+
29
+ return raw
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :raw
35
+
36
+ # added values
37
+ # @return [Array<HashDeepDiff::Delta>]
38
+ def additions
39
+ diff.reject { |delta| delta.right == NO_VALUE }
40
+ .map { |delta| [delta.change_key, delta.addition] }
41
+ .reject { |(_, addition)| [] == addition }
42
+ end
43
+
44
+ # deleted values
45
+ # @return [Array<HashDeepDiff::Delta>]
46
+ def deletions
47
+ diff.reject { |delta| delta.left == NO_VALUE }
48
+ .map { |delta| [delta.change_key, delta.deletion] }
49
+ .reject { |(_, deletion)| [] == deletion }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module HashDeepDiff
4
4
  # Version of a gem
5
- VERSION = '0.5.0'
5
+ VERSION = '0.8.0'
6
6
  end
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'hash_deep_diff/version'
4
+ require 'hash_deep_diff/reports/diff'
5
+ require 'hash_deep_diff/reports/yml'
6
+ require 'hash_deep_diff/delta'
7
+ require 'hash_deep_diff/change_key'
4
8
  require 'hash_deep_diff/comparison'
5
9
 
6
10
  # Global namespace
7
11
  module HashDeepDiff
8
12
  # value was not found
9
13
  NO_VALUE = Class.new(NilClass)
14
+ # Abstract method
15
+ AbstractMethodError = Class.new(NoMethodError)
16
+ # Any error
17
+ Error = Class.new(StandardError)
10
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hash_deep_diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bohdan Pohorilets
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-18 00:00:00.000000000 Z
11
+ date: 2022-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -122,6 +122,34 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: 1.5.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: naught
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 1.1.0
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 1.1.0
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-byebug
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 3.9.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 3.9.0
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: rake
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -164,6 +192,20 @@ dependencies:
164
192
  - - "~>"
165
193
  - !ruby/object:Gem::Version
166
194
  version: 0.18.0
195
+ - !ruby/object:Gem::Dependency
196
+ name: rubocop-performance
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: 1.13.3
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: 1.13.3
167
209
  - !ruby/object:Gem::Dependency
168
210
  name: rubocop-rake
169
211
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +220,20 @@ dependencies:
178
220
  - - "~>"
179
221
  - !ruby/object:Gem::Version
180
222
  version: 0.6.0
223
+ - !ruby/object:Gem::Dependency
224
+ name: rubocop-rspec
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: 2.10.0
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: 2.10.0
181
237
  description: Find the exact difference between two Hash objects
182
238
  email:
183
239
  - bohdan.pohorilets@gmail.com
@@ -209,10 +265,13 @@ files:
209
265
  - bin/yri
210
266
  - hash_deep_diff.gemspec
211
267
  - lib/hash_deep_diff.rb
212
- - lib/hash_deep_diff/acts_as_hash.rb
268
+ - lib/hash_deep_diff/change_key.rb
213
269
  - lib/hash_deep_diff/comparison.rb
214
270
  - lib/hash_deep_diff/delta.rb
215
- - lib/hash_deep_diff/report.rb
271
+ - lib/hash_deep_diff/factories/comparison.rb
272
+ - lib/hash_deep_diff/reports/base.rb
273
+ - lib/hash_deep_diff/reports/diff.rb
274
+ - lib/hash_deep_diff/reports/yml.rb
216
275
  - lib/hash_deep_diff/version.rb
217
276
  homepage: https://github.com/bpohoriletz/hash_deep_diff
218
277
  licenses:
@@ -220,6 +279,7 @@ licenses:
220
279
  metadata:
221
280
  allowed_push_host: https://rubygems.org/
222
281
  homepage_uri: https://github.com/bpohoriletz/hash_deep_diff
282
+ documentation_uri: https://rdoc.info/gems/hash_deep_diff
223
283
  source_code_uri: https://github.com/bpohoriletz/hash_deep_diff
224
284
  changelog_uri: https://github.com/bpohoriletz/hash_deep_diff/blob/main/CHANGELOG.md
225
285
  rubygems_mfa_required: 'true'
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'forwardable'
4
-
5
- module HashDeepDiff
6
- # This module includes behavior that is needed to use instances of Delta instead of Hash
7
- # in this gem
8
- module ActsAsHash
9
- # @param [Object] base a hook that is invoked when module is included in a class
10
- def self.included(base)
11
- base.extend Forwardable
12
- base.def_delegators :@delta, :==, :each_with_object, :each_key, :[],
13
- :to_a, :empty?, :keys
14
- base.include InstanceMethods
15
- end
16
-
17
- # We assume that the class will initialize instance variable +@delta+ that will return
18
- # a representation of an instance of a class as a +Hash+ object
19
- module InstanceMethods
20
- # a +Hash+ representation of an object
21
- def to_h
22
- to_hash
23
- end
24
-
25
- # a +Hash+ representation of an object
26
- def to_hash
27
- @delta
28
- end
29
- end
30
- end
31
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module HashDeepDiff
4
- # Visual representation of the difference between two values
5
- class Report
6
- # We have two cases
7
- # * added - when value on the left is missing
8
- # * deleted - when the value on the right is missing
9
- module Mode
10
- # for additions
11
- ADDITION = '+left'
12
- # for deletions
13
- DELETION = '-left'
14
- end
15
-
16
- # A report with all additions and deletions
17
- # @return [String]
18
- def to_str
19
- if @value.respond_to?(:to_hash) && !@value.empty?
20
- [@mode, diff_prefix, ' = ', "{}\n"].join +
21
- @value.keys.map do |key|
22
- Report.new(path: @path + [key], value: @value[key], mode: @mode)
23
- end.join("\n")
24
- else
25
- [@mode, diff_prefix, ' = ', @value.to_s].join
26
- end
27
- end
28
-
29
- # A report with all additions and deletions
30
- # @return [String]
31
- def to_s
32
- to_str
33
- end
34
-
35
- private
36
-
37
- # @param [Array] path Keys from compared objects to fetch the compared values
38
- # @param [Object] value value from a compared object at +@path+
39
- # @param [Mode::ADDITION, Mode::DELETION] mode
40
- def initialize(path:, value:, mode: Mode::ADDITION)
41
- @path = path.to_ary
42
- @value = value
43
- @mode = mode
44
- end
45
-
46
- # Visual representation of keys from compared objects needed to fetch the compared values
47
- # @return [String]
48
- def diff_prefix
49
- # TOFIX poor naming
50
- @path.map { |key| "[#{key}]" }.join
51
- end
52
- end
53
- end