hash_deep_diff 0.4.0 → 0.6.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: 24756c15f3f48bbe108e5c2bb12c2db3e6030cef78ab91a6cf76044ca70d2dd5
4
- data.tar.gz: a8cca8ab8a23c704f6f69c2613ded6b4d0d2c7b0e5caed34d437fe0951582a45
3
+ metadata.gz: ddbaa8e8f12ba99330973c609c00ab710e9fa39b369797b80dd46f24aa6740e1
4
+ data.tar.gz: 6a5954a960ca0876cdb9e28f3e535be687de5596ed07be61581102b75b178ab4
5
5
  SHA512:
6
- metadata.gz: 731ccc90f750c6796374ae83ccf9c78741e47251c7940add8f0e9c9c34417f84f8375e74c639891c4e37e5976200bb8786e4bd2dbbe9357bb9b92e8a14b46c7b
7
- data.tar.gz: 8dfbab0f3e5fd2666e2d5eb3458d9df858e204e49cd604348676af43c511bd855dba61720702cc69c7e3ecd0751e29606eec10bef41ee1a0e8bdd4ba33db79f7
6
+ metadata.gz: af66a50e5a7eb848c96b757e8e37a4d767868de6fd96ebfd4f528085111e2fe1c9e39215b8514d534d33f94cfc4c024c5c2bba1813acad729fa906e18eee796a
7
+ data.tar.gz: 358e25298ee3c42ea469e44113fe62935ec8f564c4111c52e170bd25764d99cc4b71c42d154689ad2a9c54ff21f9359dad57cfbd450677853eb60655b90aecf4
@@ -0,0 +1,39 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+
5
+ jobs:
6
+ minitest:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - name: Checkout Repo
10
+ uses: actions/checkout@v2
11
+
12
+ - name: Install Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: 2.6
16
+ bundler-cache: false
17
+
18
+ - name: Install Gems
19
+ run: bin/bundle
20
+
21
+ - name: Run Minitest
22
+ run: bin/rake test
23
+ rubocop:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - name: Checkout Repo
27
+ uses: actions/checkout@v2
28
+
29
+ - name: Install Ruby
30
+ uses: ruby/setup-ruby@v1
31
+ with:
32
+ ruby-version: 2.6
33
+ bundler-cache: false
34
+
35
+ - name: Install Gems
36
+ run: bin/bundle
37
+
38
+ - name: Run Rubocop
39
+ run: bin/rubocop
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --private
data/Guardfile CHANGED
@@ -20,14 +20,17 @@
20
20
  guard :rubocop do
21
21
  watch(/.+\.rb$/)
22
22
  watch(/.+\.gemspec$/)
23
- watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
24
23
  end
25
24
 
26
25
  guard :minitest do
27
26
  # with Minitest::Unit
28
27
  watch(%r{^test/(.*)/?test_(.*)\.rb$})
29
- # watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
30
- 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' }
31
30
  watch(%r{^test/test_helper\.rb$}) { 'test' }
32
31
  watch(%r{^test/support/.*$}) { 'test' }
33
32
  end
