hash_deep_diff 0.4.1 → 0.7.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: 965ed70dc327febfcab65e1d8a9ae2511e06b117316a368140eae6a00dc8afba
4
- data.tar.gz: f905ef6c174c729ec28f9d585db116deb32727276ae61b1e448f4c17917cbf47
3
+ metadata.gz: dc574fc78f743f8b629ecbb87baebea7e75789d99401f7e54691ad02469572ec
4
+ data.tar.gz: 0f378780b1f08173779e3684a8d42361c0532411c7972952a626bc3e4a0bebb0
5
5
  SHA512:
6
- metadata.gz: 6669aa02e12598277b0d59e0abb1f173a1a28dfb5aa9770e5c209bde4972bb7e9166788dfcae30743fcc95050bf23104e768edae1ecec855f07b72fa704ad46a
7
- data.tar.gz: d4f9fc51fc4acbaaf0228f84c8aa47e84cb1b17dbad6e45df7047df0cf086b7ff6deeab85de648ec5916174b0d12e1a982bac975d9a05ef65609714f23ab91dc
6
+ metadata.gz: dd12f9bc333f1a8ab3d4967bcdaeee7eac9cf66a97883a122cfa3f3e831165da62cce4b4dadf0e5e3c750cc43dcd59629b20c279d215b42ebd2d6fd65ebcc497
7
+ data.tar.gz: 64b6e8dbf48807f23ca32e84cfec991e14ff56ffda7bb65b71a1db3bc7d800ca0f94f00038e49dc1cd2bd30b84ad6c05496de599038d97960dece9f827cde4fb
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --private
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,12 @@ 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
33
+
34
+ guard 'yard' do
35
+ watch(%r{lib/.+\.rb})
36
+ 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 by liufengyun](https://github.com/liufengyun/hashdiff) and [hash_hdiff by CodingZeal](https://github.com/CodingZeal/hash_diff)
9
11
 
10
12
  ## Installation
11
13
 
@@ -36,6 +38,19 @@ HashDeepDiff::Comparison.new(left, right).report
36
38
  - left[a] = a
37
39
  + left[a] = b
38
40
  ```
41
+ please see [Documentation](https://rdoc.info/gems/hash_deep_diff/HashDeepDiff/Comparison) for
42
+ more information or [Reporting test](https://github.com/bpohoriletz/hash_deep_diff/blob/a525d239189b0310aec3741dfc4862834805252d/test/integration/locales/test_uk_ru.rb#L59)
43
+
44
+ ## Customization
45
+
46
+ 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`
47
+
48
+ ```ruby
49
+ left = { a: :a }
50
+ right = { a: :b }
51
+
52
+ HashDeepDiff::Comparison.new(left, right, reporting_engine: CustomEngine).report
53
+ ```
39
54
 
40
55
  ## Contributing
41
56
 
data/bin/yard ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yard' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("yard", "yard")
data/bin/yardoc ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yardoc' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("yard", "yardoc")
data/bin/yri ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yri' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("yard", "yri")
@@ -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'
@@ -43,9 +44,12 @@ Gem::Specification.new do |spec|
43
44
  spec.add_development_dependency 'guard', '~> 2.18.0'
44
45
  spec.add_development_dependency 'guard-minitest', '~> 2.4.6'
45
46
  spec.add_development_dependency 'guard-rubocop', '~> 1.5.0'
47
+ spec.add_development_dependency 'guard-yard', '~> 2.2.1'
46
48
  spec.add_development_dependency 'minitest', '~> 5.15.0'
47
49
  spec.add_development_dependency 'minitest-focus', '~> 1.3.1'
48
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'
49
53
  spec.add_development_dependency 'rake', '~> 10.5.0'
50
54
  spec.add_development_dependency 'rubocop', '~> 1.26.1'
51
55
  spec.add_development_dependency 'rubocop-minitest', '~> 0.18.0'
@@ -1,52 +1,149 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'delta'
3
+ require_relative 'factories/comparison'
4
4
 
5
- # :nodoc:
6
5
  module HashDeepDiff
7
- # An instrument to build and report the difference between two hash-like objects
6
+ # Representation of the recursive difference between two hashes
7
+ # main parts are
8
+ # * path - empty for original hashes, otherwise path to values being compared
9
+ # * left - basically left.dig(path), left value of two being compared
10
+ # * right - basically right.dig(path), right value of two being compared
11
+ #
12
+ # Examples:
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
15
+ # - { two: :a } compared with { two: :b, three: :c }, as there is no more nesting we compare keys and values
16
+ # and have the following comparisons
17
+ # { one: { two: :a } } compared to { one: { two: :b } } - value was changed
18
+ # i.e :a vas replaced with :b on path [:one, :two]
19
+ # { one: { zero: :z } } compared to NO_VALUE - value was deleted
20
+ # i.e :z vas replaced with NO_VALUE on path [:one, :zero]
21
+ # NO_VALUE compared to { one: { three: :c } } compared - value was added
22
+ # i.e NO_VALUE vas replaced with :c on path [:one, :three]
23
+ # [
24
+ # #<HashDeepDiff::Delta
25
+ # @delta={:two=>{:left=>:a, :right=>:b}},
26
+ # @prefix=[:one],
27
+ # @value={:left=>:a, :right=>:b}>,
28
+ # #<HashDeepDiff::Delta
29
+ # @delta={:zero=>{:left=>:z, :right=>HashDeepDiff::NO_VALUE}},
30
+ # @prefix=[:one],
31
+ # @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}>
36
+ # ]
8
37
  class Comparison
9
- attr_reader :left, :right, :path
38
+ extend Forwardable
39
+ # @!attribute [r] left
40
+ # @return [Hash] original version of the Hash
41
+ # @!attribute [r] right
42
+ # @return [Hash] Hash that the original is compared to
43
+ # @!attribute [r] path
44
+ # @return [Array<Object>] subset of keys from original Hashes to fetch compared values
45
+ # (is empty for top-level comparison)
46
+ attr_reader :reporting_engine, :delta_engine
10
47
 
48
+ def_delegators :comparison_factory, :comparison
49
+
50
+ # @return [String]
51
+ def report
52
+ diff.map { |simple_delta| reporting_engine.new(delta: simple_delta).to_s }.join
53
+ end
54
+
55
+ # @return [Array<HashDeepDiff::Delta>]
11
56
  def diff
12
- deep_delta
57
+ return [] if left == right
58
+
59
+ deltas.flat_map { |new_delta| new_delta.simple? ? new_delta : inward_comparison(new_delta) }
13
60
  end
14
61
 
15
- def report
16
- diff.join("\n")
62
+ # @param [Object] key the key which value we're currently comparing
63
+ def left(key = NO_VALUE)
64
+ return NO_VALUE if @left == NO_VALUE
65
+ return @left if key == NO_VALUE
66
+ return @left unless left.respond_to?(:to_hash)
67
+
68
+ @left[key] || NO_VALUE
17
69
  end
18
70
 
19
- private
71
+ # @param [Object] key the key which value we're currently comparing
72
+ def right(key = NO_VALUE)
73
+ return NO_VALUE if @right == NO_VALUE
74
+ return @right if key == NO_VALUE
75
+ return @right unless right.respond_to?(:to_hash)
20
76
 
21
- def initialize(left, right, path = [])
22
- @left = left.to_hash
23
- @right = right.to_hash
24
- @path = path.to_ary
77
+ @right[key] || NO_VALUE
25
78
  end
26
79
 
27
- def deep_delta
28
- delta.flat_map do |diff|
29
- if diff.complex?
30
- self.class.new(diff.left, diff.right, diff.path).diff
31
- else
32
- diff
33
- end
34
- end
80
+ private
81
+
82
+ attr_reader :path
83
+
84
+ # @param [Object] original original version
85
+ # @param [Object] changed new version
86
+ # @param [Array] prefix keys to fetch current comparison (not empty for nested comparisons)
87
+ def initialize(original, changed, prefix = [], reporting_engine: Reports::Diff, delta_engine: Delta)
88
+ @left = original
89
+ @right = changed
90
+ @path = prefix.to_ary
91
+ @reporting_engine = reporting_engine
92
+ @delta_engine = delta_engine
35
93
  end
36
94
 
37
- def delta
95
+ # {Comparison} broken down into array of {Delta}
96
+ # @return [Array<HashDeepDiff::Delta>]
97
+ def deltas
98
+ return [delta] if common_keys.empty?
99
+
38
100
  common_keys.each_with_object([]) do |key, memo|
39
- value_left = left[key] || NO_VALUE
40
- value_right = right[key] || NO_VALUE
101
+ next if values_equal?(key)
41
102
 
42
- next if value_right.instance_of?(value_left.class) && (value_right == value_left)
103
+ memo.append(delta(key: key))
104
+ end.flatten
105
+ end
43
106
 
44
- memo << Delta.new(path: path + [key], value: { left: value_left, right: value_right })
107
+ # depending on circumstances will return necessary comparisons
108
+ # @return [Array<HashDeepDiff::Delta>]
109
+ def inward_comparison(complex_delta)
110
+ if complex_delta.partial?
111
+ complex_delta.placebo +
112
+ comparison(delta: complex_delta, modifier: :addition).map(&:diff).flatten +
113
+ comparison(delta: complex_delta, modifier: :deletion).map(&:diff).flatten
114
+ else
115
+ comparison(delta: complex_delta).map(&:diff).flatten
45
116
  end
46
117
  end
47
118
 
119
+ # @param [Object] key the key which value we're currently comparing
120
+ # @return [Bool]
121
+ def values_equal?(key)
122
+ right(key).instance_of?(left(key).class) && (right(key) == left(key))
123
+ end
124
+
125
+ # All keys from both original and compared objects
126
+ # @return [Array]
48
127
  def common_keys
49
- (left.keys + right.keys).uniq
128
+ keys = []
129
+ keys += left.keys if left.respond_to?(:keys)
130
+ keys += right.keys if right.respond_to?(:keys)
131
+
132
+ keys.uniq
133
+ end
134
+
135
+ # @return [HashDeepDiff::Factories::Comparison]
136
+ def comparison_factory
137
+ HashDeepDiff::Factories::Comparison.new(reporting_engine: reporting_engine)
138
+ end
139
+
140
+ # factory function
141
+ # @return [HashDeepDiff::Delta]
142
+ def delta(key: NO_VALUE)
143
+ change_key = path
144
+ change_key += [key] unless key == NO_VALUE
145
+
146
+ HashDeepDiff::Delta.new(change_key: change_key, value: { left: left(key), right: right(key) })
50
147
  end
51
148
  end
52
149
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'acts_as_hash'
3
+ require 'forwardable'
4
4
 
5
5
  module HashDeepDiff
6
6
  # Representation of the diff of two values
@@ -9,72 +9,107 @@ module HashDeepDiff
9
9
  # - diff of { a: a } and { a: b } is { a: { left: a, right: b } }
10
10
  # - diff of {} and { a: b } is { a: { left: HashDeepDiff::NO_VALUE, right: b } }
11
11
  class Delta
12
- include ActsAsHash
12
+ extend Forwardable
13
13
 
14
- def to_str
15
- [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(change_key: change_key, value: placebo)]
16
24
  end
17
25
 
26
+ # true if any value is an +Array+ with hashes
27
+ # @return [TrueClass, FalseClass]
18
28
  def complex?
19
- 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?
20
54
  end
21
55
 
22
- # TOFIX poor naming
23
- # overrides parameter in initializer
24
- def path
25
- @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?
26
60
  end
27
61
 
62
+ # Original value
28
63
  def left
29
- @value[:left]
64
+ value[:left]
30
65
  end
31
66
 
67
+ # Value we compare to
32
68
  def right
33
- @value[:right]
69
+ value[:right]
34
70
  end
35
71
 
36
- def to_s
37
- to_str
72
+ # see {#to_hash}
73
+ # @return [Hash]
74
+ def to_h
75
+ to_hash
76
+ end
77
+
78
+ # @return [Hash]
79
+ def to_hash
80
+ { change_key[-1] => value }
38
81
  end
39
82
 
40
83
  private
41
84
 
42
- def initialize(path:, value:)
43
- # TOFIX this may prohibit usage of hashes with Array keys
44
- if path.respond_to?(:to_ary)
45
- @delta = { path[-1] => value }
46
- @value = value
47
- @prefix = path[0..-2]
48
- else
49
- @delta = { path => value }
50
- @value = value
51
- @prefix = []
52
- end
85
+ attr_reader :value
86
+
87
+ # @param [Array] change_key list of keys to fetch values we're comparing
88
+ # @param [Hash<(:left, :right), Object>] value +Hash+ object with two keys - :left and :right,
89
+ # that represents compared original value (at :left) and value we compare to (at :right)
90
+ def initialize(change_key:, value:)
91
+ @value = value
92
+ @change_key = change_key
53
93
  end
54
94
 
55
- def deletion
56
- return nil if left == NO_VALUE
95
+ # an indication of added/removed nested Hash
96
+ # @return [Array, Hash]
97
+ def placebo_elment
98
+ return [{}] if complex_left? || complex_right?
57
99
 
58
- if left.respond_to?(:to_hash)
59
- left.keys.map { |key| "-left#{diff_prefix}[#{key}] = #{left[key]}" }.join("\n")
60
- else
61
- "-left#{diff_prefix} = #{left}"
62
- end
100
+ return {}
63
101
  end
64
102
 
65
- def addition
66
- return nil if right == NO_VALUE
67
-
68
- if right.respond_to?(:to_hash)
69
- right.keys.map { |key| "+left#{diff_prefix}[#{key}] = #{right[key]}" }.join("\n")
70
- else
71
- "+left#{diff_prefix} = #{right}"
72
- end
103
+ # true if left value has no nested Hashes
104
+ # @return [TrueClass, FalseClass]
105
+ def simple_left?
106
+ !left.respond_to?(:to_hash) && !complex_left?
73
107
  end
74
108
 
75
- # TOFIX poor naming
76
- def diff_prefix
77
- path.map { |key| "[#{key}]" }.join
109
+ # true if right value has no nested Hashes
110
+ # @return [TrueClass, FalseClass]
111
+ def simple_right?
112
+ !right.respond_to?(:to_hash) && !complex_right?
78
113
  end
79
114
  end
80
115
  end
@@ -0,0 +1,91 @@
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 + ['...']],
46
+ [nesting_left, nesting_right, change_key + ['{}']]]
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 left unless left.respond_to?(:to_ary)
58
+
59
+ left.reject { |el| el.respond_to?(:to_hash) }
60
+ end
61
+
62
+ # changed value without nested hashes
63
+ # @return [Object]
64
+ def value_right
65
+ return right unless right.respond_to?(:to_ary)
66
+
67
+ right.reject { |el| el.respond_to?(:to_hash) }
68
+ end
69
+
70
+ # nested hashes from original value
71
+ # @return [Array<Hash>]
72
+ def nesting_left
73
+ return NO_VALUE unless complex_left?
74
+
75
+ left
76
+ .select { |el| el.respond_to?(:to_hash) }
77
+ .each_with_object({}) { |el, memo| memo.merge!(el) }
78
+ end
79
+
80
+ # nested hashes from changed value
81
+ # @return [Array<Hash>]
82
+ def nesting_right
83
+ return NO_VALUE unless complex_right?
84
+
85
+ right
86
+ .select { |el| el.respond_to?(:to_hash) }
87
+ .each_with_object({}) { |el, memo| memo.merge!(el) }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,49 @@
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
+ # see {#to_str}
9
+ # @return [String]
10
+ def to_s
11
+ to_str
12
+ end
13
+
14
+ # A report on additions and deletions
15
+ # @return [String]
16
+ def to_str
17
+ original + replacement
18
+ end
19
+
20
+ private
21
+
22
+ # @!attribute [r] old_val
23
+ # @return [Object] original value
24
+ # @!attribute [r] new_val
25
+ # @return [Object] replacement of the original value
26
+ # @!attribute [r] change_key
27
+ # @return [Array<Object>] subset of keys from original Hashes to fetch reported values
28
+ # (is empty for top-level comparison)
29
+ attr_reader :old_val, :new_val, :change_key
30
+
31
+ # @param [Delta] delta diff to report
32
+ def initialize(delta:)
33
+ @change_key = delta.change_key.to_ary
34
+ @old_val = delta.left
35
+ @new_val = delta.right
36
+ end
37
+
38
+ # old value
39
+ def original
40
+ raise AbstractMethodError
41
+ end
42
+
43
+ # new value
44
+ def replacement
45
+ raise AbstractMethodError
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,69 @@
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
+ private
11
+
12
+ # line of the report with deleted value
13
+ # @return [String]
14
+ def original
15
+ return '' if old_val == NO_VALUE
16
+ return "#{deletion}#{path} = #{old_val}\n" unless array_to_array?
17
+ return '' if array_deletion.empty?
18
+
19
+ "#{deletion}#{path} = #{array_deletion}\n"
20
+ end
21
+
22
+ # line of the report with added value
23
+ # @return [String]
24
+ def replacement
25
+ return '' if new_val == NO_VALUE
26
+ return "#{addition}#{path} = #{new_val}\n" unless array_to_array?
27
+ return '' if array_addition.empty?
28
+
29
+ "#{addition}#{path} = #{array_addition}\n"
30
+ end
31
+
32
+ # returns true if original value and replacement are instances of +Array+
33
+ # @return Bool
34
+ def array_to_array?
35
+ old_val.instance_of?(Array) && new_val.instance_of?(Array)
36
+ end
37
+
38
+ # added elemnts of array
39
+ # @return [Array]
40
+ def array_addition
41
+ new_val - old_val
42
+ end
43
+
44
+ # added elemnts of array
45
+ # @return [Array]
46
+ def array_deletion
47
+ old_val - new_val
48
+ end
49
+
50
+ # Visual representation of keys from compared objects needed to fetch the compared values
51
+ # @return [String]
52
+ def path
53
+ change_key.map { |key| "[#{key}]" }.join
54
+ end
55
+
56
+ # visual indication of addition
57
+ # @return [String]
58
+ def addition
59
+ '+left'
60
+ end
61
+
62
+ # visual indication of deletion
63
+ # @return [String]
64
+ def deletion
65
+ '-left'
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HashDeepDiff
4
- VERSION = '0.4.1'
4
+ # Version of a gem
5
+ VERSION = '0.7.0'
5
6
  end
