hash_deep_diff 0.5.0 → 0.8.0

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
  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