hash_deep_diff 0.5.0 → 0.8.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/.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
|

|
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
|