hash_deep_diff 0.5.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -0
- data/CHANGELOG.md +1 -0
- data/Guardfile +2 -2
- data/README.md +90 -5
- data/hash_deep_diff.gemspec +5 -0
- data/lib/hash_deep_diff/change_key.rb +116 -0
- data/lib/hash_deep_diff/comparison.rb +88 -49
- data/lib/hash_deep_diff/delta.rb +95 -43
- data/lib/hash_deep_diff/factories/comparison.rb +95 -0
- data/lib/hash_deep_diff/reports/base.rb +44 -0
- data/lib/hash_deep_diff/reports/diff.rb +76 -0
- data/lib/hash_deep_diff/reports/yml.rb +53 -0
- data/lib/hash_deep_diff/version.rb +1 -1
- data/lib/hash_deep_diff.rb +8 -0
- metadata +64 -4
- data/lib/hash_deep_diff/acts_as_hash.rb +0 -31
- data/lib/hash_deep_diff/report.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e7ceeb1c2e935cc4b6c9d27e6d718f63f627d3adeba269df367c37464f3e9c0
|
4
|
+
data.tar.gz: 00053a3c87a26ea7dff3513594c6fb85e1235be3c341bc58f30d4934faa3de57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 612c398c02060fd908f449ac44893a8a81d33f3c1f13ec86bb3042d748034f18a20fbd99559c211af58e5f70d77d0b107fda0fae6cb2be3a1f94e1e0299c0f2e
|
7
|
+
data.tar.gz: c63d6e0ddad970dce331a5460981916b821805a8a3fe8b95f5ab6ea73a029942091acdf6b9681da533630fd6da38440749419d25436979e07cd15da5a2020658
|
data/.rubocop.yml
CHANGED
@@ -10,6 +10,7 @@ AllCops:
|
|
10
10
|
- '.git/**/*'
|
11
11
|
- '.bundle/**/*'
|
12
12
|
- 'bin/*'
|
13
|
+
TargetRubyVersion: 2.6.1
|
13
14
|
|
14
15
|
Style/RedundantReturn:
|
15
16
|
Enabled: false
|
@@ -18,3 +19,9 @@ Metrics/BlockLength:
|
|
18
19
|
Exclude:
|
19
20
|
- 'test/**/test_*.rb'
|
20
21
|
- '*.gemspec'
|
22
|
+
Style/YodaCondition:
|
23
|
+
EnforcedStyle: require_for_all_comparison_operators
|
24
|
+
|
25
|
+
Minitest/AssertEmptyLiteral:
|
26
|
+
Exclude:
|
27
|
+
- 'test/unit/test_change_key.rb'
|
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,8 @@ 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
|
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](https://github.com/liufengyun/hashdiff) by liufengyun and [hash_diff](https://github.com/CodingZeal/hash_diff) by CodingZeal
|
9
11
|
|
10
12
|
## Installation
|
11
13
|
|
@@ -24,20 +26,103 @@ Or install it yourself as:
|
|
24
26
|
$ gem install hash_deep_diff
|
25
27
|
|
26
28
|
## Usage
|
27
|
-
Basic
|
29
|
+
### Basic
|
28
30
|
|
29
31
|
```ruby
|
30
32
|
left = { a: :a }
|
31
33
|
right = { a: :b }
|
32
34
|
|
33
|
-
HashDeepDiff::Comparison.new(left, right).report
|
35
|
+
print HashDeepDiff::Comparison.new(left, right).report
|
34
36
|
```
|
35
37
|
```diff
|
36
38
|
- left[a] = a
|
37
39
|
+ left[a] = b
|
38
40
|
```
|
39
|
-
|
40
|
-
|
41
|
+
### Arrays
|
42
|
+
```ruby
|
43
|
+
left = [1, 2, { a: :a }]
|
44
|
+
right = [2, { a: :b }, 3]
|
45
|
+
|
46
|
+
print HashDeepDiff::Comparison.new(left, right).report
|
47
|
+
```
|
48
|
+
```diff
|
49
|
+
-left[...] = [1]
|
50
|
+
+left[...] = [3]
|
51
|
+
-left[{}][a] = a
|
52
|
+
+left[{}][a] = b
|
53
|
+
```
|
54
|
+
### Nesting
|
55
|
+
```ruby
|
56
|
+
left = { a: [1, 2, { a: :a } ], b: { c: [1, 2, { d: :e } ] } }
|
57
|
+
right = { a: [2, { a: :b }, 3], b: { c: { f: { g: :h } } } }
|
58
|
+
|
59
|
+
print HashDeepDiff::Comparison.new(left, right).report
|
60
|
+
```
|
61
|
+
```diff
|
62
|
+
-left[a][...] = [1]
|
63
|
+
+left[a][...] = [3]
|
64
|
+
-left[a][{}][a] = a
|
65
|
+
+left[a][{}][a] = b
|
66
|
+
+left[b][c][...][f] = {}
|
67
|
+
+left[b][c][...][f][g] = h
|
68
|
+
-left[b][c][...][f] = [1, 2]
|
69
|
+
-left[b][c][{}][d] = e
|
70
|
+
```
|
71
|
+
### Reporting Engines
|
72
|
+
You can choose from the default diff-like reporting engine (examples are above) and YML reporting engine
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
left = { a: [1, 2, { a: :a } ], b: { c: [1, 2, { d: :e } ] } }
|
76
|
+
right = { a: [2, { a: :b }, 3], b: { c: { f: { g: :h } } } }
|
77
|
+
```
|
78
|
+
#### Raw Report
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
print HashDeepDiff::Comparison.new(left, right, reporting_engine: HashDeepDiff::Reports::Yml).raw_report
|
82
|
+
=> {"additions"=>{:a=>[3, {:a=>:b}], :b=>{:c=>[{:f=>{:g=>:h}}]}},
|
83
|
+
"deletions"=>{:a=>[1, {:a=>:a}], :b=>{:c=>[1, 2, {:d=>:e}]}}}
|
84
|
+
```
|
85
|
+
|
86
|
+
#### YML Report
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
print HashDeepDiff::Comparison.new(left, right, reporting_engine: HashDeepDiff::Reports::Yml).report
|
90
|
+
|
91
|
+
---
|
92
|
+
additions:
|
93
|
+
:a:
|
94
|
+
- 3
|
95
|
+
- :a: :b
|
96
|
+
:b:
|
97
|
+
:c:
|
98
|
+
- :f:
|
99
|
+
:g: :h
|
100
|
+
deletions:
|
101
|
+
:a:
|
102
|
+
- 1
|
103
|
+
- :a: :a
|
104
|
+
:b:
|
105
|
+
:c:
|
106
|
+
- 1
|
107
|
+
- 2
|
108
|
+
- :d: :e
|
109
|
+
```
|
110
|
+
|
111
|
+
please see [Documentation](https://rdoc.info/gems/hash_deep_diff/HashDeepDiff/Comparison) for
|
112
|
+
more information or [Reporting test](https://github.com/bpohoriletz/hash_deep_diff/blob/a525d239189b0310aec3741dfc4862834805252d/test/integration/locales/test_uk_ru.rb#L59)
|
113
|
+
|
114
|
+
|
115
|
+
|
116
|
+
## Customization
|
117
|
+
|
118
|
+
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`
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
left = { a: :a }
|
122
|
+
right = { a: :b }
|
123
|
+
|
124
|
+
HashDeepDiff::Comparison.new(left, right, reporting_engine: CustomEngine).report
|
125
|
+
```
|
41
126
|
|
42
127
|
## Contributing
|
43
128
|
|
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'
|
@@ -47,8 +48,12 @@ Gem::Specification.new do |spec|
|
|
47
48
|
spec.add_development_dependency 'minitest', '~> 5.15.0'
|
48
49
|
spec.add_development_dependency 'minitest-focus', '~> 1.3.1'
|
49
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'
|
50
53
|
spec.add_development_dependency 'rake', '~> 10.5.0'
|
51
54
|
spec.add_development_dependency 'rubocop', '~> 1.26.1'
|
52
55
|
spec.add_development_dependency 'rubocop-minitest', '~> 0.18.0'
|
56
|
+
spec.add_development_dependency 'rubocop-performance', '~> 1.13.3'
|
53
57
|
spec.add_development_dependency 'rubocop-rake', '~> 0.6.0'
|
58
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.10.0'
|
54
59
|
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module HashDeepDiff
|
6
|
+
# Key for a compared value inside Array or Hash
|
7
|
+
class ChangeKey
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :to_ary, :[], :+, :map, :==, :first, :shift, :empty?
|
10
|
+
# element that indicates nested Hash
|
11
|
+
NESTED_HASH = '{}'
|
12
|
+
# element that indicates Array value
|
13
|
+
ARRAY_VALUE = '...'
|
14
|
+
|
15
|
+
# based on the first element of the key returns the initial object
|
16
|
+
# to buld the change representation
|
17
|
+
# @return [Array, Hash]
|
18
|
+
def self.initial_object(values:)
|
19
|
+
return [] if values.size.positive? && [NESTED_HASH, ARRAY_VALUE].include?(values[0][0][0])
|
20
|
+
|
21
|
+
{}
|
22
|
+
end
|
23
|
+
|
24
|
+
# set the value inside Hash based on the change_key
|
25
|
+
# @return [Array, Hash]
|
26
|
+
# TOFIX; check if @path are mutated
|
27
|
+
def set(obj, value, clone_keys = path.clone)
|
28
|
+
# 1. Fetch key
|
29
|
+
current_key = clone_keys.shift
|
30
|
+
# 2. Prepare object for further processing
|
31
|
+
init_value(current_key, obj, clone_keys)
|
32
|
+
init_nesting(current_key, obj, clone_keys)
|
33
|
+
# 3. Set value - directly or recursively
|
34
|
+
set_value(current_key, obj, value, clone_keys)
|
35
|
+
recursive_set(current_key, obj, value, clone_keys)
|
36
|
+
|
37
|
+
return obj
|
38
|
+
end
|
39
|
+
|
40
|
+
# see {#to_ary}
|
41
|
+
# @return [Array]
|
42
|
+
def to_a
|
43
|
+
to_ary
|
44
|
+
end
|
45
|
+
|
46
|
+
# array with keysused to initialize the object
|
47
|
+
# @return [Array]
|
48
|
+
def to_ary
|
49
|
+
path
|
50
|
+
end
|
51
|
+
|
52
|
+
# see {#to_str}
|
53
|
+
# @return [String]
|
54
|
+
def to_s
|
55
|
+
to_str
|
56
|
+
end
|
57
|
+
|
58
|
+
# visual representation of the change key
|
59
|
+
# @return [String]
|
60
|
+
def to_str
|
61
|
+
path.map { |key| "[#{key}]" }.join
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :path
|
67
|
+
|
68
|
+
def initialize(path:)
|
69
|
+
@path = path.to_ary
|
70
|
+
end
|
71
|
+
|
72
|
+
# prepare an object before further processing
|
73
|
+
def init_value(current_key, obj, clone_keys)
|
74
|
+
if [ARRAY_VALUE] == clone_keys || NESTED_HASH == clone_keys.first
|
75
|
+
obj[current_key] ||= []
|
76
|
+
elsif !clone_keys.empty? && ![ARRAY_VALUE, NESTED_HASH].include?(current_key)
|
77
|
+
obj[current_key] ||= {}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# prepare nesting before further processing
|
82
|
+
def init_nesting(current_key, obj, clone_keys)
|
83
|
+
element = if NESTED_HASH == current_key
|
84
|
+
obj
|
85
|
+
elsif NESTED_HASH == clone_keys.first
|
86
|
+
obj[current_key]
|
87
|
+
end
|
88
|
+
element << {} unless element.nil? || element.last.respond_to?(:to_hash)
|
89
|
+
end
|
90
|
+
|
91
|
+
# no more nesting - set value inside object
|
92
|
+
def set_value(current_key, obj, value, clone_keys)
|
93
|
+
if ARRAY_VALUE == current_key
|
94
|
+
obj.prepend(*value)
|
95
|
+
clone_keys.pop
|
96
|
+
elsif clone_keys.empty?
|
97
|
+
obj[current_key] = value
|
98
|
+
clone_keys.pop
|
99
|
+
elsif [ARRAY_VALUE] == clone_keys
|
100
|
+
obj[current_key] = value + obj[current_key]
|
101
|
+
clone_keys.pop
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# recursion for deeply nested values
|
106
|
+
def recursive_set(current_key, obj, value, clone_keys)
|
107
|
+
if NESTED_HASH == current_key
|
108
|
+
set(obj.last, value, clone_keys)
|
109
|
+
elsif NESTED_HASH == clone_keys.first
|
110
|
+
set(obj[current_key].last, value, clone_keys[1..])
|
111
|
+
elsif !clone_keys.empty?
|
112
|
+
set(obj[current_key], value, clone_keys)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'factories/comparison'
|
4
4
|
|
5
5
|
module HashDeepDiff
|
6
6
|
# Representation of the recursive difference between two hashes
|
@@ -11,94 +11,133 @@ module HashDeepDiff
|
|
11
11
|
#
|
12
12
|
# Examples:
|
13
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
|
14
|
+
# - { one: { two: :a, zero: :z } } compared with { one: { two: :b, three: :c } } has nesting, so is represented as
|
15
15
|
# - { two: :a } compared with { two: :b, three: :c }, as there is no more nesting we compare keys and values
|
16
16
|
# and have the following comparisons
|
17
17
|
# { one: { two: :a } } compared to { one: { two: :b } } - value was changed
|
18
18
|
# i.e :a vas replaced with :b on path [:one, :two]
|
19
|
-
# { one: { zero: z } } compared to NO_VALUE - value was deleted
|
19
|
+
# { one: { zero: :z } } compared to NO_VALUE - value was deleted
|
20
20
|
# i.e :z vas replaced with NO_VALUE on path [:one, :zero]
|
21
21
|
# NO_VALUE compared to { one: { three: :c } } compared - value was added
|
22
22
|
# i.e NO_VALUE vas replaced with :c on path [:one, :three]
|
23
23
|
# [
|
24
|
-
# #<HashDeepDiff::Delta
|
25
|
-
# @
|
26
|
-
# @prefix=[:one],
|
24
|
+
# #<HashDeepDiff::Delta:0x00007fc7bc8a6e58
|
25
|
+
# @change_key=#<HashDeepDiff::ChangeKey:0x00007fc7bc8a6d40 @path=[:one, :two]>,
|
27
26
|
# @value={:left=>:a, :right=>:b}>,
|
28
|
-
# #<HashDeepDiff::Delta
|
29
|
-
# @
|
30
|
-
# @prefix=[:one],
|
27
|
+
# #<HashDeepDiff::Delta:0x00007fc7bc8a6b60
|
28
|
+
# @change_key=#<HashDeepDiff::ChangeKey:0x00007fc7bc8a6a48 @path=[:one, :zero]>,
|
31
29
|
# @value={:left=>:z, :right=>HashDeepDiff::NO_VALUE}>,
|
32
|
-
# #<HashDeepDiff::Delta
|
33
|
-
#
|
34
|
-
#
|
35
|
-
# @value={:left=>HashDeepDiff::NO_VALUE, :right=>:c}>
|
30
|
+
# #<HashDeepDiff::Delta:0x00007fc7bc8a6930
|
31
|
+
# @change_key=#<HashDeepDiff::ChangeKey:0x00007fc7bc8a6818 @path=[:one, :three]>,
|
32
|
+
# @value={:left=>HashDeepDiff::NO_VALUE, :right=>:c}>]
|
36
33
|
# ]
|
37
34
|
class Comparison
|
38
|
-
|
39
|
-
|
40
|
-
# @!attribute [r] right
|
41
|
-
# @return [Hash] Hash that the original is compared to
|
42
|
-
# @!attribute [r] path
|
43
|
-
# @return [Array<Object>] to a compared Hashes (is empty for top-level comparison)
|
44
|
-
attr_reader :left, :right, :path
|
35
|
+
extend Forwardable
|
36
|
+
attr_reader :reporting_engine, :delta_engine
|
45
37
|
|
46
|
-
|
47
|
-
|
48
|
-
diff.join("\n")
|
49
|
-
end
|
38
|
+
def_delegators :comparison_factory, :comparison
|
39
|
+
def_delegators :report_engine_factory, :report, :raw_report
|
50
40
|
|
51
41
|
# @return [Array<HashDeepDiff::Delta>]
|
52
42
|
def diff
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
43
|
+
return [] if left == right
|
44
|
+
|
45
|
+
deltas.flat_map { |new_delta| new_delta.simple? ? new_delta : inward_comparison(new_delta) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [Object] key the key which value we're currently comparing
|
49
|
+
def left(key = NO_VALUE)
|
50
|
+
return NO_VALUE if @left == NO_VALUE
|
51
|
+
return @left if key == NO_VALUE
|
52
|
+
return @left unless left.respond_to?(:to_hash)
|
53
|
+
|
54
|
+
@left[key] || NO_VALUE
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param [Object] key the key which value we're currently comparing
|
58
|
+
def right(key = NO_VALUE)
|
59
|
+
return NO_VALUE if @right == NO_VALUE
|
60
|
+
return @right if key == NO_VALUE
|
61
|
+
return @right unless right.respond_to?(:to_hash)
|
62
|
+
|
63
|
+
@right[key] || NO_VALUE
|
58
64
|
end
|
59
65
|
|
60
66
|
private
|
61
67
|
|
62
|
-
#
|
63
|
-
#
|
68
|
+
# @!attribute [r] path
|
69
|
+
# @return [Array<Object>] subset of keys from original Hashes to fetch compared values
|
70
|
+
# (is empty for top-level comparison)
|
71
|
+
attr_reader :path
|
72
|
+
|
73
|
+
# @param [Object] original original version
|
74
|
+
# @param [Object] changed new version
|
64
75
|
# @param [Array] prefix keys to fetch current comparison (not empty for nested comparisons)
|
65
|
-
def initialize(
|
66
|
-
@left =
|
67
|
-
@right =
|
76
|
+
def initialize(original, changed, prefix = [], reporting_engine: Reports::Diff, delta_engine: Delta)
|
77
|
+
@left = original
|
78
|
+
@right = changed
|
68
79
|
@path = prefix.to_ary
|
80
|
+
@reporting_engine = reporting_engine
|
81
|
+
@delta_engine = delta_engine
|
69
82
|
end
|
70
83
|
|
84
|
+
# {Comparison} broken down into array of {Delta}
|
71
85
|
# @return [Array<HashDeepDiff::Delta>]
|
72
|
-
def
|
86
|
+
def deltas
|
87
|
+
return [delta] if common_keys.empty?
|
88
|
+
|
73
89
|
common_keys.each_with_object([]) do |key, memo|
|
74
90
|
next if values_equal?(key)
|
75
91
|
|
76
|
-
memo
|
92
|
+
memo.append(delta(key: key))
|
93
|
+
end.flatten
|
94
|
+
end
|
95
|
+
|
96
|
+
# depending on circumstances will return necessary comparisons
|
97
|
+
# @return [Array<HashDeepDiff::Delta>]
|
98
|
+
def inward_comparison(complex_delta)
|
99
|
+
if complex_delta.partial?
|
100
|
+
complex_delta.placebo +
|
101
|
+
comparison(delta: complex_delta, modifier: :addition).map(&:diff).flatten +
|
102
|
+
comparison(delta: complex_delta, modifier: :deletion).map(&:diff).flatten
|
103
|
+
else
|
104
|
+
comparison(delta: complex_delta).map(&:diff).flatten
|
77
105
|
end
|
78
106
|
end
|
79
107
|
|
80
108
|
# @param [Object] key the key which value we're currently comparing
|
81
109
|
# @return [Bool]
|
82
110
|
def values_equal?(key)
|
83
|
-
|
111
|
+
right(key).instance_of?(left(key).class) && (right(key) == left(key))
|
84
112
|
end
|
85
113
|
|
86
|
-
#
|
87
|
-
# @
|
88
|
-
def
|
89
|
-
|
114
|
+
# All keys from both original and compared objects
|
115
|
+
# @return [Array]
|
116
|
+
def common_keys
|
117
|
+
keys = []
|
118
|
+
keys += left.keys if left.respond_to?(:keys)
|
119
|
+
keys += right.keys if right.respond_to?(:keys)
|
120
|
+
|
121
|
+
keys.uniq
|
90
122
|
end
|
91
123
|
|
92
|
-
#
|
93
|
-
# @
|
94
|
-
def
|
95
|
-
|
124
|
+
# factory function
|
125
|
+
# @return [HashDeepDiff::Delta]
|
126
|
+
def delta(key: NO_VALUE)
|
127
|
+
keys = path
|
128
|
+
keys += [key] unless key == NO_VALUE
|
129
|
+
|
130
|
+
HashDeepDiff::Delta.new(path: keys, value: { left: left(key), right: right(key) })
|
96
131
|
end
|
97
132
|
|
98
|
-
#
|
99
|
-
|
100
|
-
|
101
|
-
|
133
|
+
# @return [HashDeepDiff::Factories::Comparison]
|
134
|
+
def comparison_factory
|
135
|
+
HashDeepDiff::Factories::Comparison.new(reporting_engine: reporting_engine)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [HashDeepDiff::Reports::Base]
|
139
|
+
def report_engine_factory
|
140
|
+
reporting_engine.new(diff: diff)
|
102
141
|
end
|
103
142
|
end
|
104
143
|
end
|
data/lib/hash_deep_diff/delta.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require_relative 'report'
|
3
|
+
require 'forwardable'
|
5
4
|
|
6
5
|
module HashDeepDiff
|
7
6
|
# Representation of the diff of two values
|
@@ -10,74 +9,127 @@ module HashDeepDiff
|
|
10
9
|
# - diff of { a: a } and { a: b } is { a: { left: a, right: b } }
|
11
10
|
# - diff of {} and { a: b } is { a: { left: HashDeepDiff::NO_VALUE, right: b } }
|
12
11
|
class Delta
|
13
|
-
|
12
|
+
extend Forwardable
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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(path: change_key, value: placebo)]
|
19
24
|
end
|
20
25
|
|
21
|
-
#
|
22
|
-
# @return [
|
26
|
+
# true if any value is an +Array+ with hashes
|
27
|
+
# @return [TrueClass, FalseClass]
|
23
28
|
def complex?
|
24
|
-
|
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?
|
25
54
|
end
|
26
55
|
|
27
|
-
#
|
28
|
-
# @return [
|
29
|
-
def
|
30
|
-
|
56
|
+
# true if none of the values is a Hash
|
57
|
+
# @return [TrueClass, FalseClass]
|
58
|
+
def simple?
|
59
|
+
simple_left? && simple_right?
|
60
|
+
end
|
61
|
+
|
62
|
+
# removed element(s)
|
63
|
+
def deletion
|
64
|
+
return left unless array_with_array?
|
65
|
+
|
66
|
+
return left - right
|
31
67
|
end
|
32
68
|
|
33
69
|
# Original value
|
34
70
|
def left
|
35
|
-
|
71
|
+
value[:left]
|
72
|
+
end
|
73
|
+
|
74
|
+
# added element(s)
|
75
|
+
def addition
|
76
|
+
return right unless array_with_array?
|
77
|
+
|
78
|
+
return right - left
|
36
79
|
end
|
37
80
|
|
38
81
|
# Value we compare to
|
39
82
|
def right
|
40
|
-
|
83
|
+
value[:right]
|
41
84
|
end
|
42
85
|
|
43
|
-
#
|
44
|
-
|
45
|
-
|
86
|
+
# see {#to_hash}
|
87
|
+
# @return [Hash]
|
88
|
+
def to_h
|
89
|
+
to_hash
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Hash]
|
93
|
+
def to_hash
|
94
|
+
{ change_key[-1] => value }
|
46
95
|
end
|
47
96
|
|
48
97
|
private
|
49
98
|
|
50
|
-
|
99
|
+
attr_reader :value
|
100
|
+
|
101
|
+
# @param [Array] path list of keys to fetch values we're comparing
|
51
102
|
# @param [Hash<(:left, :right), Object>] value +Hash+ object with two keys - :left and :right,
|
52
103
|
# that represents compared original value (at :left) and value we compare to (at :right)
|
53
104
|
def initialize(path:, value:)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
@delta = { path[-1] => value }
|
58
|
-
@value = value
|
59
|
-
@prefix = path[0..-2]
|
60
|
-
else
|
61
|
-
@delta = { path => value }
|
62
|
-
@value = value
|
63
|
-
@prefix = []
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Visual representation of additions
|
68
|
-
# @return [NillClass, String]
|
69
|
-
def deletion
|
70
|
-
return nil if left == NO_VALUE
|
105
|
+
@value = value
|
106
|
+
@change_key = HashDeepDiff::ChangeKey.new(path: path)
|
107
|
+
end
|
71
108
|
|
72
|
-
|
109
|
+
# an indication of added/removed nested Hash
|
110
|
+
# @return [Array, Hash]
|
111
|
+
def placebo_elment
|
112
|
+
return [{}] if complex_left? || complex_right?
|
113
|
+
|
114
|
+
return {}
|
73
115
|
end
|
74
116
|
|
75
|
-
#
|
76
|
-
# @return [
|
77
|
-
def
|
78
|
-
|
117
|
+
# true if left value has no nested Hashes
|
118
|
+
# @return [TrueClass, FalseClass]
|
119
|
+
def simple_left?
|
120
|
+
!left.respond_to?(:to_hash) && !complex_left?
|
121
|
+
end
|
122
|
+
|
123
|
+
# true if right value has no nested Hashes
|
124
|
+
# @return [TrueClass, FalseClass]
|
125
|
+
def simple_right?
|
126
|
+
!right.respond_to?(:to_hash) && !complex_right?
|
127
|
+
end
|
79
128
|
|
80
|
-
|
129
|
+
# true if both left and right are arrays
|
130
|
+
# @return [TrueClass, FalseClass]
|
131
|
+
def array_with_array?
|
132
|
+
left.respond_to?(:to_ary) && right.respond_to?(:to_ary)
|
81
133
|
end
|
82
134
|
end
|
83
135
|
end
|
@@ -0,0 +1,95 @@
|
|
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 + [ChangeKey::ARRAY_VALUE]],
|
46
|
+
[nesting_left, nesting_right, change_key + [ChangeKey::NESTED_HASH]]]
|
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 NO_VALUE if left.respond_to?(:to_hash) && right.respond_to?(:to_ary)
|
58
|
+
return left unless left.respond_to?(:to_ary)
|
59
|
+
|
60
|
+
left.reject { |el| el.respond_to?(:to_hash) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# changed value without nested hashes
|
64
|
+
# @return [Object]
|
65
|
+
def value_right
|
66
|
+
return NO_VALUE if right.respond_to?(:to_hash) && left.respond_to?(:to_ary)
|
67
|
+
return right unless right.respond_to?(:to_ary)
|
68
|
+
|
69
|
+
right.reject { |el| el.respond_to?(:to_hash) }
|
70
|
+
end
|
71
|
+
|
72
|
+
# nested hashes from original value
|
73
|
+
# @return [Array<Hash>]
|
74
|
+
def nesting_left
|
75
|
+
return left if left.respond_to?(:to_hash)
|
76
|
+
return NO_VALUE unless complex_left?
|
77
|
+
|
78
|
+
left
|
79
|
+
.select { |el| el.respond_to?(:to_hash) }
|
80
|
+
.each_with_object({}) { |el, memo| memo.merge!(el) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# nested hashes from changed value
|
84
|
+
# @return [Array<Hash>]
|
85
|
+
def nesting_right
|
86
|
+
return right if right.respond_to?(:to_hash)
|
87
|
+
return NO_VALUE unless complex_right?
|
88
|
+
|
89
|
+
right
|
90
|
+
.select { |el| el.respond_to?(:to_hash) }
|
91
|
+
.each_with_object({}) { |el, memo| memo.merge!(el) }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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
|
+
# raw data for {#report}
|
9
|
+
def raw_report
|
10
|
+
raise AbstractMethodError
|
11
|
+
end
|
12
|
+
|
13
|
+
# see {#to_str}
|
14
|
+
# @return [String]
|
15
|
+
def to_s
|
16
|
+
to_str
|
17
|
+
end
|
18
|
+
|
19
|
+
# see {#report}
|
20
|
+
# @return [String]
|
21
|
+
def to_str
|
22
|
+
report
|
23
|
+
end
|
24
|
+
|
25
|
+
# A report on additions and deletions
|
26
|
+
# @return [String]
|
27
|
+
def report
|
28
|
+
raise AbstractMethodError
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @!attribute [r] diff
|
34
|
+
# @return [Array<HashDeepDiff::Delta>] set of deltas from Comparison of two objects
|
35
|
+
attr_reader :diff
|
36
|
+
|
37
|
+
# @param [Array<HashDeepDiff::Delta>] diff comparison data to report
|
38
|
+
def initialize(diff:, change_key_engine: HashDeepDiff::ChangeKey)
|
39
|
+
@diff = diff.to_ary
|
40
|
+
@change_key = change_key_engine
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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
|
+
# additiond and deletions represented as diff
|
11
|
+
# @return [String]
|
12
|
+
def report
|
13
|
+
raw_report.map { |delta| original(delta) + replacement(delta) }.join
|
14
|
+
end
|
15
|
+
|
16
|
+
# additiond and deletions raw
|
17
|
+
# @return [Array<HashDeepDiff::Delta>]
|
18
|
+
def raw_report
|
19
|
+
diff
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# line of the report with deleted value
|
25
|
+
# @return [String]
|
26
|
+
def original(delta)
|
27
|
+
return '' if delta.left == NO_VALUE
|
28
|
+
return "#{deletion}#{delta.change_key} = #{delta.left}\n" unless array_to_array?(delta)
|
29
|
+
return '' if array_deletion(delta).empty?
|
30
|
+
|
31
|
+
"#{deletion}#{delta.change_key} = #{array_deletion(delta)}\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
# line of the report with added value
|
35
|
+
# @return [String]
|
36
|
+
def replacement(delta)
|
37
|
+
return '' if delta.right == NO_VALUE
|
38
|
+
return "#{addition}#{delta.change_key} = #{delta.right}\n" unless array_to_array?(delta)
|
39
|
+
return '' if array_addition(delta).empty?
|
40
|
+
|
41
|
+
"#{addition}#{delta.change_key} = #{array_addition(delta)}\n"
|
42
|
+
end
|
43
|
+
|
44
|
+
# returns true if original value and replacement are instances of +Array+
|
45
|
+
# @return Bool
|
46
|
+
# TOFIX drop
|
47
|
+
def array_to_array?(delta)
|
48
|
+
delta.left.instance_of?(Array) && delta.right.instance_of?(Array)
|
49
|
+
end
|
50
|
+
|
51
|
+
# added elemnts of array
|
52
|
+
# @return [Array]
|
53
|
+
def array_addition(delta)
|
54
|
+
delta.right - delta.left
|
55
|
+
end
|
56
|
+
|
57
|
+
# added elemnts of array
|
58
|
+
# @return [Array]
|
59
|
+
def array_deletion(delta)
|
60
|
+
delta.left - delta.right
|
61
|
+
end
|
62
|
+
|
63
|
+
# visual indication of addition
|
64
|
+
# @return [String]
|
65
|
+
def addition
|
66
|
+
'+left'
|
67
|
+
end
|
68
|
+
|
69
|
+
# visual indication of deletion
|
70
|
+
# @return [String]
|
71
|
+
def deletion
|
72
|
+
'-left'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'forwardable'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module HashDeepDiff
|
8
|
+
# Different reporting enjines for {Delta}
|
9
|
+
module Reports
|
10
|
+
# Visual representation of the {Delta} as diff
|
11
|
+
class Yml < Base
|
12
|
+
extend Forwardable
|
13
|
+
def_delegators :@change_key, :initial_object, :dig_set
|
14
|
+
|
15
|
+
# additions and deletions represented as YAML
|
16
|
+
# @return [String]
|
17
|
+
def report
|
18
|
+
YAML.dump(raw_report)
|
19
|
+
end
|
20
|
+
|
21
|
+
# additions and deletiond represented as Hash
|
22
|
+
# @return [Hash]
|
23
|
+
def raw_report
|
24
|
+
@raw = { 'additions' => initial_object(values: additions), 'deletions' => initial_object(values: deletions) }
|
25
|
+
|
26
|
+
additions.each { |(change_key, addition)| change_key.set(raw['additions'], addition) }
|
27
|
+
deletions.each { |(change_key, deletion)| change_key.set(raw['deletions'], deletion) }
|
28
|
+
|
29
|
+
return raw
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :raw
|
35
|
+
|
36
|
+
# added values
|
37
|
+
# @return [Array<HashDeepDiff::Delta>]
|
38
|
+
def additions
|
39
|
+
diff.reject { |delta| delta.right == NO_VALUE }
|
40
|
+
.map { |delta| [delta.change_key, delta.addition] }
|
41
|
+
.reject { |(_, addition)| [] == addition }
|
42
|
+
end
|
43
|
+
|
44
|
+
# deleted values
|
45
|
+
# @return [Array<HashDeepDiff::Delta>]
|
46
|
+
def deletions
|
47
|
+
diff.reject { |delta| delta.left == NO_VALUE }
|
48
|
+
.map { |delta| [delta.change_key, delta.deletion] }
|
49
|
+
.reject { |(_, deletion)| [] == deletion }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/hash_deep_diff.rb
CHANGED
@@ -1,10 +1,18 @@
|
|
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/reports/yml'
|
6
|
+
require 'hash_deep_diff/delta'
|
7
|
+
require 'hash_deep_diff/change_key'
|
4
8
|
require 'hash_deep_diff/comparison'
|
5
9
|
|
6
10
|
# Global namespace
|
7
11
|
module HashDeepDiff
|
8
12
|
# value was not found
|
9
13
|
NO_VALUE = Class.new(NilClass)
|
14
|
+
# Abstract method
|
15
|
+
AbstractMethodError = Class.new(NoMethodError)
|
16
|
+
# Any error
|
17
|
+
Error = Class.new(StandardError)
|
10
18
|
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.8.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-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -122,6 +122,34 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
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
|
125
153
|
- !ruby/object:Gem::Dependency
|
126
154
|
name: rake
|
127
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -164,6 +192,20 @@ dependencies:
|
|
164
192
|
- - "~>"
|
165
193
|
- !ruby/object:Gem::Version
|
166
194
|
version: 0.18.0
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: rubocop-performance
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: 1.13.3
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: 1.13.3
|
167
209
|
- !ruby/object:Gem::Dependency
|
168
210
|
name: rubocop-rake
|
169
211
|
requirement: !ruby/object:Gem::Requirement
|
@@ -178,6 +220,20 @@ dependencies:
|
|
178
220
|
- - "~>"
|
179
221
|
- !ruby/object:Gem::Version
|
180
222
|
version: 0.6.0
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: rubocop-rspec
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - "~>"
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: 2.10.0
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - "~>"
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: 2.10.0
|
181
237
|
description: Find the exact difference between two Hash objects
|
182
238
|
email:
|
183
239
|
- bohdan.pohorilets@gmail.com
|
@@ -209,10 +265,13 @@ files:
|
|
209
265
|
- bin/yri
|
210
266
|
- hash_deep_diff.gemspec
|
211
267
|
- lib/hash_deep_diff.rb
|
212
|
-
- lib/hash_deep_diff/
|
268
|
+
- lib/hash_deep_diff/change_key.rb
|
213
269
|
- lib/hash_deep_diff/comparison.rb
|
214
270
|
- lib/hash_deep_diff/delta.rb
|
215
|
-
- lib/hash_deep_diff/
|
271
|
+
- lib/hash_deep_diff/factories/comparison.rb
|
272
|
+
- lib/hash_deep_diff/reports/base.rb
|
273
|
+
- lib/hash_deep_diff/reports/diff.rb
|
274
|
+
- lib/hash_deep_diff/reports/yml.rb
|
216
275
|
- lib/hash_deep_diff/version.rb
|
217
276
|
homepage: https://github.com/bpohoriletz/hash_deep_diff
|
218
277
|
licenses:
|
@@ -220,6 +279,7 @@ licenses:
|
|
220
279
|
metadata:
|
221
280
|
allowed_push_host: https://rubygems.org/
|
222
281
|
homepage_uri: https://github.com/bpohoriletz/hash_deep_diff
|
282
|
+
documentation_uri: https://rdoc.info/gems/hash_deep_diff
|
223
283
|
source_code_uri: https://github.com/bpohoriletz/hash_deep_diff
|
224
284
|
changelog_uri: https://github.com/bpohoriletz/hash_deep_diff/blob/main/CHANGELOG.md
|
225
285
|
rubygems_mfa_required: 'true'
|
@@ -1,31 +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 instances of Delta instead of Hash
|
7
|
-
# in this gem
|
8
|
-
module ActsAsHash
|
9
|
-
# @param [Object] base a hook that is invoked when module is included in a class
|
10
|
-
def self.included(base)
|
11
|
-
base.extend Forwardable
|
12
|
-
base.def_delegators :@delta, :==, :each_with_object, :each_key, :[],
|
13
|
-
:to_a, :empty?, :keys
|
14
|
-
base.include InstanceMethods
|
15
|
-
end
|
16
|
-
|
17
|
-
# We assume that the class will initialize instance variable +@delta+ that will return
|
18
|
-
# a representation of an instance of a class as a +Hash+ object
|
19
|
-
module InstanceMethods
|
20
|
-
# a +Hash+ representation of an object
|
21
|
-
def to_h
|
22
|
-
to_hash
|
23
|
-
end
|
24
|
-
|
25
|
-
# a +Hash+ representation of an object
|
26
|
-
def to_hash
|
27
|
-
@delta
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module HashDeepDiff
|
4
|
-
# Visual representation of the difference between two values
|
5
|
-
class Report
|
6
|
-
# We have two cases
|
7
|
-
# * added - when value on the left is missing
|
8
|
-
# * deleted - when the value on the right is missing
|
9
|
-
module Mode
|
10
|
-
# for additions
|
11
|
-
ADDITION = '+left'
|
12
|
-
# for deletions
|
13
|
-
DELETION = '-left'
|
14
|
-
end
|
15
|
-
|
16
|
-
# A report with all additions and deletions
|
17
|
-
# @return [String]
|
18
|
-
def to_str
|
19
|
-
if @value.respond_to?(:to_hash) && !@value.empty?
|
20
|
-
[@mode, diff_prefix, ' = ', "{}\n"].join +
|
21
|
-
@value.keys.map do |key|
|
22
|
-
Report.new(path: @path + [key], value: @value[key], mode: @mode)
|
23
|
-
end.join("\n")
|
24
|
-
else
|
25
|
-
[@mode, diff_prefix, ' = ', @value.to_s].join
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# A report with all additions and deletions
|
30
|
-
# @return [String]
|
31
|
-
def to_s
|
32
|
-
to_str
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
# @param [Array] path Keys from compared objects to fetch the compared values
|
38
|
-
# @param [Object] value value from a compared object at +@path+
|
39
|
-
# @param [Mode::ADDITION, Mode::DELETION] mode
|
40
|
-
def initialize(path:, value:, mode: Mode::ADDITION)
|
41
|
-
@path = path.to_ary
|
42
|
-
@value = value
|
43
|
-
@mode = mode
|
44
|
-
end
|
45
|
-
|
46
|
-
# Visual representation of keys from compared objects needed to fetch the compared values
|
47
|
-
# @return [String]
|
48
|
-
def diff_prefix
|
49
|
-
# TOFIX poor naming
|
50
|
-
@path.map { |key| "[#{key}]" }.join
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|