json-merge 1.1.2 → 7.0.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
- checksums.yaml.gz.sig +0 -0
- data/lib/json/merge/version.rb +3 -4
- data/lib/json/merge.rb +396 -114
- data/lib/json-merge.rb +1 -4
- data.tar.gz.sig +0 -0
- metadata +28 -256
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -149
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -227
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/README.md +0 -1036
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
- data/lib/json/merge/conflict_resolver.rb +0 -336
- data/lib/json/merge/debug_logger.rb +0 -41
- data/lib/json/merge/emitter.rb +0 -163
- data/lib/json/merge/file_analysis.rb +0 -190
- data/lib/json/merge/merge_result.rb +0 -136
- data/lib/json/merge/node_wrapper.rb +0 -307
- data/lib/json/merge/object_match_refiner.rb +0 -339
- data/lib/json/merge/smart_merger.rb +0 -150
- data/sig/json/merge.rbs +0 -201
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Json
|
|
4
|
-
module Merge
|
|
5
|
-
# Match refiner for JSON objects and array elements that didn't match by exact signature.
|
|
6
|
-
#
|
|
7
|
-
# This refiner uses fuzzy matching to pair JSON nodes that have:
|
|
8
|
-
# - Similar key names in objects (e.g., `databaseUrl` vs `database_url`)
|
|
9
|
-
# - Array elements with similar structure or content
|
|
10
|
-
# - Objects with overlapping keys but different values
|
|
11
|
-
#
|
|
12
|
-
# The matching algorithm considers:
|
|
13
|
-
# - Key name similarity for object pairs (Levenshtein distance)
|
|
14
|
-
# - Value type and content similarity
|
|
15
|
-
# - Structural overlap for nested objects
|
|
16
|
-
#
|
|
17
|
-
# @example Basic usage
|
|
18
|
-
# refiner = ObjectMatchRefiner.new(threshold: 0.6)
|
|
19
|
-
# matches = refiner.call(template_nodes, dest_nodes)
|
|
20
|
-
#
|
|
21
|
-
# @example With custom weights
|
|
22
|
-
# refiner = ObjectMatchRefiner.new(
|
|
23
|
-
# threshold: 0.5,
|
|
24
|
-
# key_weight: 0.6,
|
|
25
|
-
# value_weight: 0.4
|
|
26
|
-
# )
|
|
27
|
-
#
|
|
28
|
-
# @see Ast::Merge::MatchRefinerBase
|
|
29
|
-
class ObjectMatchRefiner < Ast::Merge::MatchRefinerBase
|
|
30
|
-
# Default weight for key similarity
|
|
31
|
-
DEFAULT_KEY_WEIGHT = 0.7
|
|
32
|
-
|
|
33
|
-
# Default weight for value similarity
|
|
34
|
-
DEFAULT_VALUE_WEIGHT = 0.3
|
|
35
|
-
|
|
36
|
-
# @return [Float] Weight for key similarity (0.0-1.0)
|
|
37
|
-
attr_reader :key_weight
|
|
38
|
-
|
|
39
|
-
# @return [Float] Weight for value similarity (0.0-1.0)
|
|
40
|
-
attr_reader :value_weight
|
|
41
|
-
|
|
42
|
-
# Initialize an object match refiner.
|
|
43
|
-
#
|
|
44
|
-
# @param threshold [Float] Minimum score to accept a match (default: 0.5)
|
|
45
|
-
# @param key_weight [Float] Weight for key similarity (default: 0.7)
|
|
46
|
-
# @param value_weight [Float] Weight for value similarity (default: 0.3)
|
|
47
|
-
def initialize(threshold: DEFAULT_THRESHOLD, key_weight: DEFAULT_KEY_WEIGHT, value_weight: DEFAULT_VALUE_WEIGHT, **options)
|
|
48
|
-
super(threshold: threshold, **options)
|
|
49
|
-
@key_weight = key_weight
|
|
50
|
-
@value_weight = value_weight
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Find matches between unmatched JSON nodes.
|
|
54
|
-
#
|
|
55
|
-
# Handles both object key-value pairs and array elements.
|
|
56
|
-
#
|
|
57
|
-
# @param template_nodes [Array] Unmatched nodes from template
|
|
58
|
-
# @param dest_nodes [Array] Unmatched nodes from destination
|
|
59
|
-
# @param context [Hash] Additional context
|
|
60
|
-
# @return [Array<MatchResult>] Array of node matches
|
|
61
|
-
def call(template_nodes, dest_nodes, context = {})
|
|
62
|
-
# Match object pairs (key-value entries)
|
|
63
|
-
pair_matches = match_pairs(template_nodes, dest_nodes)
|
|
64
|
-
|
|
65
|
-
# Match array elements (objects within arrays)
|
|
66
|
-
array_matches = match_array_objects(template_nodes, dest_nodes)
|
|
67
|
-
|
|
68
|
-
pair_matches + array_matches
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
# Match key-value pairs from JSON objects.
|
|
74
|
-
#
|
|
75
|
-
# @param template_nodes [Array] Template nodes
|
|
76
|
-
# @param dest_nodes [Array] Destination nodes
|
|
77
|
-
# @return [Array<MatchResult>]
|
|
78
|
-
def match_pairs(template_nodes, dest_nodes)
|
|
79
|
-
template_pairs = template_nodes.select { |n| pair_node?(n) }
|
|
80
|
-
dest_pairs = dest_nodes.select { |n| pair_node?(n) }
|
|
81
|
-
|
|
82
|
-
return [] if template_pairs.empty? || dest_pairs.empty?
|
|
83
|
-
|
|
84
|
-
greedy_match(template_pairs, dest_pairs) do |t_node, d_node|
|
|
85
|
-
compute_pair_similarity(t_node, d_node)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Match object elements from JSON arrays.
|
|
90
|
-
#
|
|
91
|
-
# @param template_nodes [Array] Template nodes
|
|
92
|
-
# @param dest_nodes [Array] Destination nodes
|
|
93
|
-
# @return [Array<MatchResult>]
|
|
94
|
-
def match_array_objects(template_nodes, dest_nodes)
|
|
95
|
-
template_objects = template_nodes.select { |n| object_node?(n) && !pair_node?(n) }
|
|
96
|
-
dest_objects = dest_nodes.select { |n| object_node?(n) && !pair_node?(n) }
|
|
97
|
-
|
|
98
|
-
return [] if template_objects.empty? || dest_objects.empty?
|
|
99
|
-
|
|
100
|
-
greedy_match(template_objects, dest_objects) do |t_node, d_node|
|
|
101
|
-
compute_object_similarity(t_node, d_node)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Check if a node is a key-value pair.
|
|
106
|
-
#
|
|
107
|
-
# @param node [Object] Node to check
|
|
108
|
-
# @return [Boolean]
|
|
109
|
-
def pair_node?(node)
|
|
110
|
-
return false unless node.respond_to?(:pair?)
|
|
111
|
-
|
|
112
|
-
node.pair?
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Check if a node is a JSON object.
|
|
116
|
-
#
|
|
117
|
-
# @param node [Object] Node to check
|
|
118
|
-
# @return [Boolean]
|
|
119
|
-
def object_node?(node)
|
|
120
|
-
return false unless node.respond_to?(:object?)
|
|
121
|
-
|
|
122
|
-
node.object?
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Compute similarity score between two key-value pairs.
|
|
126
|
-
#
|
|
127
|
-
# @param t_pair [NodeWrapper] Template pair
|
|
128
|
-
# @param d_pair [NodeWrapper] Destination pair
|
|
129
|
-
# @return [Float] Similarity score (0.0-1.0)
|
|
130
|
-
def compute_pair_similarity(t_pair, d_pair)
|
|
131
|
-
t_key = t_pair.key_name
|
|
132
|
-
d_key = d_pair.key_name
|
|
133
|
-
|
|
134
|
-
return 0.0 unless t_key && d_key
|
|
135
|
-
|
|
136
|
-
key_score = key_similarity(t_key, d_key)
|
|
137
|
-
value_score = value_similarity(t_pair.value_node, d_pair.value_node)
|
|
138
|
-
|
|
139
|
-
(key_score * key_weight) + (value_score * value_weight)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Compute similarity score between two JSON objects.
|
|
143
|
-
#
|
|
144
|
-
# @param t_obj [NodeWrapper] Template object
|
|
145
|
-
# @param d_obj [NodeWrapper] Destination object
|
|
146
|
-
# @return [Float] Similarity score (0.0-1.0)
|
|
147
|
-
def compute_object_similarity(t_obj, d_obj)
|
|
148
|
-
t_keys = extract_keys(t_obj)
|
|
149
|
-
d_keys = extract_keys(d_obj)
|
|
150
|
-
|
|
151
|
-
return 1.0 if t_keys.empty? && d_keys.empty?
|
|
152
|
-
return 0.0 if t_keys.empty? || d_keys.empty?
|
|
153
|
-
|
|
154
|
-
# Compute key overlap
|
|
155
|
-
common_keys = (t_keys & d_keys).size
|
|
156
|
-
total_keys = (t_keys | d_keys).size
|
|
157
|
-
key_overlap = common_keys.to_f / total_keys
|
|
158
|
-
|
|
159
|
-
# Compute fuzzy key similarity for non-exact matches
|
|
160
|
-
fuzzy_score = compute_fuzzy_key_matches(t_keys - d_keys, d_keys - t_keys)
|
|
161
|
-
|
|
162
|
-
# Combine exact overlap and fuzzy matching
|
|
163
|
-
(key_overlap * 0.7) + (fuzzy_score * 0.3)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Compute fuzzy matches between two sets of keys.
|
|
167
|
-
#
|
|
168
|
-
# @param keys1 [Array<String>] First set of keys
|
|
169
|
-
# @param keys2 [Array<String>] Second set of keys
|
|
170
|
-
# @return [Float] Fuzzy match score (0.0-1.0)
|
|
171
|
-
def compute_fuzzy_key_matches(keys1, keys2)
|
|
172
|
-
return 1.0 if keys1.empty? && keys2.empty?
|
|
173
|
-
return 0.0 if keys1.empty? || keys2.empty?
|
|
174
|
-
|
|
175
|
-
total_similarity = 0.0
|
|
176
|
-
keys1.each do |k1|
|
|
177
|
-
best_match = keys2.map { |k2| key_similarity(k1, k2) }.max || 0.0
|
|
178
|
-
total_similarity += best_match
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
total_similarity / keys1.size
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Extract keys from a JSON object.
|
|
185
|
-
#
|
|
186
|
-
# @param obj [NodeWrapper] Object node
|
|
187
|
-
# @return [Array<String>] Keys
|
|
188
|
-
def extract_keys(obj)
|
|
189
|
-
return [] unless obj.respond_to?(:pairs)
|
|
190
|
-
|
|
191
|
-
obj.pairs.map(&:key_name).compact
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Compute similarity between two keys.
|
|
195
|
-
#
|
|
196
|
-
# @param key1 [String] First key
|
|
197
|
-
# @param key2 [String] Second key
|
|
198
|
-
# @return [Float] Key similarity (0.0-1.0)
|
|
199
|
-
def key_similarity(key1, key2)
|
|
200
|
-
return 1.0 if key1 == key2
|
|
201
|
-
|
|
202
|
-
str1 = normalize_key(key1.to_s)
|
|
203
|
-
str2 = normalize_key(key2.to_s)
|
|
204
|
-
|
|
205
|
-
string_similarity(str1, str2)
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Normalize a key for comparison.
|
|
209
|
-
# Converts to lowercase and normalizes common naming conventions.
|
|
210
|
-
#
|
|
211
|
-
# @param key [String] Key to normalize
|
|
212
|
-
# @return [String] Normalized key
|
|
213
|
-
def normalize_key(key)
|
|
214
|
-
# Convert camelCase to snake_case first, then normalize
|
|
215
|
-
key.gsub(/([A-Z])/) { "_#{$1.downcase}" }
|
|
216
|
-
.downcase
|
|
217
|
-
.gsub(/[-_]/, "")
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Compute similarity between two values.
|
|
221
|
-
#
|
|
222
|
-
# @param t_value [NodeWrapper, nil] Template value
|
|
223
|
-
# @param d_value [NodeWrapper, nil] Destination value
|
|
224
|
-
# @return [Float] Value similarity (0.0-1.0)
|
|
225
|
-
def value_similarity(t_value, d_value)
|
|
226
|
-
return 0.5 unless t_value && d_value
|
|
227
|
-
|
|
228
|
-
# Check if they're the same type
|
|
229
|
-
return 0.0 unless same_value_type?(t_value, d_value)
|
|
230
|
-
|
|
231
|
-
if t_value.string?
|
|
232
|
-
# Compare string values
|
|
233
|
-
t_text = extract_string_value(t_value)
|
|
234
|
-
d_text = extract_string_value(d_value)
|
|
235
|
-
string_similarity(t_text, d_text)
|
|
236
|
-
elsif t_value.object?
|
|
237
|
-
# Compare object structures
|
|
238
|
-
compute_object_similarity(t_value, d_value)
|
|
239
|
-
elsif t_value.array?
|
|
240
|
-
# Compare array lengths
|
|
241
|
-
array_similarity(t_value, d_value)
|
|
242
|
-
else
|
|
243
|
-
# Same type, consider similar
|
|
244
|
-
0.5
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Check if two values have the same JSON type.
|
|
249
|
-
#
|
|
250
|
-
# @param val1 [NodeWrapper] First value
|
|
251
|
-
# @param val2 [NodeWrapper] Second value
|
|
252
|
-
# @return [Boolean]
|
|
253
|
-
def same_value_type?(val1, val2)
|
|
254
|
-
val1.type == val2.type
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Extract string content from a string node.
|
|
258
|
-
#
|
|
259
|
-
# @param node [NodeWrapper] String node
|
|
260
|
-
# @return [String]
|
|
261
|
-
def extract_string_value(node)
|
|
262
|
-
# String nodes include quotes, remove them
|
|
263
|
-
text = node.respond_to?(:text) ? node.text : ""
|
|
264
|
-
text.gsub(/\A"|"\z/, "")
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
# Compute similarity between two arrays.
|
|
268
|
-
#
|
|
269
|
-
# @param arr1 [NodeWrapper] First array
|
|
270
|
-
# @param arr2 [NodeWrapper] Second array
|
|
271
|
-
# @return [Float] Array similarity (0.0-1.0)
|
|
272
|
-
def array_similarity(arr1, arr2)
|
|
273
|
-
len1 = arr1.respond_to?(:elements) ? arr1.elements.size : 0
|
|
274
|
-
len2 = arr2.respond_to?(:elements) ? arr2.elements.size : 0
|
|
275
|
-
|
|
276
|
-
return 1.0 if len1 == 0 && len2 == 0
|
|
277
|
-
return 0.0 if len1 == 0 || len2 == 0
|
|
278
|
-
|
|
279
|
-
[len1, len2].min.to_f / [len1, len2].max
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# Compute string similarity using Levenshtein distance.
|
|
283
|
-
#
|
|
284
|
-
# @param str1 [String] First string
|
|
285
|
-
# @param str2 [String] Second string
|
|
286
|
-
# @return [Float] Similarity score (0.0-1.0)
|
|
287
|
-
def string_similarity(str1, str2)
|
|
288
|
-
return 1.0 if str1 == str2
|
|
289
|
-
return 0.0 if str1.to_s.empty? || str2.to_s.empty?
|
|
290
|
-
|
|
291
|
-
distance = levenshtein_distance(str1.to_s, str2.to_s)
|
|
292
|
-
max_len = [str1.to_s.length, str2.to_s.length].max
|
|
293
|
-
|
|
294
|
-
1.0 - (distance.to_f / max_len)
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
# Compute Levenshtein distance between two strings.
|
|
298
|
-
#
|
|
299
|
-
# Uses Wagner-Fischer algorithm with O(min(m,n)) space.
|
|
300
|
-
#
|
|
301
|
-
# @param str1 [String] First string
|
|
302
|
-
# @param str2 [String] Second string
|
|
303
|
-
# @return [Integer] Edit distance
|
|
304
|
-
def levenshtein_distance(str1, str2)
|
|
305
|
-
return str2.length if str1.empty?
|
|
306
|
-
return str1.length if str2.empty?
|
|
307
|
-
|
|
308
|
-
# Ensure str1 is the shorter string for space optimization
|
|
309
|
-
if str1.length > str2.length
|
|
310
|
-
str1, str2 = str2, str1
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
m = str1.length
|
|
314
|
-
n = str2.length
|
|
315
|
-
|
|
316
|
-
# Use two rows instead of full matrix
|
|
317
|
-
prev_row = (0..m).to_a
|
|
318
|
-
curr_row = Array.new(m + 1, 0)
|
|
319
|
-
|
|
320
|
-
(1..n).each do |j|
|
|
321
|
-
curr_row[0] = j
|
|
322
|
-
|
|
323
|
-
(1..m).each do |i|
|
|
324
|
-
cost = (str1[i - 1] == str2[j - 1]) ? 0 : 1
|
|
325
|
-
curr_row[i] = [
|
|
326
|
-
prev_row[i] + 1, # deletion
|
|
327
|
-
curr_row[i - 1] + 1, # insertion
|
|
328
|
-
prev_row[i - 1] + cost, # substitution
|
|
329
|
-
].min
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
prev_row, curr_row = curr_row, prev_row
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
prev_row[m]
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
end
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Json
|
|
4
|
-
module Merge
|
|
5
|
-
# High-level merger for JSON/JSONC content.
|
|
6
|
-
# Orchestrates parsing, analysis, and conflict resolution.
|
|
7
|
-
#
|
|
8
|
-
# @example Basic usage
|
|
9
|
-
# merger = SmartMerger.new(template_content, dest_content)
|
|
10
|
-
# result = merger.merge
|
|
11
|
-
# File.write("merged.json", result.output)
|
|
12
|
-
#
|
|
13
|
-
# @example With options
|
|
14
|
-
# merger = SmartMerger.new(template, dest,
|
|
15
|
-
# preference: :template,
|
|
16
|
-
# add_template_only_nodes: true)
|
|
17
|
-
# result = merger.merge
|
|
18
|
-
#
|
|
19
|
-
# @example Enable fuzzy matching
|
|
20
|
-
# merger = SmartMerger.new(template, dest, match_refiner: ObjectMatchRefiner.new)
|
|
21
|
-
#
|
|
22
|
-
# @example With regions (embedded content)
|
|
23
|
-
# merger = SmartMerger.new(template, dest,
|
|
24
|
-
# regions: [{ detector: SomeDetector.new, merger_class: SomeMerger }])
|
|
25
|
-
class SmartMerger < ::Ast::Merge::SmartMergerBase
|
|
26
|
-
# Creates a new SmartMerger
|
|
27
|
-
#
|
|
28
|
-
# @param template_content [String] Template JSON content
|
|
29
|
-
# @param dest_content [String] Destination JSON content
|
|
30
|
-
# @param signature_generator [Proc, nil] Custom signature generator
|
|
31
|
-
# @param preference [Symbol, Hash] :destination, :template, or per-type Hash
|
|
32
|
-
# @param add_template_only_nodes [Boolean] Whether to add nodes only found in template
|
|
33
|
-
# @param freeze_token [String, nil] Token for freeze block markers
|
|
34
|
-
# @param match_refiner [#call, nil] Match refiner for fuzzy matching
|
|
35
|
-
# @param regions [Array<Hash>, nil] Region configurations for nested merging
|
|
36
|
-
# @param region_placeholder [String, nil] Custom placeholder for regions
|
|
37
|
-
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
38
|
-
# for per-node-type merge preferences
|
|
39
|
-
# @param options [Hash] Additional options for forward compatibility
|
|
40
|
-
def initialize(
|
|
41
|
-
template_content,
|
|
42
|
-
dest_content,
|
|
43
|
-
signature_generator: nil,
|
|
44
|
-
preference: :destination,
|
|
45
|
-
add_template_only_nodes: false,
|
|
46
|
-
freeze_token: nil,
|
|
47
|
-
match_refiner: nil,
|
|
48
|
-
regions: nil,
|
|
49
|
-
region_placeholder: nil,
|
|
50
|
-
node_typing: nil,
|
|
51
|
-
**options
|
|
52
|
-
)
|
|
53
|
-
super(
|
|
54
|
-
template_content,
|
|
55
|
-
dest_content,
|
|
56
|
-
signature_generator: signature_generator,
|
|
57
|
-
preference: preference,
|
|
58
|
-
add_template_only_nodes: add_template_only_nodes,
|
|
59
|
-
freeze_token: freeze_token,
|
|
60
|
-
match_refiner: match_refiner,
|
|
61
|
-
regions: regions,
|
|
62
|
-
region_placeholder: region_placeholder,
|
|
63
|
-
node_typing: node_typing,
|
|
64
|
-
**options
|
|
65
|
-
)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Backward-compatible options hash
|
|
69
|
-
#
|
|
70
|
-
# @return [Hash] The merge options
|
|
71
|
-
def options
|
|
72
|
-
{
|
|
73
|
-
preference: @preference,
|
|
74
|
-
add_template_only_nodes: @add_template_only_nodes,
|
|
75
|
-
match_refiner: @match_refiner,
|
|
76
|
-
}
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
protected
|
|
80
|
-
|
|
81
|
-
# @return [Class] The analysis class for JSON files
|
|
82
|
-
def analysis_class
|
|
83
|
-
FileAnalysis
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# @return [String] The default freeze token (not used for JSON)
|
|
87
|
-
def default_freeze_token
|
|
88
|
-
"json-merge"
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# @return [Class] The resolver class for JSON files
|
|
92
|
-
def resolver_class
|
|
93
|
-
ConflictResolver
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# @return [Class] The result class for JSON files
|
|
97
|
-
def result_class
|
|
98
|
-
MergeResult
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Perform the JSON-specific merge
|
|
102
|
-
#
|
|
103
|
-
# @return [MergeResult] The merge result
|
|
104
|
-
def perform_merge
|
|
105
|
-
@resolver.resolve(@result)
|
|
106
|
-
|
|
107
|
-
DebugLogger.debug("Merge complete", {
|
|
108
|
-
lines: @result.line_count,
|
|
109
|
-
decisions: @result.statistics,
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
@result
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Build the resolver with JSON-specific signature
|
|
116
|
-
def build_resolver
|
|
117
|
-
ConflictResolver.new(
|
|
118
|
-
@template_analysis,
|
|
119
|
-
@dest_analysis,
|
|
120
|
-
preference: @preference,
|
|
121
|
-
add_template_only_nodes: @add_template_only_nodes,
|
|
122
|
-
match_refiner: @match_refiner,
|
|
123
|
-
node_typing: @node_typing,
|
|
124
|
-
)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Build the result (no-arg constructor for JSON)
|
|
128
|
-
def build_result
|
|
129
|
-
MergeResult.new
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# @return [Class] The template parse error class for JSON
|
|
133
|
-
def template_parse_error_class
|
|
134
|
-
TemplateParseError
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# @return [Class] The destination parse error class for JSON
|
|
138
|
-
def destination_parse_error_class
|
|
139
|
-
DestinationParseError
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
private
|
|
143
|
-
|
|
144
|
-
# JSON FileAnalysis only accepts signature_generator, not freeze_token
|
|
145
|
-
def build_full_analysis_options
|
|
146
|
-
{signature_generator: @signature_generator}
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
data/sig/json/merge.rbs
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
# Type signatures for json-merge gem
|
|
2
|
-
# Smart merge for JSON files using tree-sitter AST analysis
|
|
3
|
-
|
|
4
|
-
module Json
|
|
5
|
-
module Merge
|
|
6
|
-
VERSION: String
|
|
7
|
-
|
|
8
|
-
# Base error class for json-merge errors
|
|
9
|
-
class Error < StandardError
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Error raised when template JSON file has syntax errors
|
|
13
|
-
class TemplateParseError < Error
|
|
14
|
-
attr_reader errors: Array[untyped]
|
|
15
|
-
attr_reader content: String?
|
|
16
|
-
|
|
17
|
-
def initialize: (?String? message, ?errors: Array[untyped], ?content: String?) -> void
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Error raised when destination JSON file has syntax errors
|
|
21
|
-
class DestinationParseError < Error
|
|
22
|
-
attr_reader errors: Array[untyped]
|
|
23
|
-
attr_reader content: String?
|
|
24
|
-
|
|
25
|
-
def initialize: (?String? message, ?errors: Array[untyped], ?content: String?) -> void
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Debug logging utility for Json::Merge
|
|
29
|
-
module DebugLogger
|
|
30
|
-
extend Ast::Merge::DebugLogger
|
|
31
|
-
|
|
32
|
-
def self.env_var_name: () -> String
|
|
33
|
-
def self.env_var_name=: (String name) -> String
|
|
34
|
-
def self.log_prefix: () -> String
|
|
35
|
-
def self.log_prefix=: (String prefix) -> String
|
|
36
|
-
def self.enabled?: () -> bool
|
|
37
|
-
def self.debug: (String message, ?Hash[Symbol, untyped] context) -> void
|
|
38
|
-
def self.info: (String message) -> void
|
|
39
|
-
def self.warning: (String message) -> void
|
|
40
|
-
def self.time: [T] (String operation) { () -> T } -> T
|
|
41
|
-
def self.extract_node_info: (untyped node) -> Hash[Symbol, untyped]
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Wrapper for tree-sitter JSON nodes
|
|
45
|
-
class NodeWrapper
|
|
46
|
-
attr_reader node: untyped
|
|
47
|
-
attr_reader start_line: Integer
|
|
48
|
-
attr_reader end_line: Integer
|
|
49
|
-
attr_reader analysis: FileAnalysis?
|
|
50
|
-
|
|
51
|
-
def initialize: (
|
|
52
|
-
untyped node,
|
|
53
|
-
String source,
|
|
54
|
-
?analysis: FileAnalysis?
|
|
55
|
-
) -> void
|
|
56
|
-
|
|
57
|
-
def location: () -> Ast::Merge::FreezeNode::Location
|
|
58
|
-
def signature: () -> Array[untyped]
|
|
59
|
-
def object?: () -> bool
|
|
60
|
-
def array?: () -> bool
|
|
61
|
-
def pair?: () -> bool
|
|
62
|
-
def key: () -> String?
|
|
63
|
-
def value: () -> untyped
|
|
64
|
-
def slice: () -> String?
|
|
65
|
-
def text: () -> String
|
|
66
|
-
def inspect: () -> String
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# File analysis for JSON files
|
|
70
|
-
class FileAnalysis
|
|
71
|
-
include Ast::Merge::FileAnalysisBase
|
|
72
|
-
|
|
73
|
-
PARSER_SEARCH_PATHS: Array[String]
|
|
74
|
-
|
|
75
|
-
attr_reader source: String
|
|
76
|
-
attr_reader lines: Array[String]
|
|
77
|
-
attr_reader ast: untyped
|
|
78
|
-
attr_reader statements: Array[NodeWrapper]
|
|
79
|
-
attr_reader signature_generator: (^(untyped) -> Array[untyped]?)?
|
|
80
|
-
attr_reader errors: Array[untyped]
|
|
81
|
-
|
|
82
|
-
def self.find_parser_path: () -> String?
|
|
83
|
-
|
|
84
|
-
def initialize: (
|
|
85
|
-
String source,
|
|
86
|
-
?signature_generator: (^(untyped) -> Array[untyped]?)?,
|
|
87
|
-
?parser_path: String?
|
|
88
|
-
) -> void
|
|
89
|
-
|
|
90
|
-
def valid?: () -> bool
|
|
91
|
-
def line_at: (Integer line_num) -> String?
|
|
92
|
-
def normalized_line: (Integer line_num) -> String?
|
|
93
|
-
def signature_at: (Integer index) -> Array[untyped]?
|
|
94
|
-
def generate_signature: (untyped statement) -> Array[untyped]?
|
|
95
|
-
def compute_node_signature: (untyped statement) -> Array[untyped]?
|
|
96
|
-
|
|
97
|
-
private
|
|
98
|
-
|
|
99
|
-
def parse_json: () -> void
|
|
100
|
-
def extract_nodes: (untyped tree_node) -> Array[NodeWrapper]
|
|
101
|
-
def integrate_nodes: () -> Array[NodeWrapper]
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Result of a JSON merge operation
|
|
105
|
-
class MergeResult < Ast::Merge::MergeResult
|
|
106
|
-
DECISION_KEPT_TEMPLATE: Symbol
|
|
107
|
-
DECISION_KEPT_DEST: Symbol
|
|
108
|
-
DECISION_MERGED: Symbol
|
|
109
|
-
DECISION_ADDED: Symbol
|
|
110
|
-
|
|
111
|
-
attr_reader lines: Array[Hash[Symbol, untyped]]
|
|
112
|
-
attr_reader decisions: Array[Hash[Symbol, untyped]]
|
|
113
|
-
attr_reader statistics: Hash[Symbol, Integer]
|
|
114
|
-
|
|
115
|
-
def initialize: (
|
|
116
|
-
?template_analysis: FileAnalysis?,
|
|
117
|
-
?dest_analysis: FileAnalysis?,
|
|
118
|
-
?conflicts: Array[Hash[Symbol, untyped]],
|
|
119
|
-
?stats: Hash[Symbol, untyped]
|
|
120
|
-
) -> void
|
|
121
|
-
|
|
122
|
-
def add_line: (
|
|
123
|
-
String line,
|
|
124
|
-
decision: Symbol,
|
|
125
|
-
source: Symbol,
|
|
126
|
-
?original_line: Integer?
|
|
127
|
-
) -> void
|
|
128
|
-
|
|
129
|
-
def add_lines: (
|
|
130
|
-
Array[String] lines,
|
|
131
|
-
decision: Symbol,
|
|
132
|
-
source: Symbol,
|
|
133
|
-
?start_line: Integer?
|
|
134
|
-
) -> void
|
|
135
|
-
|
|
136
|
-
def add_blank_line: (?decision: Symbol, ?source: Symbol) -> void
|
|
137
|
-
def to_json: () -> String
|
|
138
|
-
def content: () -> Array[Hash[Symbol, untyped]]
|
|
139
|
-
def content_string: () -> String
|
|
140
|
-
def empty?: () -> bool
|
|
141
|
-
|
|
142
|
-
private
|
|
143
|
-
|
|
144
|
-
def track_statistics: (Symbol decision, Symbol source) -> void
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Smart merger for JSON files
|
|
148
|
-
class SmartMerger
|
|
149
|
-
include Ast::Merge::MergerConfig
|
|
150
|
-
|
|
151
|
-
attr_reader template_analysis: FileAnalysis
|
|
152
|
-
attr_reader dest_analysis: FileAnalysis
|
|
153
|
-
attr_reader signature_match_preference: (Symbol | Hash[Symbol, Symbol])
|
|
154
|
-
attr_reader add_template_only_nodes: bool
|
|
155
|
-
|
|
156
|
-
def initialize: (
|
|
157
|
-
String template_content,
|
|
158
|
-
String dest_content,
|
|
159
|
-
?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
|
|
160
|
-
?add_template_only_nodes: bool,
|
|
161
|
-
?signature_generator: (^(untyped) -> Array[untyped]?)?,
|
|
162
|
-
?node_splitter: Hash[Symbol, untyped]?,
|
|
163
|
-
?parser_path: String?
|
|
164
|
-
) -> void
|
|
165
|
-
|
|
166
|
-
def merge: () -> MergeResult
|
|
167
|
-
|
|
168
|
-
private
|
|
169
|
-
|
|
170
|
-
def perform_merge: () -> MergeResult
|
|
171
|
-
def merge_nodes: (MergeResult result) -> void
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Conflict resolver for JSON merges
|
|
175
|
-
class ConflictResolver
|
|
176
|
-
attr_reader template_analysis: FileAnalysis
|
|
177
|
-
attr_reader dest_analysis: FileAnalysis
|
|
178
|
-
attr_reader signature_match_preference: (Symbol | Hash[Symbol, Symbol])
|
|
179
|
-
attr_reader add_template_only_nodes: bool
|
|
180
|
-
|
|
181
|
-
def initialize: (
|
|
182
|
-
FileAnalysis template_analysis,
|
|
183
|
-
FileAnalysis dest_analysis,
|
|
184
|
-
?signature_match_preference: (Symbol | Hash[Symbol, Symbol]),
|
|
185
|
-
?add_template_only_nodes: bool
|
|
186
|
-
) -> void
|
|
187
|
-
|
|
188
|
-
def resolve: (untyped boundary, MergeResult result) -> void
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
# Emitter for reconstructing JSON output
|
|
192
|
-
class Emitter
|
|
193
|
-
attr_reader result: MergeResult
|
|
194
|
-
attr_reader indent: Integer
|
|
195
|
-
|
|
196
|
-
def initialize: (MergeResult result, ?indent: Integer) -> void
|
|
197
|
-
|
|
198
|
-
def emit: () -> String
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
end
|