@@ -1,9 +1,16 @@
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/delta'
4
6
  require 'hash_deep_diff/comparison'
5
7
 
8
+ # Global namespace
6
9
  module HashDeepDiff
10
+ # value was not found
7
11
  NO_VALUE = Class.new(NilClass)
8
- class Error < StandardError; end
12
+ # Abstract method
13
+ AbstractMethodError = Class.new(NoMethodError)
14
+ # Any error
15
+ Error = Class.new(StandardError)
9
16
  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.4.1
4
+ version: 0.7.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-17 00:00:00.000000000 Z
11
+ date: 2022-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 1.5.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.2.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.2.1
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: minitest
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +122,34 @@ dependencies:
108
122
  - - "~>"
109
123
  - !ruby/object:Gem::Version
110
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
111
153
  - !ruby/object:Gem::Dependency
112
154
  name: rake
113
155
  requirement: !ruby/object:Gem::Requirement
@@ -176,6 +218,7 @@ files:
176
218
  - ".github/workflows/ci.yml"
177
219
  - ".gitignore"
178
220
  - ".rubocop.yml"
221
+ - ".yardopts"
179
222
  - CHANGELOG.md
180
223
  - Gemfile
181
224
  - Guardfile
@@ -189,11 +232,16 @@ files:
189
232
  - bin/rake
