hash_deep_diff 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 965ed70dc327febfcab65e1d8a9ae2511e06b117316a368140eae6a00dc8afba
4
- data.tar.gz: f905ef6c174c729ec28f9d585db116deb32727276ae61b1e448f4c17917cbf47
3
+ metadata.gz: 3aab07a4c397dbc84845d5f0faff052bbae89eb8be4bf24a5ed9df0d5129c3da
4
+ data.tar.gz: 2ea89b301d808655625e0fa6a9c5028b9d194bba3c4dc54cf1f5c6859c59fc6f
5
5
  SHA512:
6
- metadata.gz: 6669aa02e12598277b0d59e0abb1f173a1a28dfb5aa9770e5c209bde4972bb7e9166788dfcae30743fcc95050bf23104e768edae1ecec855f07b72fa704ad46a
7
- data.tar.gz: d4f9fc51fc4acbaaf0228f84c8aa47e84cb1b17dbad6e45df7047df0cf086b7ff6deeab85de648ec5916174b0d12e1a982bac975d9a05ef65609714f23ab91dc
6
+ metadata.gz: f0a05ee023dbbd4d8551b5065a57a65b645ca3b0a864eace0de91b10ec08da77b0a15df6422af0f5ce0afd4459ca141e227398f9560198f6d350963fac1a99f0
7
+ data.tar.gz: 279185ca8464992a65f2fff626b6ee90f44ac7b7e98706e34443390ad24457961b642e5b2ebee10c4c8e3e5c72b7bfee454dd644f8f7ef68db5872c426137dc7
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --private
data/Guardfile CHANGED
@@ -30,3 +30,7 @@ guard :minitest do
30
30
  watch(%r{^test/test_helper\.rb$}) { 'test' }
31
31
  watch(%r{^test/support/.*$}) { 'test' }
32
32
  end
33
+
34
+ guard 'yard' do
35
+ watch(%r{lib/.+\.rb})
36
+ end
data/README.md CHANGED
@@ -36,6 +36,8 @@ HashDeepDiff::Comparison.new(left, right).report
36
36
  - left[a] = a
37
37
  + left[a] = b
