hash_deep_diff 0.4.0 → 0.6.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: 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