json-merge 1.0.0 → 1.1.1

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.
@@ -13,14 +13,16 @@ module Json
13
13
  #
14
14
  # @param template_analysis [FileAnalysis] Analyzed template file
15
15
  # @param dest_analysis [FileAnalysis] Analyzed destination file
16
- # @param preference [Symbol] Which version to prefer when
16
+ # @param preference [Symbol, Hash] Which version to prefer when
17
17
  # nodes have matching signatures:
18
18
  # - :destination (default) - Keep destination version (customizations)
19
19
  # - :template - Use template version (updates)
20
20
  # @param add_template_only_nodes [Boolean] Whether to add nodes only in template
21
21
  # @param match_refiner [#call, nil] Optional match refiner for fuzzy matching
22
22
  # @param options [Hash] Additional options for forward compatibility
23
- def initialize(template_analysis, dest_analysis, preference: :destination, add_template_only_nodes: false, match_refiner: nil, **options)
23
+ # @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
24
+ # for per-node-type preferences
25
+ def initialize(template_analysis, dest_analysis, preference: :destination, add_template_only_nodes: false, match_refiner: nil, node_typing: nil, **options)
24
26
  super(
25
27
  strategy: :batch,
26
28
  preference: preference,
@@ -30,6 +32,8 @@ module Json
30
32
  match_refiner: match_refiner,
31
33
  **options
32
34
  )
35
+ @node_typing = node_typing
36
+ @emitter = Emitter.new
33
37
  end
34
38
 
35
39
  protected
@@ -42,15 +46,25 @@ module Json
42
46
  template_statements = @template_analysis.statements
43
47
  dest_statements = @dest_analysis.statements
44
48
 