190
233
  - bin/rubocop
191
234
  - bin/setup
235
+ - bin/yard
236
+ - bin/yardoc
237
+ - bin/yri
192
238
  - hash_deep_diff.gemspec
193
239
  - lib/hash_deep_diff.rb
194
- - lib/hash_deep_diff/acts_as_hash.rb
195
240
  - lib/hash_deep_diff/comparison.rb
196
241
  - lib/hash_deep_diff/delta.rb
242
+ - lib/hash_deep_diff/factories/comparison.rb
243
+ - lib/hash_deep_diff/reports/base.rb
244
+ - lib/hash_deep_diff/reports/diff.rb
197
245
  - lib/hash_deep_diff/version.rb
198
246
  homepage: https://github.com/bpohoriletz/hash_deep_diff
199
247
  licenses:
@@ -201,6 +249,7 @@ licenses:
201
249
  metadata:
202
250
  allowed_push_host: https://rubygems.org/
203
251
  homepage_uri: https://github.com/bpohoriletz/hash_deep_diff
252
+ documentation_uri: https://rdoc.info/gems/hash_deep_diff
204
253
  source_code_uri: https://github.com/bpohoriletz/hash_deep_diff
205
254
  changelog_uri: https://github.com/bpohoriletz/hash_deep_diff/blob/main/CHANGELOG.md
206
255
  rubygems_mfa_required: 'true'
@@ -1,27 +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 deltas instead of Hash inside this gem
7
- module ActsAsHash
8
- def self.included(base)
9
- base.include(InstanceMethods)
10
- base.extend(Forwardable)
11
- base.def_delegators :@delta, :==, :each_with_object, :each_key, :[],
12
- :to_a, :empty?, :keys
13
- end
14
-
15
- # Assumes that the class will include method delta that will return a representation of an
16
- # instance of a class as a Hash
17
- module InstanceMethods
18
- def to_h
19
- @delta
20
- end
21
-
22
- def to_hash
23
- @delta
24
- end
25
- end
26
- end
27
- end