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 +4 -4
- data/.yardopts +1 -0
- data/CHANGELOG.md +1 -0
- data/Guardfile +6 -2
- data/README.md +16 -1
- data/bin/yard +27 -0
- data/bin/yardoc +27 -0
- data/bin/yri +27 -0
- data/hash_deep_diff.gemspec +4 -0
- data/lib/hash_deep_diff/comparison.rb +123 -26
- data/lib/hash_deep_diff/delta.rb +77 -42
- data/lib/hash_deep_diff/factories/comparison.rb +91 -0
- data/lib/hash_deep_diff/reports/base.rb +49 -0
- data/lib/hash_deep_diff/reports/diff.rb +69 -0
- data/lib/hash_deep_diff/version.rb +2 -1
- data/lib/hash_deep_diff.rb +8 -1
- metadata +52 -3
- data/lib/hash_deep_diff/acts_as_hash.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc574fc78f743f8b629ecbb87baebea7e75789d99401f7e54691ad02469572ec
|
4
|
+
data.tar.gz: 0f378780b1f08173779e3684a8d42361c0532411c7972952a626bc3e4a0bebb0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|

|
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")
|
data/hash_deep_diff.gemspec
CHANGED
@@ -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 '
|
3
|
+
require_relative 'factories/comparison'
|
4
4
|
|
5
|
-
# :nodoc:
|
6
5
|
module HashDeepDiff
|
7
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
40
|
-
value_right = right[key] || NO_VALUE
|
101
|
+
next if values_equal?(key)
|
41
102
|
|
42
|
-
|
103
|
+
memo.append(delta(key: key))
|
104
|
+
end.flatten
|
105
|
+
end
|
43
106
|
|
44
|
-
|
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
|
-
|
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
|
data/lib/hash_deep_diff/delta.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
-
|
12
|
+
extend Forwardable
|
13
13
|
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
-
#
|
23
|
-
#
|
24
|
-
def
|
25
|
-
|
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
|
-
|
64
|
+
value[:left]
|
30
65
|
end
|
31
66
|
|
67
|
+
# Value we compare to
|
32
68
|
def right
|
33
|
-
|
69
|
+
value[:right]
|
34
70
|
end
|
35
71
|
|
36
|
-
|
37
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
#
|
76
|
-
|
77
|
-
|
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
|
data/lib/hash_deep_diff.rb
CHANGED
@@ -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
|
-
|
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
|
+
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-
|
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
|