45
- # Merge root-level statements (typically just the root object)
46
- merge_node_lists(
49
+ # Clear emitter for fresh merge
50
+ @emitter.clear
51
+
52
+ # Merge root-level statements via emitter
53
+ merge_node_lists_to_emitter(
47
54
  template_statements,
48
55
  dest_statements,
49
56
  @template_analysis,
50
57
  @dest_analysis,
51
- result,
52
58
  )
53
59
 
60
+ # Transfer emitter output to result
61
+ emitted_content = @emitter.to_s
62
+ unless emitted_content.empty?
63
+ emitted_content.lines.each do |line|
64
+ result.add_line(line.chomp, decision: MergeResult::DECISION_MERGED, source: :merged)
65
+ end
66
+ end
67
+
54
68
  DebugLogger.debug("Conflict resolution complete", {
55
69
  template_statements: template_statements.size,
56
70
  dest_statements: dest_statements.size,
@@ -61,13 +75,12 @@ module Json
61
75
 
62
76
  private
63
77
 
64
- # Recursively merge two lists of nodes (tree-based merge)
78
+ # Recursively merge two lists of nodes, emitting to emitter
65
79
  # @param template_nodes [Array<NodeWrapper>] Template nodes
66
80
  # @param dest_nodes [Array<NodeWrapper>] Destination nodes
67
81
  # @param template_analysis [FileAnalysis] Template analysis for line access
68
82
  # @param dest_analysis [FileAnalysis] Destination analysis for line access
69
- # @param result [MergeResult] Result to populate
70
- def merge_node_lists(template_nodes, dest_nodes, template_analysis, dest_analysis, result)
83
+ def merge_node_lists_to_emitter(template_nodes, dest_nodes, template_analysis, dest_analysis)
71
84
  # Build signature maps for matching
72
85
  template_by_sig = build_signature_map(template_nodes, template_analysis)
73
86
  dest_by_sig = build_signature_map(dest_nodes, dest_analysis)
@@ -84,20 +97,13 @@ module Json
84
97
  dest_nodes.each do |dest_node|
85
98
  dest_sig = dest_analysis.generate_signature(dest_node)
86
99
 
87
- # Freeze blocks from destination are always preserved
88
- if freeze_node?(dest_node)
89
- add_node_to_result(dest_node, result, :destination, MergeResult::DECISION_FREEZE_BLOCK, dest_analysis)
90
- processed_dest_sigs << dest_sig if dest_sig
91
- next
92
- end
93
-
94
100
  # Check for signature match
95
101
  if dest_sig && template_by_sig[dest_sig]
96
102
  template_info = template_by_sig[dest_sig].first
97
103
  template_node = template_info[:node]
98
104
 
99
105
  # Both have this node - merge them (recursively if containers)
100
- merge_matched_nodes(template_node, dest_node, template_analysis, dest_analysis, result)
106
+ merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
101
107
 
102
108
  processed_dest_sigs << dest_sig
103
109
  processed_template_sigs << dest_sig
@@ -107,13 +113,13 @@ module Json
107
113
  template_sig = template_analysis.generate_signature(template_node)
108
114
 
109
115
  # Merge matched nodes
110
- merge_matched_nodes(template_node, dest_node, template_analysis, dest_analysis, result)
116
+ merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
111
117
 
112
118
  processed_dest_sigs << dest_sig if dest_sig
113
119
  processed_template_sigs << template_sig if template_sig
114
120
  else
115
121
  # Destination-only node - always keep
116
- add_node_to_result(dest_node, result, :destination, MergeResult::DECISION_KEPT_DEST, dest_analysis)
122
+ emit_node(dest_node, dest_analysis)
117
123
  processed_dest_sigs << dest_sig if dest_sig
118
124
  end
119
125
  end
@@ -127,35 +133,172 @@ module Json
127
133
  # Skip if already processed
128
134
  next if template_sig && processed_template_sigs.include?(template_sig)
129
135
 
130
- # Skip freeze blocks from template
131
- next if freeze_node?(template_node)
132
-
133
136
  # Add template-only node
134
- add_node_to_result(template_node, result, :template, MergeResult::DECISION_ADDED, template_analysis)
137
+ emit_node(template_node, template_analysis)
135
138
  processed_template_sigs << template_sig if template_sig
136
139
  end
137
140
  end
138
141
 
139
142
  # Merge two matched nodes - for containers, recursively merge children
143
+ # Emits to emitter instead of result
140
144
  # @param template_node [NodeWrapper] Template node
141
145
  # @param dest_node [NodeWrapper] Destination node
142
146
  # @param template_analysis [FileAnalysis] Template analysis
143
147
  # @param dest_analysis [FileAnalysis] Destination analysis
144
- # @param result [MergeResult] Result to populate
145
- def merge_matched_nodes(template_node, dest_node, template_analysis, dest_analysis, result)
148
+ def merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
146
149
  if dest_node.container? && template_node.container?
147
150
  # Both are containers - recursively merge their children
148
- merge_container_nodes(template_node, dest_node, template_analysis, dest_analysis, result)
149
- elsif @preference == :destination
151
+ merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
152
+ elsif dest_node.pair? && template_node.pair?
153
+ # Both are pairs - check if their values are OBJECTS (not arrays) that need recursive merge
154
+ template_value = template_node.value_node
155
+ dest_value = dest_node.value_node
156
+
157
+ # Only recursively merge if BOTH values are objects (not arrays)
158
+ # Arrays are replaced atomically based on preference
159
+ if template_value&.type == :object && dest_value&.type == :object &&
160
+ template_value.container? && dest_value.container?
161
+ # Both values are objects - recursively merge
162
+ @emitter.emit_nested_object_start(dest_node.key_name)
163
+
164
+ # Recursively merge the value objects
165
+ merge_node_lists_to_emitter(
166
+ template_value.mergeable_children,
167
+ dest_value.mergeable_children,
168
+ template_analysis,
169
+ dest_analysis,
170
+ )
171
+
172
+ # Emit closing brace
173
+ @emitter.emit_nested_object_end
174
+ elsif preference_for_pair(template_node, dest_node) == :destination
175
+ # Values are not both objects, or one/both are arrays - use preference and emit
176
+ # Arrays are always replaced, not merged
177
+ emit_node(dest_node, dest_analysis)
178
+ else
179
+ emit_node(template_node, template_analysis)
180
+ end
181
+ elsif preference_for_pair(template_node, dest_node) == :destination
150
182
  # Leaf nodes or mismatched types - use preference
151
- add_node_to_result(dest_node, result, :destination, MergeResult::DECISION_KEPT_DEST, dest_analysis)
183
+ emit_node(dest_node, dest_analysis)
152
184
  else
153
- add_node_to_result(template_node, result, :template, MergeResult::DECISION_KEPT_TEMPLATE, template_analysis)
185
+ emit_node(template_node, template_analysis)
186
+ end
187
+ end
188
+
189
+ # Merge container nodes by emitting via emitter
190
+ # @param template_node [NodeWrapper] Template container node
191
+ # @param dest_node [NodeWrapper] Destination container node
192
+ # @param template_analysis [FileAnalysis] Template analysis
193
+ # @param dest_analysis [FileAnalysis] Destination analysis
194
+ def merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
195
+ # Emit opening bracket
196
+ if dest_node.object?
197
+ @emitter.emit_object_start
198
+ elsif dest_node.array?
199
+ @emitter.emit_array_start
200
+ end
201
+
202
+ # Recursively merge the children
203
+ template_children = template_node.mergeable_children
204
+ dest_children = dest_node.mergeable_children
205
+
206
+ merge_node_lists_to_emitter(
207
+ template_children,
208
+ dest_children,
209
+ template_analysis,
210
+ dest_analysis,
211
+ )
212
+
213
+ # Emit closing bracket
214
+ if dest_node.object?
215
+ @emitter.emit_object_end
216
+ elsif dest_node.array?
217
+ @emitter.emit_array_end
154
218
  end
155
219
  end
156
220
 
157
- # Build a map of refined matches from template node to destination node.
158
- # Uses the match_refiner to find additional pairings for nodes that didn't match by signature.
221
+ def preference_for_pair(template_node, dest_node)
222
+ return @preference unless @preference.is_a?(Hash)
223
+
224
+ typed_template = apply_node_typing(template_node)
225
+ typed_dest = apply_node_typing(dest_node)
226
+
227
+ if Ast::Merge::NodeTyping.typed_node?(typed_template)
228
+ merge_type = Ast::Merge::NodeTyping.merge_type_for(typed_template)
229
+ return @preference.fetch(merge_type) { default_preference } if merge_type
230
+ end
231
+
232
+ if Ast::Merge::NodeTyping.typed_node?(typed_dest)
233
+ merge_type = Ast::Merge::NodeTyping.merge_type_for(typed_dest)
234
+ return @preference.fetch(merge_type) { default_preference } if merge_type
235
+ end
236
+
237
+ default_preference
238
+ end
239
+
240
+ def apply_node_typing(node)
241
+ return node unless @node_typing
242
+ return node unless node
243
+
244
+ Ast::Merge::NodeTyping.process(node, @node_typing)
245
+ end
246
+
247
+ # Emit a single node to the emitter
248
+ # @param node [NodeWrapper] Node to emit
249
+ # @param analysis [FileAnalysis] Analysis for accessing source
250
+ def emit_node(node, analysis)
251
+ # Emit the node content
252
+ if node.pair?
253
+ # Emit as pair
254
+ key = node.key_name
255
+ value_node = node.value_node
256
+
257
+ if value_node
258
+ # Check if value is an object (not array) and needs recursive emission
259
+ if value_node.type == :object && value_node.container?
260
+ # Object value - emit structure recursively
261
+ @emitter.emit_nested_object_start(key)
262
+ # Recursively emit object children
263
+ value_node.mergeable_children.each do |child|
264
+ emit_node(child, analysis)
265
+ end
266
+ @emitter.emit_nested_object_end
267
+ else
268
+ # Leaf value or array - get its text and emit as simple pair
269
+ # Arrays are emitted as raw text (not recursively)
270
+ value_text = if value_node.start_line == value_node.end_line
271
+ value_node.text
272
+ else
273
+ # Multi-line value - get all lines
274
+ lines = []
275
+ (value_node.start_line..value_node.end_line).each do |ln|
276
+ lines << analysis.line_at(ln)
277
+ end
278
+ lines.join("\n")
279
+ end
280
+
281
+ @emitter.emit_pair(key, value_text) if key && value_text
282
+ end
283
+ end
284
+ elsif node.start_line && node.end_line
285
+ # Emit raw content for non-pair nodes
286
+ if node.start_line == node.end_line
287
+ # Single line - add directly
288
+ @emitter.lines << node.text
289
+ else
290
+ # Multi-line - collect and emit
291
+ lines = []
292
+ (node.start_line..node.end_line).each do |ln|
293
+ line = analysis.line_at(ln)
294
+ lines << line if line
295
+ end
296
+ @emitter.emit_raw_lines(lines)
297
+ end
298
+ end
299
+ end
300
+
301
+ # Build a map of refined matches from template node to destination node
159
302
  # @param template_nodes [Array<NodeWrapper>] Template nodes
160
303
  # @param dest_nodes [Array<NodeWrapper>] Destination nodes
161
304
  # @param template_by_sig [Hash] Template signature map
@@ -188,62 +331,6 @@ module Json
188
331
  h[match.template_node] = match.dest_node
189
332
  end
190
333
  end
191
-
192
- # Merge two container nodes by emitting opening, recursively merging children, then closing
193
- # @param template_node [NodeWrapper] Template container node
194
- # @param dest_node [NodeWrapper] Destination container node
195
- # @param template_analysis [FileAnalysis] Template analysis
196
- # @param dest_analysis [FileAnalysis] Destination analysis
197
- # @param result [MergeResult] Result to populate
198
- def merge_container_nodes(template_node, dest_node, template_analysis, dest_analysis, result)
199
- # Use destination's opening line (or template if dest doesn't have one)
200
- opening = dest_node.opening_line || template_node.opening_line
201
- result.add_line(opening, decision: MergeResult::DECISION_MERGED, source: :merged) if opening
202
-
203
- # Recursively merge the children
204
- template_children = template_node.mergeable_children
205
- dest_children = dest_node.mergeable_children
206
-
207
- merge_node_lists(
208
- template_children,
209
- dest_children,
210
- template_analysis,
211
- dest_analysis,
212
- result,
213
- )
214
-
215
- # Use destination's closing line (or template if dest doesn't have one)
216
- closing = dest_node.closing_line || template_node.closing_line
217
- result.add_line(closing, decision: MergeResult::DECISION_MERGED, source: :merged) if closing
218
- end
219
-
220
- # Add a node to the result (non-container or leaf node)
221
- # @param node [NodeWrapper] Node to add
222
- # @param result [MergeResult] Result to populate
223
- # @param source [Symbol] :template or :destination
224
- # @param decision [String] Decision constant
225
- # @param analysis [FileAnalysis] Analysis for line access
226
- def add_node_to_result(node, result, source, decision, analysis)
227
- if freeze_node?(node)
228
- result.add_freeze_block(node)
229
- elsif node.is_a?(NodeWrapper)
230
- add_wrapper_to_result(node, result, source, decision, analysis)
231
- else
232
- DebugLogger.debug("Unknown node type", {node_type: node.class.name})
233
- end
234
- end
235
-
236
- def add_wrapper_to_result(wrapper, result, source, decision, analysis)
237
- return unless wrapper.start_line && wrapper.end_line
238
-
239
- # Add the node content line by line
240
- (wrapper.start_line..wrapper.end_line).each do |line_num|
241
- line = analysis.line_at(line_num)
242
- next unless line
243
-
244
- result.add_line(line.chomp, decision: decision, source: source, original_line: line_num)
245
- end
246
- end
247
334
  end
248
335
  end
249
336
  end
@@ -6,31 +6,38 @@ module Json
6
6
  # This class provides utilities for emitting JSON while maintaining
7
7
  # the original structure, comments, and style choices.
8
8
  #
9
+ # Inherits common emitter functionality from Ast::Merge::EmitterBase.
10
+ #
9
11
  # @example Basic usage
10
12
  # emitter = Emitter.new
11
13
  # emitter.emit_object_start
12
14
  # emitter.emit_pair("key", '"value"')
13
15
  # emitter.emit_object_end
14
- class Emitter
15
- # @return [Array<String>] Output lines
16
- attr_reader :lines
17
-
18
- # @return [Integer] Current indentation level
19
- attr_reader :indent_level
16
+ class Emitter < Ast::Merge::EmitterBase
17
+ # @return [Boolean] Whether next item needs a comma
18
+ attr_reader :needs_comma
20
19
 
21
- # @return [Integer] Spaces per indent level
22
- attr_reader :indent_size
20
+ # Initialize subclass-specific state (comma tracking for JSON)
21
+ def initialize_subclass_state(**options)
22
+ @needs_comma = false
23
+ end
23
24
 
24
- # Initialize a new emitter
25
- #
26
- # @param indent_size [Integer] Number of spaces per indent level
27
- def initialize(indent_size: 2)
28
- @lines = []
29
- @indent_level = 0
30
- @indent_size = indent_size
25
+ # Clear subclass-specific state
26
+ def clear_subclass_state
31
27
  @needs_comma = false
32
28
  end
33
29
 
30
+ # Emit a tracked comment from CommentTracker
31
+ # @param comment [Hash] Comment with :text, :indent, :block
32
+ def emit_tracked_comment(comment)
33
+ indent = " " * (comment[:indent] || 0)
34
+ @lines << if comment[:block]
35
+ "#{indent}/* #{comment[:text]} */"
36
+ else
37
+ "#{indent}// #{comment[:text]}"
38
+ end
39
+ end
40
+
34
41
  # Emit a single-line comment
35
42
  #
36
43
  # @param text [String] Comment text (without //)
@@ -53,36 +60,17 @@ module Json
53
60
  @lines << "#{current_indent}/* #{text} */"
54
61
  end
55
62
 
56
- # Emit leading comments
57
- #
58
- # @param comments [Array<Hash>] Comment hashes from CommentTracker
59
- def emit_leading_comments(comments)
60
- comments.each do |comment|
61
- indent = " " * (comment[:indent] || 0)
62
- @lines << if comment[:block]
63
- "#{indent}/* #{comment[:text]} */"
64
- else
65
- "#{indent}// #{comment[:text]}"
66
- end
67
- end
68
- end
69
-
70
- # Emit a blank line
71
- def emit_blank_line
72
- @lines << ""
73
- end
74
-
75
63
  # Emit object start
76
64
  def emit_object_start
77
65
  add_comma_if_needed
78
66
  @lines << "#{current_indent}{"
79
- @indent_level += 1
67
+ indent
80
68
  @needs_comma = false
81
69
  end
82
70
 
83
71
  # Emit object end
84
72
  def emit_object_end
85
- @indent_level -= 1 if @indent_level > 0
73
+ dedent
86
74
  @lines << "#{current_indent}}"
87
75
  @needs_comma = true
88
76
  end
@@ -97,13 +85,13 @@ module Json
97
85
  else
98
86
  "#{current_indent}["
99
87
  end
100
- @indent_level += 1
88
+ indent
101
89
  @needs_comma = false
102
90
  end
103
91
 
104
92
  # Emit array end
105
93
  def emit_array_end
106
- @indent_level -= 1 if @indent_level > 0
94
+ dedent
107
95
  @lines << "#{current_indent}]"
108
96
  @needs_comma = true
109
97
  end
@@ -133,39 +121,31 @@ module Json
133
121
  @needs_comma = true
134
122
  end
135
123
 
136
- # Emit raw lines (for preserving existing content)
137
- #
138
- # @param raw_lines [Array<String>] Lines to emit as-is
139
- def emit_raw_lines(raw_lines)
140
- raw_lines.each { |line| @lines << line.chomp }
124
+ # Emit a key with opening brace for nested object
125
+ # @param key [String] Key name
126
+ def emit_nested_object_start(key)
127
+ add_comma_if_needed
128
+ @lines << "#{current_indent}\"#{key}\": {"
129
+ indent
130
+ @needs_comma = false
141
131
  end
142
132
 
143
- # Get the output as a single string
144
- #
145
- # @return [String]
146
- def to_json
147
- content = @lines.join("\n")
148
- content += "\n" unless content.empty? || content.end_with?("\n")
149
- content
133
+ # Emit closing brace for nested object
134
+ def emit_nested_object_end
135
+ dedent
136
+ @lines << "#{current_indent}}"
137
+ @needs_comma = true
150
138
  end
151
139
 
152
- # Alias for consistency
140
+ # Get the output as a JSON string
141
+ #
153
142
  # @return [String]
154
- alias_method :to_s, :to_json
155
-
156
- # Clear the output
157
- def clear
158
- @lines = []
159
- @indent_level = 0
160
- @needs_comma = false
143
+ def to_json
144
+ to_s
161
145
  end
162
146
 
163
147
  private
164
148
 
165
- def current_indent
166
- " " * (@indent_level * @indent_size)
167
- end
168
-
169
149
  def add_comma_if_needed
170
150
  return unless @needs_comma && @lines.any?
171
151
 
@@ -74,6 +74,7 @@ module Json
74
74
 
75
75
  # In JSON tree-sitter, pair has key and value children
76
76
  key_node = find_child_by_field("key")
77
+
77
78
  return unless key_node
78
79
 
79
80
  # Key is typically a string, extract its content without quotes using byte positions
@@ -88,6 +89,7 @@ module Json
88
89
  return unless pair?
89
90
 
90
91
  value = find_child_by_field("value")
92
+
91
93
  return unless value
92
94
 
93
95
  NodeWrapper.new(value, lines: @lines, source: @source)
@@ -148,6 +150,19 @@ module Json
148
150
  object? || array?
149
151
  end
150
152
 
153
+ # Check if this is a root-level container (direct child of document)
154
+ # Root-level containers get a generic signature so they always match.
155
+ # @return [Boolean]
156
+ def root_level_container?
157
+ return false unless container?
158
+
159
+ # Check if parent is a document node
160
+ parent_node = @node.parent if @node.respond_to?(:parent)
161
+ return false unless parent_node
162
+
163
+ parent_node.type.to_s == "document"
164
+ end
165
+
151
166
  # Get the opening line for a container node (the line with { or [)
152
167
  # Returns the full line content including any leading whitespace
153
168
  # @return [String, nil]
@@ -226,14 +241,26 @@ module Json
226
241
  child_type = child&.type&.to_s
227
242
  [:document, child_type]
228
243
  when "object"
229
- # Objects identified by their keys
230
- keys = extract_object_keys(node)
231
- [:object, keys.sort]
244
+ # For root-level objects (direct child of document), use a generic signature
245
+ # that always matches so merging happens at the pair level.
246
+ # This is critical for JSON merging - there's typically only one root object/array.
247
+ if root_level_container?
248
+ [:root_object]
249
+ else
250
+ # Nested objects identified by their keys
251
+ keys = extract_object_keys(node)
252
+ [:object, keys.sort]
253
+ end
232
254
  when "array"
233
- # Arrays identified by their length and first few elements
234
- elements_count = 0
235
- node.each { |c| elements_count += 1 unless %w[comment , \[ \]].include?(c.type.to_s) }
236
- [:array, elements_count]
255
+ # For root-level arrays (direct child of document), use a generic signature
256
+ if root_level_container?
257
+ [:root_array]
258
+ else
259
+ # Nested arrays identified by their length and first few elements
260
+ elements_count = 0
261
+ node.each { |c| elements_count += 1 unless %w[comment , \[ \]].include?(c.type.to_s) }
262
+ [:array, elements_count]
263
+ end
237
264
  when "pair"
238
265
  # Pairs identified by their key name
239
266
  key = key_name
@@ -267,6 +294,7 @@ module Json
267
294
  next unless child.type.to_s == "pair"
268
295
 
269
296
  key_node = child.respond_to?(:child_by_field_name) ? child.child_by_field_name("key") : nil
297
+
270
298
  next unless key_node
271
299
 
272
300
  key_text = node_text(key_node)&.gsub(/\A"|"\z/, "")
@@ -120,6 +120,7 @@ module Json
120
120
  preference: @preference,
121
121
  add_template_only_nodes: @add_template_only_nodes,
122
122
  match_refiner: @match_refiner,
123
+ node_typing: @node_typing,
123
124
  )
124
125
  end
125
126
 
@@ -5,7 +5,7 @@ module Json
5
5
  # Version information for Json::Merge
6
6
  module Version
7
7
  # Current version of the json-merge gem
8
- VERSION = "1.0.0"
8
+ VERSION = "1.1.1"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data/lib/json/merge.rb CHANGED
@@ -108,6 +108,18 @@ module Json
108
108
  end
109
109
  end
110
110
 
111
+ # Register with ast-merge's MergeGemRegistry for RSpec dependency tags
112
+ # Only register if MergeGemRegistry is loaded (i.e., in test environment)
113
+ if defined?(Ast::Merge::RSpec::MergeGemRegistry)
114
+ Ast::Merge::RSpec::MergeGemRegistry.register(
115
+ :json_merge,
116
+ require_path: "json/merge",
117
+ merger_class: "Json::Merge::SmartMerger",
118
+ test_source: '{"key": "value"}',
119
+ category: :data,
120
+ )
121
+ end
122
+
111
123
  Json::Merge::Version.class_eval do
112
124
  extend VersionGem::Basic
113
125
  end
data.tar.gz.sig CHANGED
Binary file