33
+
34
+ guard 'yard' do
35
+ watch(%r{lib/.+\.rb})
36
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 - 2022 Bohdan Pohorilets
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # HashDeepDiff
2
+ [![Gem
3
+ Version](https://badge.fury.io/rb/hash_deep_diff.svg)](https://badge.fury.io/rb/hash_deep_diff) ![GitHub Workflow
4
+ Status](https://img.shields.io/github/workflow/status/bpohoriletz/hash_deep_diff/CI)
5
+ ![GitHub](https://img.shields.io/github/license/bpohoriletz/hash_deep_diff)
2
6
 
3
- Find the exact difference between two Hash objects and build a report to visualize it
4
7
 
5
- [![Gem
6
- Version](https://badge.fury.io/rb/hash_deep_diff.svg)](https://badge.fury.io/rb/hash_deep_diff)
8
+ Find the exact difference between two Hash objects and build a report to visualize it
7
9
 
8
10
  ## Installation
9
11
 
@@ -34,6 +36,19 @@ HashDeepDiff::Comparison.new(left, right).report
34
36
  - left[a] = a
35
37
  + left[a] = b
36
38
  ```
39
+ please see [Documentation](https://rdoc.info/gems/hash_deep_diff/HashDeepDiff/Comparison) for
40
+ more information
41
+
42
+ ## Customization
43
+
44
+ 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`
45
+
46
+ ```ruby
47
+ left = { a: :a }
48
+ right = { a: :b }
49
+
50
+ HashDeepDiff::Comparison.new(left, right, reporting_engine: CustomEngine).report
51
+ ```
37
52
 
38
53
  ## Contributing
39
54
 
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/testtask'
5
5
 
6
6
  Rake::TestTask.new do |t|
7
7
  t.libs << 'test'
8
- t.test_files = FileList['test/**/*_spec.rb']
8
+ t.test_files = FileList['test/hash_deep_diff/test_*.rb']
9
9
  t.verbose = true
10
10
  end
11
11
 
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'
@@ -39,13 +40,15 @@ Gem::Specification.new do |spec|
39
40
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
40
41
  spec.require_paths = ['lib']
41
42
 
42
- spec.add_development_dependency 'bundler', '~> 1.17.2'
43
+ spec.add_development_dependency 'bundler', '~> 2.3.11'
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'
49
52
  spec.add_development_dependency 'rake', '~> 10.5.0'
50
53
  spec.add_development_dependency 'rubocop', '~> 1.26.1'
51
54
  spec.add_development_dependency 'rubocop-minitest', '~> 0.18.0'
@@ -1,52 +1,150 @@
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
+
67
+ @left[key] || NO_VALUE
17
68
  end
18
69
 
19
- private
70
+ # @param [Object] key the key which value we're currently comparing
71
+ def right(key = NO_VALUE)
72
+ return NO_VALUE if @right == NO_VALUE
73
+ return @right if key == NO_VALUE
20
74
 
21
- def initialize(left, right, path = [])
22
- @left = left.to_hash
23
- @right = right.to_hash
24
- @path = path.to_ary
75
+ @right[key] || NO_VALUE
25
76
  end
26
77
 
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
78
+ private
79
+
80
+ attr_reader :path
81
+
82
+ # @param [Object] original original version
83
+ # @param [Object] changed new version
84
+ # @param [Array] prefix keys to fetch current comparison (not empty for nested comparisons)
85
+ def initialize(original, changed, prefix = [], reporting_engine: Reports::Diff, delta_engine: Delta)
86
+ @left = original
87
+ @right = changed
88
+ @path = prefix.to_ary
89
+ @reporting_engine = reporting_engine
90
+ @delta_engine = delta_engine
35
91
  end
36
92
 
37
- def delta
93
+ # {Comparison} broken down into array of {Delta}
94
+ # @return [Array<HashDeepDiff::Delta>]
95
+ def deltas
96
+ return [delta] if common_keys.empty?
97
+
38
98
  common_keys.each_with_object([]) do |key, memo|
39
- value_left = left[key] || NO_VALUE
40
- value_right = right[key] || NO_VALUE
99
+ next if values_equal?(key)
41
100
 
42
- next if value_right.instance_of?(value_left.class) && (value_right == value_left)
101
+ memo << delta(key: key)
102
+ end
103
+ end
43
104
 
44
- memo << Delta.new(path: path + [key], value: { left: value_left, right: value_right })
105
+ # depending on circumstances will return necessary comparisons
106
+ # @return [Array<HashDeepDiff::Delta>]
107
+ def inward_comparison(complex_delta)
108
+ if complex_delta.partial?
109
+ [
110
+ complex_delta.placebo,
111
+ comparison(delta: complex_delta, modifier: :right).diff,
112
+ comparison(delta: complex_delta, modifier: :left).diff
113
+ ].compact.flatten
114
+ # TOFIX add test an drop flatten
115
+ else
116
+ comparison(delta: complex_delta).diff
45
117
  end
46
118
  end
47
119
 
120
+ # @param [Object] key the key which value we're currently comparing
121
+ # @return [Bool]
122
+ def values_equal?(key)
123
+ right(key).instance_of?(left(key).class) && (right(key) == left(key))
124
+ end
125
+
126
+ # All keys from both original and compared objects
127
+ # @return [Array]
48
128
  def common_keys
49
- (left.keys + right.keys).uniq
129
+ keys = []
130
+ keys += left.keys if left.respond_to?(:keys)
131
+ keys += right.keys if right.respond_to?(:keys)
132
+
133
+ keys.uniq
134
+ end
135
+
136
+ # @return [HashDeepDiff::Factories::Comparison]
137
+ def comparison_factory
138
+ HashDeepDiff::Factories::Comparison.new(reporting_engine: reporting_engine)
139
+ end
140
+
141
+ # factory function
142
+ # @return [HashDeepDiff::Delta]
143
+ def delta(key: NO_VALUE)
144
+ change_key = path
145
+ change_key += [key] unless key == NO_VALUE
146
+
147
+ HashDeepDiff::Delta.new(change_key: change_key, value: { left: left(key), right: right(key) })
50
148
  end
51
149
  end
52
150
  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,83 @@ 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 [HashDeepDiff::Delta, NilClass]
20
+ def placebo
21
+ return nil unless partial?
22
+
23
+ placebo = simple_left? ? { left: NO_VALUE, right: {} } : { left: {}, right: NO_VALUE }
24
+
25
+ self.class.new(change_key: change_key, value: placebo)
26
+ end
27
+
28
+ # true if at least one of the values is a Hash
29
+ # @return [Bool]
30
+ def partial?
31
+ !composite? && !simple?
16
32
  end
17
33
 
18
- def complex?
19
- left.respond_to?(:to_hash) && right.respond_to?(:to_hash)
34
+ # true if both valus are Hashes
35
+ # @return [Bool]
36
+ def composite?
37
+ !simple_left? && !simple_right?
20
38
  end
21
39
 
22
- # TOFIX poor naming
23
- # overrides parameter in initializer
24
- def path
25
- @prefix + [@delta.keys.first]
40
+ # true if none of the values is a Hash
41
+ # @return [Bool]
42
+ def simple?
43
+ simple_left? && simple_right?
26
44
  end
27
45
 
46
+ # Original value
28
47
  def left
29
- @value[:left]
48
+ value[:left]
30
49
  end
31
50
 
51
+ # Value we compare to
32
52
  def right
33
- @value[:right]
53
+ value[:right]
34
54
  end
35
55
 
36
- def to_s
37
- to_str
56
+ # see {#to_hash}
57
+ # @return [Hash]
58
+ def to_h
59
+ to_hash
38
60
  end
39
61
 
40
- private
41
-
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
62
+ # @return [Hash]
63
+ def to_hash
64
+ { change_key[-1] => value }
53
65
  end
54
66
 
55
- def deletion
56
- return nil if left == NO_VALUE
67
+ private
57
68
 
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
63
- end
69
+ attr_reader :value
64
70
 
65
- def addition
66
- return nil if right == NO_VALUE
71
+ # @param [Array] change_key list of keys to fetch values we're comparing
72
+ # @param [Hash<(:left, :right), Object>] value +Hash+ object with two keys - :left and :right,
73
+ # that represents compared original value (at :left) and value we compare to (at :right)
74
+ def initialize(change_key:, value:)
75
+ @value = value
76
+ @change_key = change_key
77
+ end
67
78
 
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
79
+ # Returns true if left value has no nested Hashes
80
+ # @return [Bool]
81
+ def simple_left?
82
+ !left.respond_to?(:to_hash)
73
83
  end
74
84
 
75
- # TOFIX poor naming
76
- def diff_prefix
77
- path.map { |key| "[#{key}]" }.join
85
+ # Returns true if right value has no nested Hashes
86
+ # @return [Bool]
87
+ def simple_right?
88
+ !right.respond_to?(:to_hash)
78
89
  end
79
90
  end
80
91
  end
@@ -0,0 +1,58 @@
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
+ def_delegators :delta, :left, :right, :change_key
12
+
13
+ # factory function
14
+ # @return [Comparison]
15
+ def comparison(delta:, modifier: nil)
16
+ @delta = delta
17
+
18
+ case modifier
19
+ when nil
20
+ full_compare
21
+ when :left
22
+ compare_left
23
+ when :right
24
+ compare_right
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :reporting_engine, :delta
31
+
32
+ def initialize(reporting_engine:)
33
+ @reporting_engine = reporting_engine
34
+ end
35
+
36
+ # compare two hashes
37
+ def full_compare
38
+ HashDeepDiff::Comparison.new(left, right, change_key,
39
+ delta_engine: delta.class,
40
+ reporting_engine: reporting_engine)
41
+ end
42
+
43
+ # compare Hash with nothing (deletion)
44
+ def compare_left
45
+ HashDeepDiff::Comparison.new(left, NO_VALUE, change_key,
46
+ delta_engine: delta.class,
47
+ reporting_engine: reporting_engine)
48
+ end
49
+
50
+ # compare nothing with Hash (addition)
51
+ def compare_right
52
+ HashDeepDiff::Comparison.new(NO_VALUE, right, change_key,
53
+ delta_engine: delta.class,
54
+ reporting_engine: reporting_engine)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,42 @@
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
+ attr_reader :old_val, :new_val, :change_key
23
+
24
+ # @param [Delta] delta diff to report
25
+ def initialize(delta:)
26
+ @change_key = delta.change_key.to_ary
27
+ @old_val = delta.left
28
+ @new_val = delta.right
29
+ end
30
+
31
+ # old value
32
+ def original
33
+ raise AbstractMethodError
34
+ end
35
+
36
+ # new value
37
+ def replacement
38
+ raise AbstractMethodError
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
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
+ # old value
13
+ # @return [String]
14
+ def original
15
+ return '' if old_val == NO_VALUE
16
+
17
+ return "#{deletion}#{path} = #{old_val}\n"
18
+ end
19
+
20
+ # new value
21
+ # @return [String]
22
+ def replacement
23
+ return '' if new_val == NO_VALUE
24
+
25
+ return "#{addition}#{path} = #{new_val}\n"
26
+ end
27
+
28
+ # Visual representation of keys from compared objects needed to fetch the compared values
29
+ # @return [String]
30
+ def path
31
+ change_key.map { |key| "[#{key}]" }.join
32
+ end
33
+
34
+ # visual indication of addition
35
+ # @return [String]
36
+ def addition
37
+ '+left'
38
+ end
39
+
40
+ # visual indication of deletion
41
+ # @return [String]
42
+ def deletion
43
+ '-left'
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HashDeepDiff
4
- VERSION = '0.4.0'
4
+ # Version of a gem
5
+ VERSION = '0.6.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.0
4
+ version: 0.6.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-14 00:00:00.000000000 Z
11
+ date: 2022-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 1.17.2
19
+ version: 2.3.11
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.17.2
26
+ version: 2.3.11
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: guard
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -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,20 @@ 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
111
139
  - !ruby/object:Gem::Dependency
112
140
  name: rake
113
141
  requirement: !ruby/object:Gem::Requirement
@@ -173,11 +201,14 @@ extra_rdoc_files: []
173
201
  files:
174
202
  - ".bundle/.keep"
175
203
  - ".bundle/config"
204
+ - ".github/workflows/ci.yml"
176
205
  - ".gitignore"
177
206
  - ".rubocop.yml"
207
+ - ".yardopts"
178
208
  - CHANGELOG.md
179
209
  - Gemfile
180
210
  - Guardfile
211
+ - MIT-LICENSE
181
212
  - README.md
182
213
  - Rakefile
183
214
  - bin/_guard-core
@@ -187,11 +218,16 @@ files:
187
218
  - bin/rake
188
219
  - bin/rubocop
189
220
  - bin/setup
221
+ - bin/yard
222
+ - bin/yardoc
223
+ - bin/yri
190
224
  - hash_deep_diff.gemspec
191
225
  - lib/hash_deep_diff.rb
192
- - lib/hash_deep_diff/acts_as_hash.rb
193
226
  - lib/hash_deep_diff/comparison.rb
194
227
  - lib/hash_deep_diff/delta.rb
228
+ - lib/hash_deep_diff/factories/comparison.rb
229
+ - lib/hash_deep_diff/reports/base.rb
230
+ - lib/hash_deep_diff/reports/diff.rb
195
231
  - lib/hash_deep_diff/version.rb
196
232
  homepage: https://github.com/bpohoriletz/hash_deep_diff
197
233
  licenses:
@@ -199,6 +235,7 @@ licenses:
199
235
  metadata:
200
236
  allowed_push_host: https://rubygems.org/
201
237
  homepage_uri: https://github.com/bpohoriletz/hash_deep_diff
238
+ documentation_uri: https://rdoc.info/gems/hash_deep_diff
202
239
  source_code_uri: https://github.com/bpohoriletz/hash_deep_diff
203
240
  changelog_uri: https://github.com/bpohoriletz/hash_deep_diff/blob/main/CHANGELOG.md
204
241
  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