hash_deep_diff 0.4.0 → 0.6.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/.github/workflows/ci.yml +39 -0
- data/.yardopts +1 -0
- data/Guardfile +6 -3
- data/MIT-LICENSE +20 -0
- data/README.md +18 -3
- data/Rakefile +1 -1
- data/bin/yard +27 -0
- data/bin/yardoc +27 -0
- data/bin/yri +27 -0
- data/hash_deep_diff.gemspec +4 -1
- data/lib/hash_deep_diff/comparison.rb +124 -26
- data/lib/hash_deep_diff/delta.rb +56 -45
- data/lib/hash_deep_diff/factories/comparison.rb +58 -0
- data/lib/hash_deep_diff/reports/base.rb +42 -0
- data/lib/hash_deep_diff/reports/diff.rb +47 -0
- data/lib/hash_deep_diff/version.rb +2 -1
- data/lib/hash_deep_diff.rb +8 -1
- metadata +42 -5
- 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: ddbaa8e8f12ba99330973c609c00ab710e9fa39b369797b80dd46f24aa6740e1
|
|
4
|
+
data.tar.gz: 6a5954a960ca0876cdb9e28f3e535be687de5596ed07be61581102b75b178ab4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af66a50e5a7eb848c96b757e8e37a4d767868de6fd96ebfd4f528085111e2fe1c9e39215b8514d534d33f94cfc4c024c5c2bba1813acad729fa906e18eee796a
|
|
7
|
+
data.tar.gz: 358e25298ee3c42ea469e44113fe62935ec8f564c4111c52e170bd25764d99cc4b71c42d154689ad2a9c54ff21f9359dad57cfbd450677853eb60655b90aecf4
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
minitest:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- name: Checkout Repo
|
|
10
|
+
uses: actions/checkout@v2
|
|
11
|
+
|
|
12
|
+
- name: Install Ruby
|
|
13
|
+
uses: ruby/setup-ruby@v1
|
|
14
|
+
with:
|
|
15
|
+
ruby-version: 2.6
|
|
16
|
+
bundler-cache: false
|
|
17
|
+
|
|
18
|
+
- name: Install Gems
|
|
19
|
+
run: bin/bundle
|
|
20
|
+
|
|
21
|
+
- name: Run Minitest
|
|
22
|
+
run: bin/rake test
|
|
23
|
+
rubocop:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- name: Checkout Repo
|
|
27
|
+
uses: actions/checkout@v2
|
|
28
|
+
|
|
29
|
+
- name: Install Ruby
|
|
30
|
+
uses: ruby/setup-ruby@v1
|
|
31
|
+
with:
|
|
32
|
+
ruby-version: 2.6
|
|
33
|
+
bundler-cache: false
|
|
34
|
+
|
|
35
|
+
- name: Install Gems
|
|
36
|
+
run: bin/bundle
|
|
37
|
+
|
|
38
|
+
- name: Run Rubocop
|
|
39
|
+
run: bin/rubocop
|
data/.yardopts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--private
|
data/Guardfile
CHANGED
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
guard :rubocop do
|
|
21
21
|
watch(/.+\.rb$/)
|
|
22
22
|
watch(/.+\.gemspec$/)
|
|
23
|
-
watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
guard :minitest do
|
|
27
26
|
# with Minitest::Unit
|
|
28
27
|
watch(%r{^test/(.*)/?test_(.*)\.rb$})
|
|
29
|
-
|
|
30
|
-
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { 'test' }
|
|
28
|
+
watch(%r{^lib/hash_deep_diff/(.*/)?([^/]+)\.rb$}) { |m| "test/unit/#{m[1]}test_#{m[2]}.rb" }
|
|
29
|
+
# watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { 'test' }
|
|
31
30
|
watch(%r{^test/test_helper\.rb$}) { 'test' }
|
|
32
31
|
watch(%r{^test/support/.*$}) { 'test' }
|
|
33
32
|
end
|
|
33
|
+
|
|
34
|
+
guard 'yard' do
|
|
35
|
+
watch(%r{lib/.+\.rb})
|
|
36
|
+
end
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 - 2022 Bohdan Pohorilets
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# HashDeepDiff
|
|
2
|
+
[](https://badge.fury.io/rb/hash_deep_diff) 
|
|
5
|
+

|
|
2
6
|
|
|
3
|
-
Find the exact difference between two Hash objects and build a report to visualize it
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
Version](https://badge.fury.io/rb/hash_deep_diff.svg)](https://badge.fury.io/rb/hash_deep_diff)
|
|
8
|
+
Find the exact difference between two Hash objects and build a report to visualize it
|
|
7
9
|
|
|
8
10
|
## Installation
|
|
9
11
|
|
|
@@ -34,6 +36,19 @@ HashDeepDiff::Comparison.new(left, right).report
|
|
|
34
36
|
- left[a] = a
|
|
35
37
|
+ left[a] = b
|
|
36
38
|
```
|
|
39
|
+
please see [Documentation](https://rdoc.info/gems/hash_deep_diff/HashDeepDiff/Comparison) for
|
|
40
|
+
more information
|
|
41
|
+
|
|
42
|
+
## Customization
|
|
43
|
+
|
|
44
|
+
You can implement and use your own reporting engines with the default `HashDeepDiff::Delta` objects as a source of the report. In order to do so implement your own version of the reporting engine (example can be found [here](https://github.com/bpohoriletz/hash_deep_diff/tree/main/lib/hash_deep_diff/reports)) and inject it into a `Comparison`
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
left = { a: :a }
|
|
48
|
+
right = { a: :b }
|
|
49
|
+
|
|
50
|
+
HashDeepDiff::Comparison.new(left, right, reporting_engine: CustomEngine).report
|
|
51
|
+
```
|
|
37
52
|
|
|
38
53
|
## Contributing
|
|
39
54
|
|
data/Rakefile
CHANGED
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'
|
|
@@ -39,13 +40,15 @@ Gem::Specification.new do |spec|
|
|
|
39
40
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
40
41
|
spec.require_paths = ['lib']
|
|
41
42
|
|
|
42
|
-
spec.add_development_dependency 'bundler', '~>
|
|
43
|
+
spec.add_development_dependency 'bundler', '~> 2.3.11'
|
|
43
44
|
spec.add_development_dependency 'guard', '~> 2.18.0'
|
|
44
45
|
spec.add_development_dependency 'guard-minitest', '~> 2.4.6'
|
|
45
46
|
spec.add_development_dependency 'guard-rubocop', '~> 1.5.0'
|
|
47
|
+
spec.add_development_dependency 'guard-yard', '~> 2.2.1'
|
|
46
48
|
spec.add_development_dependency 'minitest', '~> 5.15.0'
|
|
47
49
|
spec.add_development_dependency 'minitest-focus', '~> 1.3.1'
|
|
48
50
|
spec.add_development_dependency 'minitest-reporters', '~> 1.5.0'
|
|
51
|
+
spec.add_development_dependency 'naught', '~> 1.1.0'
|
|
49
52
|
spec.add_development_dependency 'rake', '~> 10.5.0'
|
|
50
53
|
spec.add_development_dependency 'rubocop', '~> 1.26.1'
|
|
51
54
|
spec.add_development_dependency 'rubocop-minitest', '~> 0.18.0'
|
|
@@ -1,52 +1,150 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '
|
|
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
|
+
|
|
67
|
+
@left[key] || NO_VALUE
|
|
17
68
|
end
|
|
18
69
|
|
|
19
|
-
|
|
70
|
+
# @param [Object] key the key which value we're currently comparing
|
|
71
|
+
def right(key = NO_VALUE)
|
|
72
|
+
return NO_VALUE if @right == NO_VALUE
|
|
73
|
+
return @right if key == NO_VALUE
|
|
20
74
|
|
|
21
|
-
|
|
22
|
-
@left = left.to_hash
|
|
23
|
-
@right = right.to_hash
|
|
24
|
-
@path = path.to_ary
|
|
75
|
+
@right[key] || NO_VALUE
|
|
25
76
|
end
|
|
26
77
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
attr_reader :path
|
|
81
|
+
|
|
82
|
+
# @param [Object] original original version
|
|
83
|
+
# @param [Object] changed new version
|
|
84
|
+
# @param [Array] prefix keys to fetch current comparison (not empty for nested comparisons)
|
|
85
|
+
def initialize(original, changed, prefix = [], reporting_engine: Reports::Diff, delta_engine: Delta)
|
|
86
|
+
@left = original
|
|
87
|
+
@right = changed
|
|
88
|
+
@path = prefix.to_ary
|
|
89
|
+
@reporting_engine = reporting_engine
|
|
90
|
+
@delta_engine = delta_engine
|
|
35
91
|
end
|
|
36
92
|
|
|
37
|
-
|
|
93
|
+
# {Comparison} broken down into array of {Delta}
|
|
94
|
+
# @return [Array<HashDeepDiff::Delta>]
|
|
95
|
+
def deltas
|
|
96
|
+
return [delta] if common_keys.empty?
|
|
97
|
+
|
|
38
98
|
common_keys.each_with_object([]) do |key, memo|
|
|
39
|
-
|
|
40
|
-
value_right = right[key] || NO_VALUE
|
|
99
|
+
next if values_equal?(key)
|
|
41
100
|
|
|
42
|
-
|
|
101
|
+
memo << delta(key: key)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
43
104
|
|
|
44
|
-
|
|
105
|
+
# depending on circumstances will return necessary comparisons
|
|
106
|
+
# @return [Array<HashDeepDiff::Delta>]
|
|
107
|
+
def inward_comparison(complex_delta)
|
|
108
|
+
if complex_delta.partial?
|
|
109
|
+
[
|
|
110
|
+
complex_delta.placebo,
|
|
111
|
+
comparison(delta: complex_delta, modifier: :right).diff,
|
|
112
|
+
comparison(delta: complex_delta, modifier: :left).diff
|
|
113
|
+
].compact.flatten
|
|
114
|
+
# TOFIX add test an drop flatten
|
|
115
|
+
else
|
|
116
|
+
comparison(delta: complex_delta).diff
|
|
45
117
|
end
|
|
46
118
|
end
|
|
47
119
|
|
|
120
|
+
# @param [Object] key the key which value we're currently comparing
|
|
121
|
+
# @return [Bool]
|
|
122
|
+
def values_equal?(key)
|
|
123
|
+
right(key).instance_of?(left(key).class) && (right(key) == left(key))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# All keys from both original and compared objects
|
|
127
|
+
# @return [Array]
|
|
48
128
|
def common_keys
|
|
49
|
-
|
|
129
|
+
keys = []
|
|
130
|
+
keys += left.keys if left.respond_to?(:keys)
|
|
131
|
+
keys += right.keys if right.respond_to?(:keys)
|
|
132
|
+
|
|
133
|
+
keys.uniq
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [HashDeepDiff::Factories::Comparison]
|
|
137
|
+
def comparison_factory
|
|
138
|
+
HashDeepDiff::Factories::Comparison.new(reporting_engine: reporting_engine)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# factory function
|
|
142
|
+
# @return [HashDeepDiff::Delta]
|
|
143
|
+
def delta(key: NO_VALUE)
|
|
144
|
+
change_key = path
|
|
145
|
+
change_key += [key] unless key == NO_VALUE
|
|
146
|
+
|
|
147
|
+
HashDeepDiff::Delta.new(change_key: change_key, value: { left: left(key), right: right(key) })
|
|
50
148
|
end
|
|
51
149
|
end
|
|
52
150
|
end
|
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,83 @@ module HashDeepDiff
|
|
|
9
9
|
# - diff of { a: a } and { a: b } is { a: { left: a, right: b } }
|
|
10
10
|
# - diff of {} and { a: b } is { a: { left: HashDeepDiff::NO_VALUE, right: b } }
|
|
11
11
|
class Delta
|
|
12
|
-
|
|
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 [HashDeepDiff::Delta, NilClass]
|
|
20
|
+
def placebo
|
|
21
|
+
return nil unless partial?
|
|
22
|
+
|
|
23
|
+
placebo = simple_left? ? { left: NO_VALUE, right: {} } : { left: {}, right: NO_VALUE }
|
|
24
|
+
|
|
25
|
+
self.class.new(change_key: change_key, value: placebo)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# true if at least one of the values is a Hash
|
|
29
|
+
# @return [Bool]
|
|
30
|
+
def partial?
|
|
31
|
+
!composite? && !simple?
|
|
16
32
|
end
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
# true if both valus are Hashes
|
|
35
|
+
# @return [Bool]
|
|
36
|
+
def composite?
|
|
37
|
+
!simple_left? && !simple_right?
|
|
20
38
|
end
|
|
21
39
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
def
|
|
25
|
-
|
|
40
|
+
# true if none of the values is a Hash
|
|
41
|
+
# @return [Bool]
|
|
42
|
+
def simple?
|
|
43
|
+
simple_left? && simple_right?
|
|
26
44
|
end
|
|
27
45
|
|
|
46
|
+
# Original value
|
|
28
47
|
def left
|
|
29
|
-
|
|
48
|
+
value[:left]
|
|
30
49
|
end
|
|
31
50
|
|
|
51
|
+
# Value we compare to
|
|
32
52
|
def right
|
|
33
|
-
|
|
53
|
+
value[:right]
|
|
34
54
|
end
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
# see {#to_hash}
|
|
57
|
+
# @return [Hash]
|
|
58
|
+
def to_h
|
|
59
|
+
to_hash
|
|
38
60
|
end
|
|
39
61
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# TOFIX this may prohibit usage of hashes with Array keys
|
|
44
|
-
if path.respond_to?(:to_ary)
|
|
45
|
-
@delta = { path[-1] => value }
|
|
46
|
-
@value = value
|
|
47
|
-
@prefix = path[0..-2]
|
|
48
|
-
else
|
|
49
|
-
@delta = { path => value }
|
|
50
|
-
@value = value
|
|
51
|
-
@prefix = []
|
|
52
|
-
end
|
|
62
|
+
# @return [Hash]
|
|
63
|
+
def to_hash
|
|
64
|
+
{ change_key[-1] => value }
|
|
53
65
|
end
|
|
54
66
|
|
|
55
|
-
|
|
56
|
-
return nil if left == NO_VALUE
|
|
67
|
+
private
|
|
57
68
|
|
|
58
|
-
|
|
59
|
-
left.keys.map { |key| "-left#{diff_prefix}[#{key}] = #{left[key]}" }.join("\n")
|
|
60
|
-
else
|
|
61
|
-
"-left#{diff_prefix} = #{left}"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
69
|
+
attr_reader :value
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
# @param [Array] change_key list of keys to fetch values we're comparing
|
|
72
|
+
# @param [Hash<(:left, :right), Object>] value +Hash+ object with two keys - :left and :right,
|
|
73
|
+
# that represents compared original value (at :left) and value we compare to (at :right)
|
|
74
|
+
def initialize(change_key:, value:)
|
|
75
|
+
@value = value
|
|
76
|
+
@change_key = change_key
|
|
77
|
+
end
|
|
67
78
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
79
|
+
# Returns true if left value has no nested Hashes
|
|
80
|
+
# @return [Bool]
|
|
81
|
+
def simple_left?
|
|
82
|
+
!left.respond_to?(:to_hash)
|
|
73
83
|
end
|
|
74
84
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
# Returns true if right value has no nested Hashes
|
|
86
|
+
# @return [Bool]
|
|
87
|
+
def simple_right?
|
|
88
|
+
!right.respond_to?(:to_hash)
|
|
78
89
|
end
|
|
79
90
|
end
|
|
80
91
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
|
|
5
|
+
module HashDeepDiff
|
|
6
|
+
# factories
|
|
7
|
+
module Factories
|
|
8
|
+
# Factory for {HashDeepDiff::Comparison}
|
|
9
|
+
class Comparison
|
|
10
|
+
extend Forwardable
|
|
11
|
+
def_delegators :delta, :left, :right, :change_key
|
|
12
|
+
|
|
13
|
+
# factory function
|
|
14
|
+
# @return [Comparison]
|
|
15
|
+
def comparison(delta:, modifier: nil)
|
|
16
|
+
@delta = delta
|
|
17
|
+
|
|
18
|
+
case modifier
|
|
19
|
+
when nil
|
|
20
|
+
full_compare
|
|
21
|
+
when :left
|
|
22
|
+
compare_left
|
|
23
|
+
when :right
|
|
24
|
+
compare_right
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :reporting_engine, :delta
|
|
31
|
+
|
|
32
|
+
def initialize(reporting_engine:)
|
|
33
|
+
@reporting_engine = reporting_engine
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# compare two hashes
|
|
37
|
+
def full_compare
|
|
38
|
+
HashDeepDiff::Comparison.new(left, right, change_key,
|
|
39
|
+
delta_engine: delta.class,
|
|
40
|
+
reporting_engine: reporting_engine)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# compare Hash with nothing (deletion)
|
|
44
|
+
def compare_left
|
|
45
|
+
HashDeepDiff::Comparison.new(left, NO_VALUE, change_key,
|
|
46
|
+
delta_engine: delta.class,
|
|
47
|
+
reporting_engine: reporting_engine)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# compare nothing with Hash (addition)
|
|
51
|
+
def compare_right
|
|
52
|
+
HashDeepDiff::Comparison.new(NO_VALUE, right, change_key,
|
|
53
|
+
delta_engine: delta.class,
|
|
54
|
+
reporting_engine: reporting_engine)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HashDeepDiff
|
|
4
|
+
# Different reporting enjines for {Delta}
|
|
5
|
+
module Reports
|
|
6
|
+
# Abstract Class
|
|
7
|
+
class Base
|
|
8
|
+
# see {#to_str}
|
|
9
|
+
# @return [String]
|
|
10
|
+
def to_s
|
|
11
|
+
to_str
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# A report on additions and deletions
|
|
15
|
+
# @return [String]
|
|
16
|
+
def to_str
|
|
17
|
+
original + replacement
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :old_val, :new_val, :change_key
|
|
23
|
+
|
|
24
|
+
# @param [Delta] delta diff to report
|
|
25
|
+
def initialize(delta:)
|
|
26
|
+
@change_key = delta.change_key.to_ary
|
|
27
|
+
@old_val = delta.left
|
|
28
|
+
@new_val = delta.right
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# old value
|
|
32
|
+
def original
|
|
33
|
+
raise AbstractMethodError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# new value
|
|
37
|
+
def replacement
|
|
38
|
+
raise AbstractMethodError
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module HashDeepDiff
|
|
6
|
+
# Different reporting enjines for {Delta}
|
|
7
|
+
module Reports
|
|
8
|
+
# Visual representation of the {Delta} as diff
|
|
9
|
+
class Diff < Base
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# old value
|
|
13
|
+
# @return [String]
|
|
14
|
+
def original
|
|
15
|
+
return '' if old_val == NO_VALUE
|
|
16
|
+
|
|
17
|
+
return "#{deletion}#{path} = #{old_val}\n"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# new value
|
|
21
|
+
# @return [String]
|
|
22
|
+
def replacement
|
|
23
|
+
return '' if new_val == NO_VALUE
|
|
24
|
+
|
|
25
|
+
return "#{addition}#{path} = #{new_val}\n"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Visual representation of keys from compared objects needed to fetch the compared values
|
|
29
|
+
# @return [String]
|
|
30
|
+
def path
|
|
31
|
+
change_key.map { |key| "[#{key}]" }.join
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# visual indication of addition
|
|
35
|
+
# @return [String]
|
|
36
|
+
def addition
|
|
37
|
+
'+left'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# visual indication of deletion
|
|
41
|
+
# @return [String]
|
|
42
|
+
def deletion
|
|
43
|
+
'-left'
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bohdan Pohorilets
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-04-
|
|
11
|
+
date: 2022-04-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -16,14 +16,14 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version:
|
|
19
|
+
version: 2.3.11
|
|
20
20
|
type: :development
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version:
|
|
26
|
+
version: 2.3.11
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: guard
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: 1.5.0
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: guard-yard
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 2.2.1
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 2.2.1
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
84
|
name: minitest
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -108,6 +122,20 @@ dependencies:
|
|
|
108
122
|
- - "~>"
|
|
109
123
|
- !ruby/object:Gem::Version
|
|
110
124
|
version: 1.5.0
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: naught
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 1.1.0
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: 1.1.0
|
|
111
139
|
- !ruby/object:Gem::Dependency
|
|
112
140
|
name: rake
|
|
113
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -173,11 +201,14 @@ extra_rdoc_files: []
|
|
|
173
201
|
files:
|
|
174
202
|
- ".bundle/.keep"
|
|
175
203
|
- ".bundle/config"
|
|
204
|
+
- ".github/workflows/ci.yml"
|
|
176
205
|
- ".gitignore"
|
|
177
206
|
- ".rubocop.yml"
|
|
207
|
+
- ".yardopts"
|
|
178
208
|
- CHANGELOG.md
|
|
179
209
|
- Gemfile
|
|
180
210
|
- Guardfile
|
|
211
|
+
- MIT-LICENSE
|
|
181
212
|
- README.md
|
|
182
213
|
- Rakefile
|
|
183
214
|
- bin/_guard-core
|
|
@@ -187,11 +218,16 @@ files:
|
|
|
187
218
|
- bin/rake
|
|
188
219
|
- bin/rubocop
|
|
189
220
|
- bin/setup
|
|
221
|
+
- bin/yard
|
|
222
|
+
- bin/yardoc
|
|
223
|
+
- bin/yri
|
|
190
224
|
- hash_deep_diff.gemspec
|
|
191
225
|
- lib/hash_deep_diff.rb
|
|
192
|
-
- lib/hash_deep_diff/acts_as_hash.rb
|
|
193
226
|
- lib/hash_deep_diff/comparison.rb
|
|
194
227
|
- lib/hash_deep_diff/delta.rb
|
|
228
|
+
- lib/hash_deep_diff/factories/comparison.rb
|
|
229
|
+
- lib/hash_deep_diff/reports/base.rb
|
|
230
|
+
- lib/hash_deep_diff/reports/diff.rb
|
|
195
231
|
- lib/hash_deep_diff/version.rb
|
|
196
232
|
homepage: https://github.com/bpohoriletz/hash_deep_diff
|
|
197
233
|
licenses:
|
|
@@ -199,6 +235,7 @@ licenses:
|
|
|
199
235
|
metadata:
|
|
200
236
|
allowed_push_host: https://rubygems.org/
|
|
201
237
|
homepage_uri: https://github.com/bpohoriletz/hash_deep_diff
|
|
238
|
+
documentation_uri: https://rdoc.info/gems/hash_deep_diff
|
|
202
239
|
source_code_uri: https://github.com/bpohoriletz/hash_deep_diff
|
|
203
240
|
changelog_uri: https://github.com/bpohoriletz/hash_deep_diff/blob/main/CHANGELOG.md
|
|
204
241
|
rubygems_mfa_required: 'true'
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'forwardable'
|
|
4
|
-
|
|
5
|
-
module HashDeepDiff
|
|
6
|
-
# This module includes behavior that is needed to use deltas instead of Hash inside this gem
|
|
7
|
-
module ActsAsHash
|
|
8
|
-
def self.included(base)
|
|
9
|
-
base.include(InstanceMethods)
|
|
10
|
-
base.extend(Forwardable)
|
|
11
|
-
base.def_delegators :@delta, :==, :each_with_object, :each_key, :[],
|
|
12
|
-
:to_a, :empty?, :keys
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Assumes that the class will include method delta that will return a representation of an
|
|
16
|
-
# instance of a class as a Hash
|
|
17
|
-
module InstanceMethods
|
|
18
|
-
def to_h
|
|
19
|
-
@delta
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def to_hash
|
|
23
|
-
@delta
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|