hash_deep_diff 0.4.1 → 0.7.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: 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