philiprehberger-differ 0.1.2 → 0.2.2
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/CHANGELOG.md +21 -4
- data/README.md +81 -7
- data/lib/philiprehberger/differ/changeset.rb +8 -0
- data/lib/philiprehberger/differ/comparator.rb +54 -15
- data/lib/philiprehberger/differ/formatters.rb +46 -0
- data/lib/philiprehberger/differ/similarity.rb +80 -0
- data/lib/philiprehberger/differ/version.rb +1 -1
- data/lib/philiprehberger/differ.rb +8 -2
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2895233828f37deff74861ac0c8c0fb7b7f678c644aeda4b4db14344760c38e
|
|
4
|
+
data.tar.gz: c1ebbe193bce2b499bbb9ad91f29a74949180a69de143440becb5d26b15acaf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a4c3e61b19d56fcb837339e4789b38ee3a2cfc32000a98d6f7ab5dd1f09b8730d694431ccfdf8416a506f2f34cec2106825b9144979f06f481e589ef52629b7
|
|
7
|
+
data.tar.gz: 5ea7bea69ce7d9a0736cfda3fbd6034ecadb1b0d514908069a8ccc459064b7636fe47b16424739e6adc500f4222b106ab12d5da12c606887220c2e4f50004de1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.2.2
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
- Revert gemspec to single-quoted strings per RuboCop default configuration
|
|
6
|
+
|
|
7
|
+
## 0.2.1
|
|
8
|
+
|
|
9
|
+
- Fix RuboCop Style/StringLiterals violations in gemspec
|
|
8
10
|
|
|
9
11
|
All notable changes to this gem will be documented in this file.
|
|
10
12
|
|
|
11
13
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
12
14
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
13
15
|
|
|
16
|
+
## [0.2.0] - 2026-03-17
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Ignore paths with symbol support: `Differ.diff(a, b, ignore: [:updated_at, "user.email"])`
|
|
20
|
+
- Similarity score: `Differ.similarity(a, b)` returns Float between 0.0 and 1.0
|
|
21
|
+
- Text formatter: `changeset.to_text` for human-readable +/- output
|
|
22
|
+
- JSON Patch formatter: `changeset.to_json_patch` for RFC 6902 operations
|
|
23
|
+
- Nested array diff by key: `Differ.diff(a, b, array_key: :id)` matches elements by field
|
|
24
|
+
|
|
25
|
+
## [0.1.2]
|
|
26
|
+
|
|
27
|
+
- Add License badge to README
|
|
28
|
+
- Add bug_tracker_uri to gemspec
|
|
29
|
+
- Add Requirements section to README
|
|
30
|
+
|
|
14
31
|
## [Unreleased]
|
|
15
32
|
|
|
16
33
|
## [0.1.0] - 2026-03-15
|
data/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# philiprehberger-differ
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/philiprehberger-differ)
|
|
4
|
+
[](https://github.com/philiprehberger/rb-differ/actions/workflows/ci.yml)
|
|
4
5
|
[](LICENSE)
|
|
5
6
|
|
|
6
|
-
Deep structural diff for hashes, arrays, and nested objects in Ruby
|
|
7
|
+
Deep structural diff for hashes, arrays, and nested objects in Ruby
|
|
7
8
|
|
|
8
9
|
## Requirements
|
|
9
10
|
|
|
@@ -19,7 +20,7 @@ gem 'philiprehberger-differ'
|
|
|
19
20
|
|
|
20
21
|
Or install directly:
|
|
21
22
|
|
|
22
|
-
```
|
|
23
|
+
```bash
|
|
23
24
|
gem install philiprehberger-differ
|
|
24
25
|
```
|
|
25
26
|
|
|
@@ -44,17 +45,87 @@ result = changeset.apply(old_data)
|
|
|
44
45
|
|
|
45
46
|
# Revert changes to produce old version from new
|
|
46
47
|
original = changeset.revert(new_data)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Ignore Paths
|
|
51
|
+
|
|
52
|
+
Exclude specific keys from comparison. Supports both symbols and dot-notation strings for nested paths:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
changeset = Philiprehberger::Differ.diff(old_data, new_data, ignore: [:updated_at, :metadata])
|
|
47
56
|
|
|
48
|
-
# Ignore
|
|
49
|
-
changeset = Philiprehberger::Differ.diff(old_data, new_data, ignore: ['
|
|
57
|
+
# Ignore nested paths
|
|
58
|
+
changeset = Philiprehberger::Differ.diff(old_data, new_data, ignore: ['user.email', 'meta.version'])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Similarity Score
|
|
62
|
+
|
|
63
|
+
Get a ratio of unchanged fields to total fields, returned as a Float between 0.0 and 1.0:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
score = Philiprehberger::Differ.similarity(old_data, new_data)
|
|
67
|
+
# => 0.5 (half the fields are identical)
|
|
68
|
+
|
|
69
|
+
score = Philiprehberger::Differ.similarity(old_data, old_data)
|
|
70
|
+
# => 1.0 (identical)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Text Formatter
|
|
74
|
+
|
|
75
|
+
Human-readable text output with +/- prefixes:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
changeset = Philiprehberger::Differ.diff(
|
|
79
|
+
{ name: 'Alice', age: 30 },
|
|
80
|
+
{ name: 'Bob', email: 'bob@example.com' }
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
puts changeset.to_text
|
|
84
|
+
# ~ name: "Alice" -> "Bob"
|
|
85
|
+
# - age: 30
|
|
86
|
+
# + email: "bob@example.com"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### JSON Patch Format
|
|
90
|
+
|
|
91
|
+
Returns an array of RFC 6902 JSON Patch operations:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
changeset = Philiprehberger::Differ.diff(
|
|
95
|
+
{ name: 'Alice', age: 30 },
|
|
96
|
+
{ name: 'Bob' }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
changeset.to_json_patch
|
|
100
|
+
# => [
|
|
101
|
+
# { op: "replace", path: "/name", value: "Bob" },
|
|
102
|
+
# { op: "remove", path: "/age" }
|
|
103
|
+
# ]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Nested Array Diff by Key
|
|
107
|
+
|
|
108
|
+
Match array elements by a key field instead of index for smarter array comparison:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
old_data = { users: [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }] }
|
|
112
|
+
new_data = { users: [{ id: 2, name: 'Bobby' }, { id: 1, name: 'Alice' }] }
|
|
113
|
+
|
|
114
|
+
# Without array_key: detects changes at every index (order-sensitive)
|
|
115
|
+
# With array_key: matches by :id, only detects Bob -> Bobby change
|
|
116
|
+
changeset = Philiprehberger::Differ.diff(old_data, new_data, array_key: :id)
|
|
50
117
|
```
|
|
51
118
|
|
|
52
119
|
## API
|
|
53
120
|
|
|
54
|
-
### `Philiprehberger::Differ.diff(old_val, new_val, ignore: [])`
|
|
121
|
+
### `Philiprehberger::Differ.diff(old_val, new_val, ignore: [], array_key: nil)`
|
|
55
122
|
|
|
56
123
|
Returns a `Changeset` representing all structural differences.
|
|
57
124
|
|
|
125
|
+
### `Philiprehberger::Differ.similarity(old_val, new_val, ignore: [], array_key: nil)`
|
|
126
|
+
|
|
127
|
+
Returns a Float between 0.0 (completely different) and 1.0 (identical).
|
|
128
|
+
|
|
58
129
|
### `Changeset`
|
|
59
130
|
|
|
60
131
|
| Method | Description |
|
|
@@ -67,6 +138,8 @@ Returns a `Changeset` representing all structural differences.
|
|
|
67
138
|
| `apply(hash)` | Applies changes to produce the new version |
|
|
68
139
|
| `revert(hash)` | Reverts changes to produce the old version |
|
|
69
140
|
| `to_h` | Serializable hash representation |
|
|
141
|
+
| `to_text` | Human-readable text with +/- prefixes |
|
|
142
|
+
| `to_json_patch` | Array of RFC 6902 JSON Patch operations |
|
|
70
143
|
|
|
71
144
|
### `Change`
|
|
72
145
|
|
|
@@ -81,9 +154,10 @@ Returns a `Changeset` representing all structural differences.
|
|
|
81
154
|
|
|
82
155
|
## Development
|
|
83
156
|
|
|
84
|
-
```
|
|
157
|
+
```bash
|
|
85
158
|
bundle install
|
|
86
|
-
bundle exec rspec
|
|
159
|
+
bundle exec rspec # Run tests
|
|
160
|
+
bundle exec rubocop # Check code style
|
|
87
161
|
```
|
|
88
162
|
|
|
89
163
|
## License
|
|
@@ -3,51 +3,88 @@
|
|
|
3
3
|
module Philiprehberger
|
|
4
4
|
module Differ
|
|
5
5
|
class Comparator
|
|
6
|
-
def self.call(old_val, new_val, path: '', ignore: [])
|
|
7
|
-
return [] if
|
|
6
|
+
def self.call(old_val, new_val, path: '', ignore: [], array_key: nil)
|
|
7
|
+
return [] if ignored?(path, ignore)
|
|
8
8
|
|
|
9
|
+
opts = { ignore: ignore, array_key: array_key }
|
|
9
10
|
case [old_val, new_val]
|
|
10
|
-
in [Hash, Hash] then compare_hashes(old_val, new_val, path,
|
|
11
|
-
in [Array, Array] then compare_arrays(old_val, new_val, path,
|
|
11
|
+
in [Hash, Hash] then compare_hashes(old_val, new_val, path, opts)
|
|
12
|
+
in [Array, Array] then compare_arrays(old_val, new_val, path, opts)
|
|
12
13
|
else compare_scalars(old_val, new_val, path)
|
|
13
14
|
end
|
|
14
15
|
end
|
|
15
16
|
|
|
16
|
-
def self.
|
|
17
|
+
def self.ignored?(path, ignore)
|
|
18
|
+
ignore.any? { |p| p.to_s == path }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.compare_hashes(old_hash, new_hash, path, opts)
|
|
17
22
|
(old_hash.keys + new_hash.keys).uniq.each_with_object([]) do |key, changes|
|
|
18
23
|
full_path = path.empty? ? key.to_s : "#{path}.#{key}"
|
|
19
|
-
next if
|
|
24
|
+
next if ignored?(full_path, opts[:ignore])
|
|
20
25
|
|
|
21
|
-
changes.concat(hash_key_diff(old_hash, new_hash, key, full_path,
|
|
26
|
+
changes.concat(hash_key_diff(old_hash, new_hash, key, full_path, opts))
|
|
22
27
|
end
|
|
23
28
|
end
|
|
24
29
|
|
|
25
|
-
def self.hash_key_diff(old_hash, new_hash, key, full_path,
|
|
30
|
+
def self.hash_key_diff(old_hash, new_hash, key, full_path, opts)
|
|
26
31
|
if !old_hash.key?(key)
|
|
27
32
|
[Change.new(path: full_path, type: :added, new_value: new_hash[key])]
|
|
28
33
|
elsif !new_hash.key?(key)
|
|
29
34
|
[Change.new(path: full_path, type: :removed, old_value: old_hash[key])]
|
|
30
35
|
else
|
|
31
|
-
call(old_hash[key], new_hash[key], path: full_path,
|
|
36
|
+
call(old_hash[key], new_hash[key], path: full_path, **opts)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.compare_arrays(old_arr, new_arr, path, opts)
|
|
41
|
+
if opts[:array_key] && old_arr.first.is_a?(Hash)
|
|
42
|
+
compare_arrays_by_key(old_arr, new_arr, path, opts)
|
|
43
|
+
else
|
|
44
|
+
compare_arrays_by_index(old_arr, new_arr, path, opts)
|
|
32
45
|
end
|
|
33
46
|
end
|
|
34
47
|
|
|
35
|
-
def self.
|
|
48
|
+
def self.compare_arrays_by_index(old_arr, new_arr, path, opts)
|
|
36
49
|
[old_arr.length, new_arr.length].max.times.each_with_object([]) do |idx, changes|
|
|
37
50
|
full_path = "#{path}.#{idx}"
|
|
38
|
-
next if
|
|
51
|
+
next if ignored?(full_path, opts[:ignore])
|
|
39
52
|
|
|
40
|
-
changes.concat(array_idx_diff(old_arr, new_arr, idx, full_path,
|
|
53
|
+
changes.concat(array_idx_diff(old_arr, new_arr, idx, full_path, opts))
|
|
41
54
|
end
|
|
42
55
|
end
|
|
43
56
|
|
|
44
|
-
def self.array_idx_diff(old_arr, new_arr, idx, full_path,
|
|
57
|
+
def self.array_idx_diff(old_arr, new_arr, idx, full_path, opts)
|
|
45
58
|
if idx >= old_arr.length
|
|
46
59
|
[Change.new(path: full_path, type: :added, new_value: new_arr[idx])]
|
|
47
60
|
elsif idx >= new_arr.length
|
|
48
61
|
[Change.new(path: full_path, type: :removed, old_value: old_arr[idx])]
|
|
49
62
|
else
|
|
50
|
-
call(old_arr[idx], new_arr[idx], path: full_path,
|
|
63
|
+
call(old_arr[idx], new_arr[idx], path: full_path, **opts)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.compare_arrays_by_key(old_arr, new_arr, path, opts)
|
|
68
|
+
old_map = index_by_key(old_arr, opts[:array_key])
|
|
69
|
+
new_map = index_by_key(new_arr, opts[:array_key])
|
|
70
|
+
all_keys = (old_map.keys + new_map.keys).uniq
|
|
71
|
+
all_keys.each_with_object([]) do |key_val, changes|
|
|
72
|
+
changes.concat(keyed_element_diff(old_map, new_map, key_val, path, opts))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.index_by_key(arr, key)
|
|
77
|
+
arr.to_h { |item| [item[key], item] }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.keyed_element_diff(old_map, new_map, key_val, path, opts)
|
|
81
|
+
full_path = "#{path}.#{key_val}"
|
|
82
|
+
if !old_map.key?(key_val)
|
|
83
|
+
[Change.new(path: full_path, type: :added, new_value: new_map[key_val])]
|
|
84
|
+
elsif !new_map.key?(key_val)
|
|
85
|
+
[Change.new(path: full_path, type: :removed, old_value: old_map[key_val])]
|
|
86
|
+
else
|
|
87
|
+
call(old_map[key_val], new_map[key_val], path: full_path, **opts)
|
|
51
88
|
end
|
|
52
89
|
end
|
|
53
90
|
|
|
@@ -58,7 +95,9 @@ module Philiprehberger
|
|
|
58
95
|
end
|
|
59
96
|
|
|
60
97
|
private_class_method :compare_hashes, :compare_arrays, :compare_scalars,
|
|
61
|
-
:hash_key_diff, :array_idx_diff
|
|
98
|
+
:hash_key_diff, :array_idx_diff, :ignored?,
|
|
99
|
+
:compare_arrays_by_index, :compare_arrays_by_key,
|
|
100
|
+
:index_by_key, :keyed_element_diff
|
|
62
101
|
end
|
|
63
102
|
end
|
|
64
103
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Philiprehberger
|
|
6
|
+
module Differ
|
|
7
|
+
module Formatters
|
|
8
|
+
module Text
|
|
9
|
+
def self.format(changeset)
|
|
10
|
+
changeset.changes.map { |c| format_change(c) }.join("\n")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.format_change(change)
|
|
14
|
+
case change.type
|
|
15
|
+
when :added then "+ #{change.path}: #{change.new_value.inspect}"
|
|
16
|
+
when :removed then "- #{change.path}: #{change.old_value.inspect}"
|
|
17
|
+
when :changed then changed_line(change)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.changed_line(change)
|
|
22
|
+
"~ #{change.path}: #{change.old_value.inspect} -> #{change.new_value.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private_class_method :format_change, :changed_line
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module JsonPatch
|
|
29
|
+
def self.format(changeset)
|
|
30
|
+
changeset.changes.map { |c| format_op(c) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.format_op(change)
|
|
34
|
+
path = "/#{change.path.gsub('.', '/')}"
|
|
35
|
+
case change.type
|
|
36
|
+
when :added then { op: 'add', path: path, value: change.new_value }
|
|
37
|
+
when :removed then { op: 'remove', path: path }
|
|
38
|
+
when :changed then { op: 'replace', path: path, value: change.new_value }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_class_method :format_op
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module Differ
|
|
5
|
+
module Similarity
|
|
6
|
+
def self.call(old_val, new_val, ignore: [], array_key: nil)
|
|
7
|
+
total = count_fields(old_val, new_val, ignore: ignore, array_key: array_key)
|
|
8
|
+
return 1.0 if total.zero?
|
|
9
|
+
|
|
10
|
+
changes = Comparator.call(old_val, new_val, ignore: ignore, array_key: array_key)
|
|
11
|
+
changed = changes.length
|
|
12
|
+
(total - changed).to_f / total
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.count_fields(old_val, new_val, ignore: [], array_key: nil, path: '')
|
|
16
|
+
case [old_val, new_val]
|
|
17
|
+
in [Hash, Hash] then count_hash_fields(old_val, new_val, ignore, array_key, path)
|
|
18
|
+
in [Array, Array] then count_array_fields(old_val, new_val, ignore, array_key, path)
|
|
19
|
+
else 1
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.count_hash_fields(old_hash, new_hash, ignore, array_key, path)
|
|
24
|
+
opts = { ignore: ignore, array_key: array_key }
|
|
25
|
+
(old_hash.keys + new_hash.keys).uniq.sum do |key|
|
|
26
|
+
full_path = path.empty? ? key.to_s : "#{path}.#{key}"
|
|
27
|
+
next 0 if ignore.any? { |p| p.to_s == full_path }
|
|
28
|
+
|
|
29
|
+
count_hash_key(old_hash, new_hash, key, full_path, opts)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.count_hash_key(old_hash, new_hash, key, full_path, opts)
|
|
34
|
+
if old_hash.key?(key) && new_hash.key?(key)
|
|
35
|
+
count_fields(old_hash[key], new_hash[key],
|
|
36
|
+
ignore: opts[:ignore], array_key: opts[:array_key], path: full_path)
|
|
37
|
+
else
|
|
38
|
+
1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.count_array_fields(old_arr, new_arr, ignore, array_key, path)
|
|
43
|
+
if array_key && old_arr.first.is_a?(Hash)
|
|
44
|
+
count_keyed_array(old_arr, new_arr, ignore, array_key, path)
|
|
45
|
+
else
|
|
46
|
+
count_indexed_array(old_arr, new_arr, ignore, array_key, path)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.count_indexed_array(old_arr, new_arr, ignore, array_key, path)
|
|
51
|
+
[old_arr.length, new_arr.length].max.times.sum do |idx|
|
|
52
|
+
full_path = "#{path}.#{idx}"
|
|
53
|
+
next 0 if ignore.any? { |p| p.to_s == full_path }
|
|
54
|
+
next 1 if idx >= old_arr.length || idx >= new_arr.length
|
|
55
|
+
|
|
56
|
+
count_fields(old_arr[idx], new_arr[idx], ignore: ignore, array_key: array_key, path: full_path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.count_keyed_array(old_arr, new_arr, ignore, array_key, path)
|
|
61
|
+
old_map = old_arr.to_h { |item| [item[array_key], item] }
|
|
62
|
+
new_map = new_arr.to_h { |item| [item[array_key], item] }
|
|
63
|
+
count_keyed_array_fields(old_map, new_map, ignore, array_key, path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.count_keyed_array_fields(old_map, new_map, ignore, array_key, path)
|
|
67
|
+
(old_map.keys + new_map.keys).uniq.sum do |key_val|
|
|
68
|
+
full_path = "#{path}.#{key_val}"
|
|
69
|
+
next 1 unless old_map.key?(key_val) && new_map.key?(key_val)
|
|
70
|
+
|
|
71
|
+
count_fields(old_map[key_val], new_map[key_val], ignore: ignore, array_key: array_key, path: full_path)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private_class_method :count_fields, :count_hash_fields, :count_hash_key,
|
|
76
|
+
:count_array_fields, :count_indexed_array, :count_keyed_array,
|
|
77
|
+
:count_keyed_array_fields
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -4,14 +4,20 @@ require_relative 'differ/version'
|
|
|
4
4
|
require_relative 'differ/change'
|
|
5
5
|
require_relative 'differ/changeset'
|
|
6
6
|
require_relative 'differ/comparator'
|
|
7
|
+
require_relative 'differ/formatters'
|
|
8
|
+
require_relative 'differ/similarity'
|
|
7
9
|
|
|
8
10
|
module Philiprehberger
|
|
9
11
|
module Differ
|
|
10
12
|
class Error < StandardError; end
|
|
11
13
|
|
|
12
|
-
def self.diff(old_val, new_val, ignore: [])
|
|
13
|
-
changes = Comparator.call(old_val, new_val, ignore: ignore)
|
|
14
|
+
def self.diff(old_val, new_val, ignore: [], array_key: nil)
|
|
15
|
+
changes = Comparator.call(old_val, new_val, ignore: ignore, array_key: array_key)
|
|
14
16
|
Changeset.new(changes)
|
|
15
17
|
end
|
|
18
|
+
|
|
19
|
+
def self.similarity(old_val, new_val, ignore: [], array_key: nil)
|
|
20
|
+
Similarity.call(old_val, new_val, ignore: ignore, array_key: array_key)
|
|
21
|
+
end
|
|
16
22
|
end
|
|
17
23
|
end
|
metadata
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-differ
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description:
|
|
13
|
+
description: A Ruby library for deep structural diffing of hashes, arrays, and nested
|
|
14
|
+
objects with apply/revert, JSON Patch output, similarity scoring, and configurable
|
|
15
|
+
ignore paths.
|
|
14
16
|
email:
|
|
15
|
-
- philiprehberger
|
|
17
|
+
- me@philiprehberger.com
|
|
16
18
|
executables: []
|
|
17
19
|
extensions: []
|
|
18
20
|
extra_rdoc_files: []
|
|
@@ -24,6 +26,8 @@ files:
|
|
|
24
26
|
- lib/philiprehberger/differ/change.rb
|
|
25
27
|
- lib/philiprehberger/differ/changeset.rb
|
|
26
28
|
- lib/philiprehberger/differ/comparator.rb
|
|
29
|
+
- lib/philiprehberger/differ/formatters.rb
|
|
30
|
+
- lib/philiprehberger/differ/similarity.rb
|
|
27
31
|
- lib/philiprehberger/differ/version.rb
|
|
28
32
|
homepage: https://github.com/philiprehberger/rb-differ
|
|
29
33
|
licenses:
|