json-merge 7.0.0 → 7.1.3
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
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +600 -0
- data/lib/json/merge/comment_tracker.rb +27 -0
- data/lib/json/merge/conflict_resolver.rb +1379 -0
- data/lib/json/merge/debug_logger.rb +41 -0
- data/lib/json/merge/emitter.rb +227 -0
- data/lib/json/merge/file_analysis.rb +355 -0
- data/lib/json/merge/freeze_node.rb +62 -0
- data/lib/json/merge/merge_result.rb +156 -0
- data/lib/json/merge/node_wrapper.rb +258 -0
- data/lib/json/merge/object_match_refiner.rb +338 -0
- data/lib/json/merge/provider.rb +696 -0
- data/lib/json/merge/smart_merger.rb +276 -0
- data/lib/json/merge/source_locator.rb +68 -0
- data/lib/json/merge/three_way_decision.rb +168 -0
- data/lib/json/merge/version.rb +5 -3
- data/lib/json/merge.rb +393 -266
- data/lib/json-merge.rb +7 -1
- data/sig/json/merge.rbs +54 -0
- data.tar.gz.sig +0 -0
- metadata +264 -15
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Merge
|
|
5
|
+
# High-level merger for JSON / JSONC content.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# merger = SmartMerger.new(template_content, dest_content)
|
|
9
|
+
# result = merger.merge
|
|
10
|
+
# File.write("merged.json", result.output)
|
|
11
|
+
#
|
|
12
|
+
# @example With options
|
|
13
|
+
# merger = SmartMerger.new(template, dest,
|
|
14
|
+
# preference: :template,
|
|
15
|
+
# add_template_only_nodes: true)
|
|
16
|
+
# result = merger.merge
|
|
17
|
+
#
|
|
18
|
+
# @example Enable fuzzy matching
|
|
19
|
+
# merger = SmartMerger.new(template, dest, match_refiner: ObjectMatchRefiner.new)
|
|
20
|
+
#
|
|
21
|
+
# @example With regions (embedded content)
|
|
22
|
+
# merger = SmartMerger.new(template, dest,
|
|
23
|
+
# regions: [{ detector: SomeDetector.new, merger_class: SomeMerger }])
|
|
24
|
+
class SmartMerger < ::Ast::Merge::SmartMergerBase
|
|
25
|
+
include ::Ast::Merge::Runtime::RootSessionSupport
|
|
26
|
+
|
|
27
|
+
attr_reader :runtime_session, :corruption_handling, :dialect
|
|
28
|
+
|
|
29
|
+
# Creates a new SmartMerger
|
|
30
|
+
#
|
|
31
|
+
# @param template_content [String] Template JSON content
|
|
32
|
+
# @param dest_content [String] Destination JSON content
|
|
33
|
+
# @param signature_generator [Proc, nil] Custom signature generator
|
|
34
|
+
# @param preference [Symbol, Hash] :destination, :template, or per-type Hash
|
|
35
|
+
# @param add_template_only_nodes [Boolean] Whether to add nodes only found in template
|
|
36
|
+
# @param remove_template_missing_nodes [Boolean] Whether to remove nodes missing from template
|
|
37
|
+
# @param freeze_token [String, nil] Token for freeze block markers
|
|
38
|
+
# @param match_refiner [#call, nil] Match refiner for fuzzy matching
|
|
39
|
+
# @param regions [Array<Hash>, nil] Region configurations for nested merging
|
|
40
|
+
# @param region_placeholder [String, nil] Custom placeholder for regions
|
|
41
|
+
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
42
|
+
# for per-node-type merge preferences
|
|
43
|
+
# @param options [Hash] Additional options for forward compatibility
|
|
44
|
+
def initialize(
|
|
45
|
+
template_content,
|
|
46
|
+
dest_content,
|
|
47
|
+
signature_generator: nil,
|
|
48
|
+
preference: :destination,
|
|
49
|
+
add_template_only_nodes: false,
|
|
50
|
+
remove_template_missing_nodes: false,
|
|
51
|
+
corruption_handling: :heal,
|
|
52
|
+
freeze_token: nil,
|
|
53
|
+
match_refiner: nil,
|
|
54
|
+
regions: nil,
|
|
55
|
+
region_placeholder: nil,
|
|
56
|
+
node_typing: nil,
|
|
57
|
+
merge_arrays: true,
|
|
58
|
+
preserve_atomic_formatting: false,
|
|
59
|
+
dialect: :jsonc,
|
|
60
|
+
**options
|
|
61
|
+
)
|
|
62
|
+
@remove_template_missing_nodes = remove_template_missing_nodes
|
|
63
|
+
@corruption_handling = ::Ast::Merge::Healer.normalize_mode(corruption_handling)
|
|
64
|
+
@merge_arrays = merge_arrays
|
|
65
|
+
@preserve_atomic_formatting = preserve_atomic_formatting
|
|
66
|
+
@dialect = dialect.to_sym
|
|
67
|
+
unless %i[json jsonc json5].include?(@dialect)
|
|
68
|
+
raise ArgumentError, "Unsupported JSON dialect #{dialect.inspect}. Expected json, jsonc, or json5."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
super(
|
|
72
|
+
template_content,
|
|
73
|
+
dest_content,
|
|
74
|
+
signature_generator: signature_generator,
|
|
75
|
+
preference: preference,
|
|
76
|
+
add_template_only_nodes: add_template_only_nodes,
|
|
77
|
+
remove_template_missing_nodes: remove_template_missing_nodes,
|
|
78
|
+
freeze_token: freeze_token,
|
|
79
|
+
match_refiner: match_refiner,
|
|
80
|
+
regions: regions,
|
|
81
|
+
region_placeholder: region_placeholder,
|
|
82
|
+
node_typing: node_typing,
|
|
83
|
+
merge_arrays: merge_arrays,
|
|
84
|
+
preserve_atomic_formatting: preserve_atomic_formatting,
|
|
85
|
+
dialect: @dialect,
|
|
86
|
+
**options
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Backward-compatible options hash
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash] The merge options
|
|
93
|
+
def options
|
|
94
|
+
{
|
|
95
|
+
preference: @preference,
|
|
96
|
+
add_template_only_nodes: @add_template_only_nodes,
|
|
97
|
+
remove_template_missing_nodes: @remove_template_missing_nodes,
|
|
98
|
+
resolution_mode: @resolution_mode,
|
|
99
|
+
unresolved_policy: @unresolved_policy.to_h,
|
|
100
|
+
corruption_handling: @corruption_handling,
|
|
101
|
+
match_refiner: @match_refiner,
|
|
102
|
+
dialect: @dialect
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Perform the merge operation and return the full MergeResult object.
|
|
107
|
+
#
|
|
108
|
+
# @return [MergeResult] The merge result containing merged JSON content and metadata
|
|
109
|
+
def merge_result
|
|
110
|
+
return @merge_result if @merge_result
|
|
111
|
+
|
|
112
|
+
root_operation = start_runtime_session!
|
|
113
|
+
@merge_result = super
|
|
114
|
+
complete_runtime_session!(root_operation, @merge_result)
|
|
115
|
+
@merge_result
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
fail_runtime_session!(root_operation, e)
|
|
118
|
+
raise
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Perform the merge and return detailed runtime-aware debug information.
|
|
122
|
+
#
|
|
123
|
+
# @return [Hash] Hash containing :content, :debug, :runtime, :statistics, and :decisions
|
|
124
|
+
def merge_with_debug
|
|
125
|
+
result_obj = merge_result
|
|
126
|
+
template_analysis_debug = {
|
|
127
|
+
valid: @template_analysis.valid?,
|
|
128
|
+
nodes: @template_analysis.nodes.size,
|
|
129
|
+
freeze_blocks: @template_analysis.freeze_blocks.size
|
|
130
|
+
}
|
|
131
|
+
dest_analysis_debug = {
|
|
132
|
+
valid: @dest_analysis.valid?,
|
|
133
|
+
nodes: @dest_analysis.nodes.size,
|
|
134
|
+
freeze_blocks: @dest_analysis.freeze_blocks.size
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
{
|
|
138
|
+
content: result_obj.to_json,
|
|
139
|
+
debug: {
|
|
140
|
+
template_nodes: template_analysis_debug[:nodes],
|
|
141
|
+
dest_nodes: dest_analysis_debug[:nodes],
|
|
142
|
+
preference: @preference,
|
|
143
|
+
add_template_only_nodes: @add_template_only_nodes,
|
|
144
|
+
remove_template_missing_nodes: @remove_template_missing_nodes,
|
|
145
|
+
corruption_handling: @corruption_handling,
|
|
146
|
+
freeze_token: @freeze_token,
|
|
147
|
+
runtime_operation_count: runtime_session&.operations&.size || 0,
|
|
148
|
+
runtime_diagnostic_count: runtime_session&.diagnostics&.size || 0
|
|
149
|
+
},
|
|
150
|
+
runtime: runtime_session&.to_h,
|
|
151
|
+
statistics: result_obj.statistics,
|
|
152
|
+
decisions: result_obj.decision_summary,
|
|
153
|
+
template_analysis: template_analysis_debug,
|
|
154
|
+
dest_analysis: dest_analysis_debug
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
protected
|
|
159
|
+
|
|
160
|
+
# @return [Class] The analysis class for JSON files
|
|
161
|
+
def analysis_class
|
|
162
|
+
FileAnalysis
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# @return [String] The default freeze token (not used for JSON)
|
|
166
|
+
def default_freeze_token
|
|
167
|
+
'json-merge'
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @return [Class] The resolver class for JSON files
|
|
171
|
+
def resolver_class
|
|
172
|
+
ConflictResolver
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @return [Class] The result class for JSON files
|
|
176
|
+
def result_class
|
|
177
|
+
MergeResult
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Perform the JSON-specific merge
|
|
181
|
+
#
|
|
182
|
+
# @return [MergeResult] The merge result
|
|
183
|
+
def perform_merge
|
|
184
|
+
@resolver.resolve(@result)
|
|
185
|
+
|
|
186
|
+
DebugLogger.debug('Merge complete', {
|
|
187
|
+
lines: @result.line_count,
|
|
188
|
+
decisions: @result.statistics
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
@result
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Build the resolver with JSON-specific signature
|
|
195
|
+
def build_resolver
|
|
196
|
+
ConflictResolver.new(
|
|
197
|
+
@template_analysis,
|
|
198
|
+
@dest_analysis,
|
|
199
|
+
preference: @preference,
|
|
200
|
+
add_template_only_nodes: @add_template_only_nodes,
|
|
201
|
+
remove_template_missing_nodes: @remove_template_missing_nodes,
|
|
202
|
+
resolution_mode: @resolution_mode,
|
|
203
|
+
corruption_handling: @corruption_handling,
|
|
204
|
+
match_refiner: @match_refiner,
|
|
205
|
+
node_typing: @node_typing,
|
|
206
|
+
merge_arrays: @merge_arrays,
|
|
207
|
+
preserve_atomic_formatting: @preserve_atomic_formatting
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Build the result (no-arg constructor for JSON)
|
|
212
|
+
def build_result
|
|
213
|
+
MergeResult.new
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @return [Class] The template parse error class for JSON
|
|
217
|
+
def template_parse_error_class
|
|
218
|
+
TemplateParseError
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @return [Class] The destination parse error class for JSON
|
|
222
|
+
def destination_parse_error_class
|
|
223
|
+
DestinationParseError
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
def start_runtime_session!
|
|
229
|
+
start_runtime_root_session!(
|
|
230
|
+
surface_kind: :json_document,
|
|
231
|
+
declared_language: :json,
|
|
232
|
+
effective_language: :json,
|
|
233
|
+
operation_id: 'json-document-root',
|
|
234
|
+
delegate_name: 'json-runtime',
|
|
235
|
+
policy_context: {
|
|
236
|
+
preference: @preference,
|
|
237
|
+
add_template_only_nodes: @add_template_only_nodes,
|
|
238
|
+
remove_template_missing_nodes: @remove_template_missing_nodes,
|
|
239
|
+
resolution_mode: @resolution_mode,
|
|
240
|
+
unresolved_policy: @unresolved_policy.to_h
|
|
241
|
+
},
|
|
242
|
+
metadata: { merger: self.class.name },
|
|
243
|
+
options: {
|
|
244
|
+
preference: @preference,
|
|
245
|
+
add_template_only_nodes: @add_template_only_nodes,
|
|
246
|
+
remove_template_missing_nodes: @remove_template_missing_nodes,
|
|
247
|
+
resolution_mode: @resolution_mode,
|
|
248
|
+
unresolved_policy: @unresolved_policy.to_h
|
|
249
|
+
},
|
|
250
|
+
language_chain: [:json],
|
|
251
|
+
delegate_metadata: { merger: self.class.name }
|
|
252
|
+
)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def complete_runtime_session!(root_operation, merge_result)
|
|
256
|
+
complete_runtime_root_session!(
|
|
257
|
+
root_operation: root_operation,
|
|
258
|
+
replacement_text: merge_result.to_json,
|
|
259
|
+
unresolved_cases: merge_result.unresolved_cases,
|
|
260
|
+
metadata: {
|
|
261
|
+
stats: merge_result.statistics,
|
|
262
|
+
decisions: merge_result.decision_summary
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def fail_runtime_session!(root_operation, error)
|
|
268
|
+
fail_runtime_root_session!(
|
|
269
|
+
root_operation: root_operation,
|
|
270
|
+
error: error,
|
|
271
|
+
kind: :merge_failed
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Merge
|
|
5
|
+
# Locates JSON object-pair owners that can be copied as whole source lines.
|
|
6
|
+
class SourceLocator
|
|
7
|
+
Range = Data.define(:start_line, :end_line)
|
|
8
|
+
|
|
9
|
+
def initialize(source, dialect:, backend: nil)
|
|
10
|
+
@source = source
|
|
11
|
+
@lines = source.lines
|
|
12
|
+
Json::Merge.register_backend!
|
|
13
|
+
backend_id = backend.to_s.empty? ? Json::Merge::TREE_SITTER_BACKEND.id : backend.to_s
|
|
14
|
+
@analysis = TreeHaver.with_backend(backend_id) do
|
|
15
|
+
FileAnalysis.new(source, dialect: dialect)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pair_range(path)
|
|
20
|
+
pair = pair_for(path)
|
|
21
|
+
return unless pair
|
|
22
|
+
return unless whole_line_owner?(pair)
|
|
23
|
+
|
|
24
|
+
Range.new(start_line: pair.start_line, end_line: pair.end_line)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def pair_for(path)
|
|
30
|
+
current = @analysis.root_object
|
|
31
|
+
pair = nil
|
|
32
|
+
segments = pointer_segments(path)
|
|
33
|
+
until segments.empty?
|
|
34
|
+
segment = segments.shift
|
|
35
|
+
pair = pair_with_key(current, segment)
|
|
36
|
+
return unless pair
|
|
37
|
+
|
|
38
|
+
current = pair.value_node
|
|
39
|
+
end
|
|
40
|
+
pair
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pair_with_key(object, key)
|
|
44
|
+
return unless object&.object?
|
|
45
|
+
|
|
46
|
+
object.pairs.find { |candidate| candidate.key_name == key }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pointer_segments(path)
|
|
50
|
+
return [] if path.to_s.empty?
|
|
51
|
+
|
|
52
|
+
path.to_s.split('/').drop(1).map do |segment|
|
|
53
|
+
segment.gsub('~1', '/').gsub('~0', '~')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def whole_line_owner?(pair)
|
|
58
|
+
start_line = pair.start_line
|
|
59
|
+
end_line = pair.end_line
|
|
60
|
+
return false unless start_line && end_line
|
|
61
|
+
return false unless @lines.fetch(end_line - 1, '').end_with?("\n")
|
|
62
|
+
|
|
63
|
+
start_column = pair.node.start_point.fetch(:column)
|
|
64
|
+
@lines.fetch(start_line - 1, '').slice(0, start_column).to_s.strip.empty?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Merge
|
|
5
|
+
# Computes base-aware JSON-family semantic decisions without rendering.
|
|
6
|
+
# rubocop:disable Metrics/ClassLength -- recursive decisions and classifications form one algorithm
|
|
7
|
+
class ThreeWayDecision
|
|
8
|
+
State = Data.define(:present, :value) do
|
|
9
|
+
def self.present(value)
|
|
10
|
+
new(present: true, value: value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.missing
|
|
14
|
+
@missing ||= new(present: false, value: nil)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
present ? { present: true, value: value } : { present: false }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Result = Data.define(:state, :conflicts, :changes) do
|
|
23
|
+
def present?
|
|
24
|
+
state.present
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def value
|
|
28
|
+
state.value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def conflicted?
|
|
32
|
+
!conflicts.empty?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(base:, ours:, theirs:)
|
|
37
|
+
merge_states(
|
|
38
|
+
State.present(base),
|
|
39
|
+
State.present(ours),
|
|
40
|
+
State.present(theirs),
|
|
41
|
+
path: ''
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def merge_states(base, ours, theirs, path:)
|
|
48
|
+
return success(ours) if ours == theirs
|
|
49
|
+
return success(theirs, change: classify(path, base, ours, theirs)) if base == ours
|
|
50
|
+
return success(ours, change: classify(path, base, ours, theirs)) if base == theirs
|
|
51
|
+
return merge_added_objects(ours, theirs, path: path) if added_object_states?(base, ours, theirs)
|
|
52
|
+
return merge_objects(base, ours, theirs, path: path) if object_states?(base, ours, theirs)
|
|
53
|
+
|
|
54
|
+
conflict(base, ours, theirs, path: path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def merge_objects(base, ours, theirs, path:)
|
|
58
|
+
aggregate = { value: {}, conflicts: [], changes: [] }
|
|
59
|
+
states = [base, ours, theirs]
|
|
60
|
+
ordered_keys(base.value, ours.value, theirs.value).each do |key|
|
|
61
|
+
merge_object_key(aggregate, key, states, path: path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Result.new(
|
|
65
|
+
state: State.present(aggregate[:value]),
|
|
66
|
+
conflicts: aggregate[:conflicts].freeze,
|
|
67
|
+
changes: aggregate[:changes].freeze
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def merge_added_objects(ours, theirs, path:)
|
|
72
|
+
merge_objects(State.present({}), ours, theirs, path: path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def merge_object_key(aggregate, key, states, path:)
|
|
76
|
+
child_states = states.map { |state| state_for(state.value, key) }
|
|
77
|
+
child = merge_states(*child_states, path: child_path(path, key))
|
|
78
|
+
aggregate[:value][key] = child.value if child.present?
|
|
79
|
+
aggregate[:conflicts].concat(child.conflicts)
|
|
80
|
+
aggregate[:changes].concat(child.changes)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def success(state, change: nil)
|
|
84
|
+
Result.new(
|
|
85
|
+
state: state,
|
|
86
|
+
conflicts: [].freeze,
|
|
87
|
+
changes: change ? [change].freeze : [].freeze
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def conflict(base, ours, theirs, path:)
|
|
92
|
+
classification = classify(path, base, ours, theirs)
|
|
93
|
+
selected = ours.present ? ours : theirs
|
|
94
|
+
Result.new(
|
|
95
|
+
state: selected,
|
|
96
|
+
conflicts: [conflict_payload(base, ours, theirs, path, classification)].freeze,
|
|
97
|
+
changes: [classification].freeze
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def conflict_payload(base, ours, theirs, path, classification)
|
|
102
|
+
{
|
|
103
|
+
conflict_id: conflict_id(path),
|
|
104
|
+
category: conflict_category(ours, theirs),
|
|
105
|
+
path: path,
|
|
106
|
+
base: base.to_h,
|
|
107
|
+
ours: ours.to_h,
|
|
108
|
+
theirs: theirs.to_h,
|
|
109
|
+
change_classification: classification
|
|
110
|
+
}.freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def classify(path, base, ours, theirs)
|
|
114
|
+
{
|
|
115
|
+
path: path,
|
|
116
|
+
ours: change_state(base, ours),
|
|
117
|
+
theirs: change_state(base, theirs)
|
|
118
|
+
}.freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def change_state(base, side)
|
|
122
|
+
return :unchanged if base == side
|
|
123
|
+
return :added unless base.present
|
|
124
|
+
return :deleted unless side.present
|
|
125
|
+
|
|
126
|
+
:edited
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def conflict_category(ours, theirs)
|
|
130
|
+
ours.present && theirs.present ? :edit_edit : :delete_edit
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def object_states?(*states)
|
|
134
|
+
states.all? { |state| state.present && state.value.is_a?(Hash) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def added_object_states?(base, ours, theirs)
|
|
138
|
+
!base.present &&
|
|
139
|
+
ours.present && ours.value.is_a?(Hash) &&
|
|
140
|
+
theirs.present && theirs.value.is_a?(Hash)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def state_for(object, key)
|
|
144
|
+
object.key?(key) ? State.present(object.fetch(key)) : State.missing
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def ordered_keys(*objects)
|
|
148
|
+
objects.each_with_object([]) do |object, keys|
|
|
149
|
+
object.each_key { |key| keys << key unless keys.include?(key) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def child_path(path, key)
|
|
154
|
+
"#{path}/#{escape_path_segment(key)}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def escape_path_segment(key)
|
|
158
|
+
key.to_s.gsub('~', '~0').gsub('/', '~1')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def conflict_id(path)
|
|
162
|
+
suffix = path.delete_prefix('/').gsub(/[^a-zA-Z0-9_-]+/, '-')
|
|
163
|
+
"json-conflict-#{suffix.empty? ? 'root' : suffix}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
# rubocop:enable Metrics/ClassLength
|
|
167
|
+
end
|
|
168
|
+
end
|
data/lib/json/merge/version.rb
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Json
|
|
4
4
|
module Merge
|
|
5
|
+
# Version namespace for this gem.
|
|
5
6
|
module Version
|
|
6
|
-
|
|
7
|
+
# Current gem version.
|
|
8
|
+
VERSION = '7.1.3'
|
|
7
9
|
end
|
|
8
|
-
|
|
9
|
-
VERSION = Version::VERSION
|
|
10
|
+
# Current gem version exposed at the traditional constant location.
|
|
11
|
+
VERSION = Version::VERSION # Traditional Constant Location
|
|
10
12
|
end
|
|
11
13
|
end
|