38
38
  ```
39
+ please see [Documentation](https://rdoc.info/gems/hash_deep_diff) for
40
+ more info
39
41
 
40
42
  ## Contributing
41
43
 
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")
@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
43
43
  spec.add_development_dependency 'guard', '~> 2.18.0'
44
44
  spec.add_development_dependency 'guard-minitest', '~> 2.4.6'
45
45
  spec.add_development_dependency 'guard-rubocop', '~> 1.5.0'
46
+ spec.add_development_dependency 'guard-yard', '~> 2.2.1'
46
47
  spec.add_development_dependency 'minitest', '~> 5.15.0'
47
48
  spec.add_development_dependency 'minitest-focus', '~> 1.3.1'
48
49
  spec.add_development_dependency 'minitest-reporters', '~> 1.5.0'
@@ -3,22 +3,26 @@
3
3
  require 'forwardable'
4
4
 
5
5
  module HashDeepDiff
6
- # This module includes behavior that is needed to use deltas instead of Hash inside this gem
6
+ # This module includes behavior that is needed to use instances of Delta instead of Hash
7
+ # in this gem
7
8
  module ActsAsHash
9
+ # @param [Object] base a hook that is invoked when module is included in a class
8
10
  def self.included(base)
9
- base.include(InstanceMethods)
10
- base.extend(Forwardable)
11
+ base.extend Forwardable
11
12
  base.def_delegators :@delta, :==, :each_with_object, :each_key, :[],
12
13
  :to_a, :empty?, :keys
14
+ base.include InstanceMethods
13
15
  end
14
16
 
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
+ # 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
17
19
  module InstanceMethods
20
+ # a +Hash+ representation of an object
18
21
  def to_h
19
- @delta
22
+ to_hash
20
23
  end
21
24
 
25
+ # a +Hash+ representation of an object
22
26
  def to_hash
23
27
  @delta
24
28
  end
@@ -2,49 +2,101 @@
2
2
 
3
3
  require_relative 'delta'
4
4
 
5
- # :nodoc:
6
5
  module HashDeepDiff
7
- # An instrument to build and report the difference between two hash-like objects
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
38
+ # @!attribute [r] left
39
+ # @return [Hash] original version of the Hash
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)
9
44
  attr_reader :left, :right, :path
10
45
 
11
- def diff
12
- deep_delta
13
- end
14
-
46
+ # @return [String]
15
47
  def report
16
48
  diff.join("\n")
17
49
  end
18
50
 
51
+ # @return [Array<HashDeepDiff::Delta>]
52
+ def diff
53
+ comparison.flat_map do |delta|
54
+ # if there are nested hashes we need to compare them furter
55
+ # if no we return difference between values (HashDeepDiff::Delta)
56
+ delta.complex? ? self.class.new(delta.left, delta.right, delta.path).diff : delta
57
+ end
58
+ end
59
+
19
60
  private
20
61
 
21
- def initialize(left, right, path = [])
62
+ # @param [Hash] left original version of the hash
63
+ # @param [Hash] right new version of the hash
64
+ # @param [Array] prefix keys to fetch current comparison (not empty for nested comparisons)
65
+ def initialize(left, right, prefix = [])
22
66
  @left = left.to_hash
23
67
  @right = right.to_hash
24
- @path = path.to_ary
68
+ @path = prefix.to_ary
25
69
  end
26
70
 
27
- def deep_delta
28
- delta.flat_map do |diff|
29
- if diff.complex?
30
- self.class.new(diff.left, diff.right, diff.path).diff
31
- else
32
- diff
33
- end
71
+ # @return [Array<HashDeepDiff::Delta>]
72
+ def comparison
73
+ common_keys.each_with_object([]) do |key, memo|
74
+ next if values_equal?(key)
75
+
76
+ memo << Delta.new(path: path + [key], value: { left: value_left(key), right: value_right(key) })
34
77
  end
35
78
  end
36
79
 
37
- def delta
38
- common_keys.each_with_object([]) do |key, memo|
39
- value_left = left[key] || NO_VALUE
40
- value_right = right[key] || NO_VALUE
80
+ # @param [Object] key the key which value we're currently comparing
81
+ # @return [Bool]
82
+ def values_equal?(key)
83
+ value_right(key).instance_of?(value_left(key).class) && (value_right(key) == value_left(key))
84
+ end
41
85
 
42
- next if value_right.instance_of?(value_left.class) && (value_right == value_left)
86
+ # Original value
87
+ # @param [Object] key the key which value we're currently comparing
88
+ def value_left(key)
89
+ left[key] || NO_VALUE
90
+ end
43
91
 
44
- memo << Delta.new(path: path + [key], value: { left: value_left, right: value_right })
45
- end
92
+ # Value we compare to
93
+ # @param [Object] key the key which value we're currently comparing
94
+ def value_right(key)
95
+ right[key] || NO_VALUE
46
96
  end
47
97
 
98
+ # All keys from both original and compared objects
99
+ # @return [Array]
48
100
  def common_keys
49
101
  (left.keys + right.keys).uniq
50
102
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'acts_as_hash'
4
+ require_relative 'report'
4
5
 
5
6
  module HashDeepDiff
6
7
  # Representation of the diff of two values
@@ -11,36 +12,47 @@ module HashDeepDiff
11
12
  class Delta
12
13
  include ActsAsHash
13
14
 
15
+ # Visual representation of additions and deletiond at given +path+
16
+ # @return [String]
14
17
  def to_str
15
18
  [deletion, addition].compact.join("\n")
16
19
  end
17
20
 
21
+ # Returns true if we have nested Hashes
22
+ # @return [Bool]
18
23
  def complex?
19
24
  left.respond_to?(:to_hash) && right.respond_to?(:to_hash)
20
25
  end
21
26
 
22
- # TOFIX poor naming
23
- # overrides parameter in initializer
27
+ # Keys needed to fetch values that we're comparing
28
+ # @return [Array]
24
29
  def path
25
30
  @prefix + [@delta.keys.first]
26
31
  end
27
32
 
33
+ # Original value
28
34
  def left
29
35
  @value[:left]
30
36
  end
31
37
 
38
+ # Value we compare to
32
39
  def right
33
40
  @value[:right]
34
41
  end
35
42
 
43
+ # See {#to_str}
36
44
  def to_s
37
45
  to_str
38
46
  end
39
47
 
40
48
  private
41
49
 
50
+ # @param [Array, Object] path list of keys to fetch values we're comparing
51
+ # @param [Hash<(:left, :right), Object>] value +Hash+ object with two keys - :left and :right,
52
+ # that represents compared original value (at :left) and value we compare to (at :right)
42
53
  def initialize(path:, value:)
43
54
  # TOFIX this may prohibit usage of hashes with Array keys
55
+ # TOFIX extract path to a separate object
44
56
  if path.respond_to?(:to_ary)
45
57
  @delta = { path[-1] => value }
46
58
  @value = value
@@ -52,29 +64,20 @@ module HashDeepDiff
52
64
  end
53
65
  end
54
66
 
67
+ # Visual representation of additions
68
+ # @return [NillClass, String]
55
69
  def deletion
56
70
  return nil if left == NO_VALUE
57
71
 
58
- if left.respond_to?(:to_hash)
59
- left.keys.map { |key| "-left#{diff_prefix}[#{key}] = #{left[key]}" }.join("\n")
60
- else
61
- "-left#{diff_prefix} = #{left}"
62
- end
72
+ Report.new(path: path, value: left, mode: Report::Mode::DELETION)
63
73
  end
64
74
 
75
+ # Visual representation of deletions
76
+ # @return [NillClass, String]
65
77
  def addition
66
78
  return nil if right == NO_VALUE
67
79
 
68
- if right.respond_to?(:to_hash)
69
- right.keys.map { |key| "+left#{diff_prefix}[#{key}] = #{right[key]}" }.join("\n")
70
- else
71
- "+left#{diff_prefix} = #{right}"
72
- end
73
- end
74
-
75
- # TOFIX poor naming
76
- def diff_prefix
77
- path.map { |key| "[#{key}]" }.join
80
+ Report.new(path: path, value: right)
78
81
  end
79
82
  end
80
83
  end
@@ -0,0 +1,53 @@
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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HashDeepDiff
4
- VERSION = '0.4.1'
4
+ # Version of a gem
5
+ VERSION = '0.5.0'
5
6
  end
@@ -3,7 +3,8 @@
3
3
  require 'hash_deep_diff/version'
4
4
  require 'hash_deep_diff/comparison'
5
5
 
6
+ # Global namespace
6
7
  module HashDeepDiff
8
+ # value was not found
7
9
  NO_VALUE = Class.new(NilClass)
8
- class Error < StandardError; end
9
10
  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.1
4
+ version: 0.5.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-17 00:00:00.000000000 Z
11
+ date: 2022-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 1.5.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.2.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.2.1
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: minitest
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -176,6 +190,7 @@ files:
176
190
  - ".github/workflows/ci.yml"
177
191
  - ".gitignore"
178
192
  - ".rubocop.yml"
193
+ - ".yardopts"
179
194
  - CHANGELOG.md
180
195
  - Gemfile
181
196
  - Guardfile
@@ -189,11 +204,15 @@ files:
189
204
  - bin/rake
190
205
  - bin/rubocop
191
206
  - bin/setup
207
+ - bin/yard
208
+ - bin/yardoc
209
+ - bin/yri
192
210
  - hash_deep_diff.gemspec
193
211
  - lib/hash_deep_diff.rb
194
212
  - lib/hash_deep_diff/acts_as_hash.rb
195
213
  - lib/hash_deep_diff/comparison.rb
196
214
  - lib/hash_deep_diff/delta.rb
215
+ - lib/hash_deep_diff/report.rb
197
216
  - lib/hash_deep_diff/version.rb
198
217
  homepage: https://github.com/bpohoriletz/hash_deep_diff
199
218
  licenses: