hash_deep_diff 0.4.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +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
|
![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")
|
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
|