philiprehberger-differ 0.2.8 → 0.3.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/CHANGELOG.md +12 -0
- data/README.md +29 -0
- data/lib/philiprehberger/differ/version.rb +1 -1
- data/lib/philiprehberger/differ.rb +91 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 501adf50f677014ce90da4bc0222ca212f6a36d091e6612e1a93a3feab001867
|
|
4
|
+
data.tar.gz: 28e1bf4f0c0f662f7c8f7d19cb22c24271c709f45a22d7646f86857e586764ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6aaaf8bb2c8717bcba57ee14cad6386113c20c815d270fc135da8991dbfc3fa3796389e4bee2ccf1c79b63b443dc926218f4a55a7eca8748a4005a501a3be0ad
|
|
7
|
+
data.tar.gz: 7a70fb5815b3a8eb3a6150a521a130ea2c795298d97e522a14bba4bc58c18845199ea3c06cc3797914cad6d476357c2fd86ef89627763805226eac9add84e4cf
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-04-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Differ.subset(changeset, path)` for filtering changes by path prefix
|
|
14
|
+
- `Differ.merge(base, theirs, ours)` for three-way merge with conflict detection
|
|
15
|
+
- `Differ.breaking_changes?(changeset)` for detecting removals and type changes
|
|
16
|
+
|
|
17
|
+
## [0.2.9] - 2026-03-31
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Add GitHub issue templates, dependabot config, and PR template
|
|
21
|
+
|
|
10
22
|
## [0.2.8] - 2026-03-31
|
|
11
23
|
|
|
12
24
|
### Changed
|
data/README.md
CHANGED
|
@@ -116,6 +116,29 @@ new_data = { users: [{ id: 2, name: 'Bobby' }, { id: 1, name: 'Alice' }] }
|
|
|
116
116
|
changeset = Philiprehberger::Differ.diff(old_data, new_data, array_key: :id)
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### Subset Filtering
|
|
120
|
+
|
|
121
|
+
Filter a diff to only changes under a specific path:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
changeset = Philiprehberger::Differ.diff(old_data, new_data)
|
|
125
|
+
user_changes = Philiprehberger::Differ.subset(changeset, 'user')
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Three-Way Merge
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
result = Philiprehberger::Differ.merge(base, theirs, ours)
|
|
132
|
+
result[:merged] # => merged hash
|
|
133
|
+
result[:conflicts] # => [{ path:, theirs:, ours: }]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Breaking Change Detection
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
Philiprehberger::Differ.breaking_changes?(changeset) # => true/false
|
|
140
|
+
```
|
|
141
|
+
|
|
119
142
|
## API
|
|
120
143
|
|
|
121
144
|
### `Philiprehberger::Differ.diff(old_val, new_val, ignore: [], array_key: nil)`
|
|
@@ -141,6 +164,12 @@ Returns a Float between 0.0 (completely different) and 1.0 (identical).
|
|
|
141
164
|
| `to_text` | Human-readable text with +/- prefixes |
|
|
142
165
|
| `to_json_patch` | Array of RFC 6902 JSON Patch operations |
|
|
143
166
|
|
|
167
|
+
| Method | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `Differ.subset(changeset, path)` | Filter changes to a specific path prefix |
|
|
170
|
+
| `Differ.merge(base, theirs, ours)` | Three-way merge with conflict detection |
|
|
171
|
+
| `Differ.breaking_changes?(changeset)` | Detect removals and type changes |
|
|
172
|
+
|
|
144
173
|
### `Change`
|
|
145
174
|
|
|
146
175
|
| Attribute | Description |
|
|
@@ -19,5 +19,96 @@ module Philiprehberger
|
|
|
19
19
|
def self.similarity(old_val, new_val, ignore: [], array_key: nil)
|
|
20
20
|
Similarity.call(old_val, new_val, ignore: ignore, array_key: array_key)
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Filter changeset to only changes under a specific path prefix
|
|
24
|
+
#
|
|
25
|
+
# @param changeset [Changeset] the changeset to filter
|
|
26
|
+
# @param path [String] the path prefix to filter by
|
|
27
|
+
# @return [Changeset] new changeset with only matching changes
|
|
28
|
+
def self.subset(changeset, path)
|
|
29
|
+
prefix = path.to_s
|
|
30
|
+
filtered = changeset.changes.select do |change|
|
|
31
|
+
change.path.to_s == prefix || change.path.to_s.start_with?("#{prefix}.")
|
|
32
|
+
end
|
|
33
|
+
Changeset.new(filtered)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Perform a three-way merge with conflict detection
|
|
37
|
+
#
|
|
38
|
+
# @param base [Hash] the common ancestor
|
|
39
|
+
# @param theirs [Hash] their changes
|
|
40
|
+
# @param ours [Hash] our changes
|
|
41
|
+
# @return [Hash] { merged: Hash, conflicts: Array }
|
|
42
|
+
def self.merge(base, theirs, ours)
|
|
43
|
+
their_changes = Comparator.call(base, theirs)
|
|
44
|
+
our_changes = Comparator.call(base, ours)
|
|
45
|
+
|
|
46
|
+
conflicts = detect_conflicts(their_changes, our_changes)
|
|
47
|
+
conflict_paths = conflicts.map { |c| c[:path] }
|
|
48
|
+
|
|
49
|
+
merged = deep_dup(base)
|
|
50
|
+
(their_changes + our_changes).each do |change|
|
|
51
|
+
next if conflict_paths.include?(change.path)
|
|
52
|
+
|
|
53
|
+
apply_merge_change(merged, change)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
{ merged: merged, conflicts: conflicts }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Detect if a changeset contains breaking changes (removals or type changes)
|
|
60
|
+
#
|
|
61
|
+
# @param changeset [Changeset] the changeset to check
|
|
62
|
+
# @return [Boolean] true if breaking changes are detected
|
|
63
|
+
def self.breaking_changes?(changeset)
|
|
64
|
+
changeset.changes.any? do |change|
|
|
65
|
+
change.type == :removed ||
|
|
66
|
+
(change.type == :changed && change.old_value.class != change.new_value.class)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.detect_conflicts(their_changes, our_changes)
|
|
71
|
+
their_paths = their_changes.to_h { |c| [c.path, c] }
|
|
72
|
+
our_paths = our_changes.to_h { |c| [c.path, c] }
|
|
73
|
+
|
|
74
|
+
conflicts = []
|
|
75
|
+
their_paths.each do |path, their_change|
|
|
76
|
+
our_change = our_paths[path]
|
|
77
|
+
next unless our_change
|
|
78
|
+
|
|
79
|
+
if their_change.new_value != our_change.new_value
|
|
80
|
+
conflicts << { path: path, theirs: their_change.new_value, ours: our_change.new_value }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
conflicts
|
|
84
|
+
end
|
|
85
|
+
private_class_method :detect_conflicts
|
|
86
|
+
|
|
87
|
+
def self.apply_merge_change(hash, change)
|
|
88
|
+
keys = change.path.to_s.split('.')
|
|
89
|
+
target = hash
|
|
90
|
+
keys[0..-2].each do |k|
|
|
91
|
+
k = k.to_sym if target.is_a?(Hash) && target.key?(k.to_sym)
|
|
92
|
+
target = target[k]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
key = keys.last
|
|
96
|
+
key = key.to_sym if target.is_a?(Hash) && (target.key?(key.to_sym) || target.keys.any?(Symbol))
|
|
97
|
+
|
|
98
|
+
case change.type
|
|
99
|
+
when :added, :changed then target[key] = change.new_value
|
|
100
|
+
when :removed then target.delete(key)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
private_class_method :apply_merge_change
|
|
104
|
+
|
|
105
|
+
def self.deep_dup(obj)
|
|
106
|
+
case obj
|
|
107
|
+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k] = deep_dup(v) }
|
|
108
|
+
when Array then obj.map { |v| deep_dup(v) }
|
|
109
|
+
else obj
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
private_class_method :deep_dup
|
|
22
113
|
end
|
|
23
114
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-differ
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
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-
|
|
11
|
+
date: 2026-04-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A Ruby library for deep structural diffing of hashes, arrays, and nested
|
|
14
14
|
objects with apply/revert, JSON Patch output, similarity scoring, and configurable
|
|
@@ -29,11 +29,11 @@ files:
|
|
|
29
29
|
- lib/philiprehberger/differ/formatters.rb
|
|
30
30
|
- lib/philiprehberger/differ/similarity.rb
|
|
31
31
|
- lib/philiprehberger/differ/version.rb
|
|
32
|
-
homepage: https://
|
|
32
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-differ
|
|
33
33
|
licenses:
|
|
34
34
|
- MIT
|
|
35
35
|
metadata:
|
|
36
|
-
homepage_uri: https://
|
|
36
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-differ
|
|
37
37
|
source_code_uri: https://github.com/philiprehberger/rb-differ
|
|
38
38
|
changelog_uri: https://github.com/philiprehberger/rb-differ/blob/main/CHANGELOG.md
|
|
39
39
|
bug_tracker_uri: https://github.com/philiprehberger/rb-differ/issues
|