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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b373be1315d237effea59c584220c77fb0bd1deb9ba6e238b698e4919e321de
4
- data.tar.gz: 599f9707141498e91952c4d26fabe62cf1eff4ebd466d485e01403119946f1fa
3
+ metadata.gz: 501adf50f677014ce90da4bc0222ca212f6a36d091e6612e1a93a3feab001867
4
+ data.tar.gz: 28e1bf4f0c0f662f7c8f7d19cb22c24271c709f45a22d7646f86857e586764ff
5
5
  SHA512:
6
- metadata.gz: 7b62b0044c3493e371c44bea5bebd02adf183fc437aec5a8e5b63b92a53cc96a33cca0274329c4bacda38cd44042f1afec6a0de93b4f768615383fd4cc3416f7
7
- data.tar.gz: 5338d2c13503d74f963f3bd7e8ff4f8488adf739c7a57bfe2d735ab00190ad3052893c5d017014511c1a2c96c78f380384e274d4a427dfbc2c00da3b7c110a02
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 |
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Differ
5
- VERSION = '0.2.8'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -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.2.8
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-03-31 00:00:00.000000000 Z
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://github.com/philiprehberger/rb-differ
32
+ homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-differ
33
33
  licenses:
34
34
  - MIT
35
35
  metadata:
36
- homepage_uri: https://github.com/philiprehberger/rb-differ
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