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.
data/REEK DELETED
File without changes
data/RUBOCOP.md DELETED
@@ -1,71 +0,0 @@
1
- # RuboCop Usage Guide
2
-
3
- ## Overview
4
-
5
- A tale of two RuboCop plugin gems.
6
-
7
- ### RuboCop Gradual
8
-
9
- This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
-
11
- ### RuboCop LTS
12
-
13
- This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
- RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
-
16
- ## Checking RuboCop Violations
17
-
18
- To check for RuboCop violations in this project, always use:
19
-
20
- ```bash
21
- bundle exec rake rubocop_gradual:check
22
- ```
23
-
24
- **Do not use** the standard RuboCop commands like:
25
- - `bundle exec rubocop`
26
- - `rubocop`
27
-
28
- ## Understanding the Lock File
29
-
30
- The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
-
32
- 1. Prevent new violations while gradually fixing existing ones
33
- 2. Track progress on code style improvements
34
- 3. Ensure CI builds don't fail due to pre-existing violations
35
-
36
- ## Common Commands
37
-
38
- - **Check violations**
39
- - `bundle exec rake rubocop_gradual`
40
- - `bundle exec rake rubocop_gradual:check`
41
- - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
- - `bundle exec rake rubocop_gradual:autocorrect`
43
- - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
- - `bundle exec rake rubocop_gradual:force_update`
45
-
46
- ## Workflow
47
-
48
- 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
- a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
- 2. If there are new violations, either:
51
- - Fix them in your code
52
- - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
- 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
-
55
- ## Never add inline RuboCop disables
56
-
57
- Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
-
59
- - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
- - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
- - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
- - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
-
64
- In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
-
66
- ## Benefits of rubocop_gradual
67
-
68
- - Allows incremental adoption of code style rules
69
- - Prevents CI failures due to pre-existing violations
70
- - Provides a clear record of code style debt
71
- - Enables focused efforts on improving code quality over time
data/SECURITY.md DELETED
@@ -1,21 +0,0 @@
1
- # Security Policy
2
-
3
- ## Supported Versions
4
-
5
- | Version | Supported |
6
- |----------|-----------|
7
- | 1.latest | ✅ |
8
-
9
- ## Security contact information
10
-
11
- To report a security vulnerability, please use the
12
- [Tidelift security contact](https://tidelift.com/security).
13
- Tidelift will coordinate the fix and disclosure.
14
-
15
- ## Additional Support
16
-
17
- If you are interested in support for versions older than the latest release,
18
- please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
- or find other sponsorship links in the [README].
20
-
21
- [README]: README.md
@@ -1,336 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Json
4
- module Merge
5
- # Resolves conflicts between template and destination JSON content
6
- # using structural signatures and configurable preferences.
7
- #
8
- # @example Basic usage
9
- # resolver = ConflictResolver.new(template_analysis, dest_analysis)
10
- # resolver.resolve(result)
11
- class ConflictResolver < ::Ast::Merge::ConflictResolverBase
12
- # Creates a new ConflictResolver
13
- #
14
- # @param template_analysis [FileAnalysis] Analyzed template file
15
- # @param dest_analysis [FileAnalysis] Analyzed destination file
16
- # @param preference [Symbol, Hash] Which version to prefer when
17
- # nodes have matching signatures:
18
- # - :destination (default) - Keep destination version (customizations)
19
- # - :template - Use template version (updates)
20
- # @param add_template_only_nodes [Boolean] Whether to add nodes only in template
21
- # @param match_refiner [#call, nil] Optional match refiner for fuzzy matching
22
- # @param options [Hash] Additional options for forward compatibility
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)
26
- super(
27
- strategy: :batch,
28
- preference: preference,
29
- template_analysis: template_analysis,
30
- dest_analysis: dest_analysis,
31
- add_template_only_nodes: add_template_only_nodes,
32
- match_refiner: match_refiner,
33
- **options
34
- )
35
- @node_typing = node_typing
36
- @emitter = Emitter.new
37
- end
38
-
39
- protected
40
-
41
- # Resolve conflicts and populate the result using tree-based merging
42
- #
43
- # @param result [MergeResult] Result object to populate
44
- def resolve_batch(result)
45
- DebugLogger.time("ConflictResolver#resolve") do
46
- template_statements = @template_analysis.statements
47
- dest_statements = @dest_analysis.statements
48
-
49
- # Clear emitter for fresh merge
50
- @emitter.clear
51
-
52
- # Merge root-level statements via emitter
53
- merge_node_lists_to_emitter(
54
- template_statements,
55
- dest_statements,
56
- @template_analysis,
57
- @dest_analysis,
58
- )
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
-
68
- DebugLogger.debug("Conflict resolution complete", {
69
- template_statements: template_statements.size,
70
- dest_statements: dest_statements.size,
71
- result_lines: result.line_count,
72
- })
73
- end
74
- end
75
-
76
- private
77
-
78
- # Recursively merge two lists of nodes, emitting to emitter
79
- # @param template_nodes [Array<NodeWrapper>] Template nodes
80
- # @param dest_nodes [Array<NodeWrapper>] Destination nodes
81
- # @param template_analysis [FileAnalysis] Template analysis for line access
82
- # @param dest_analysis [FileAnalysis] Destination analysis for line access
83
- def merge_node_lists_to_emitter(template_nodes, dest_nodes, template_analysis, dest_analysis)
84
- # Build signature maps for matching
85
- template_by_sig = build_signature_map(template_nodes, template_analysis)
86
- dest_by_sig = build_signature_map(dest_nodes, dest_analysis)
87
-
88
- # Build refined matches for nodes that don't match by signature
89
- refined_matches = build_refined_matches(template_nodes, dest_nodes, template_by_sig, dest_by_sig)
90
- refined_dest_to_template = refined_matches.invert
91
-
92
- # Track which nodes have been processed
93
- processed_template_sigs = ::Set.new
94
- processed_dest_sigs = ::Set.new
95
-
96
- # First pass: Process destination nodes
97
- dest_nodes.each do |dest_node|
98
- dest_sig = dest_analysis.generate_signature(dest_node)
99
-
100
- # Check for signature match
101
- if dest_sig && template_by_sig[dest_sig]
102
- template_info = template_by_sig[dest_sig].first
103
- template_node = template_info[:node]
104
-
105
- # Both have this node - merge them (recursively if containers)
106
- merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
107
-
108
- processed_dest_sigs << dest_sig
109
- processed_template_sigs << dest_sig
110
- elsif refined_dest_to_template.key?(dest_node)
111
- # Found refined match
112
- template_node = refined_dest_to_template[dest_node]
113
- template_sig = template_analysis.generate_signature(template_node)
114
-
115
- # Merge matched nodes
116
- merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
117
-
118
- processed_dest_sigs << dest_sig if dest_sig
119
- processed_template_sigs << template_sig if template_sig
120
- else
121
- # Destination-only node - always keep
122
- emit_node(dest_node, dest_analysis)
123
- processed_dest_sigs << dest_sig if dest_sig
124
- end
125
- end
126
-
127
- # Second pass: Add template-only nodes if configured
128
- return unless @add_template_only_nodes
129
-
130
- template_nodes.each do |template_node|
131
- template_sig = template_analysis.generate_signature(template_node)
132
-
133
- # Skip if already processed
134
- next if template_sig && processed_template_sigs.include?(template_sig)
135
-
136
- # Add template-only node
137
- emit_node(template_node, template_analysis)
138
- processed_template_sigs << template_sig if template_sig
139
- end
140
- end
141
-
142
- # Merge two matched nodes - for containers, recursively merge children
143
- # Emits to emitter instead of result
144
- # @param template_node [NodeWrapper] Template node
145
- # @param dest_node [NodeWrapper] Destination node
146
- # @param template_analysis [FileAnalysis] Template analysis
147
- # @param dest_analysis [FileAnalysis] Destination analysis
148
- def merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
149
- if dest_node.container? && template_node.container?
150
- # Both are containers - recursively merge their children
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
182
- # Leaf nodes or mismatched types - use preference
183
- emit_node(dest_node, dest_analysis)
184
- else
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
218
- end
219
- end
220
-
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
302
- # @param template_nodes [Array<NodeWrapper>] Template nodes
303
- # @param dest_nodes [Array<NodeWrapper>] Destination nodes
304
- # @param template_by_sig [Hash] Template signature map
305
- # @param dest_by_sig [Hash] Destination signature map
306
- # @return [Hash] Map of template_node => dest_node
307
- def build_refined_matches(template_nodes, dest_nodes, template_by_sig, dest_by_sig)
308
- return {} unless @match_refiner
309
-
310
- # Find unmatched nodes
311
- matched_sigs = template_by_sig.keys & dest_by_sig.keys
312
- unmatched_t_nodes = template_nodes.reject do |n|
313
- sig = @template_analysis.generate_signature(n)
314
- sig && matched_sigs.include?(sig)
315
- end
316
- unmatched_d_nodes = dest_nodes.reject do |n|
317
- sig = @dest_analysis.generate_signature(n)
318
- sig && matched_sigs.include?(sig)
319
- end
320
-
321
- return {} if unmatched_t_nodes.empty? || unmatched_d_nodes.empty?
322
-
323
- # Call the refiner
324
- matches = @match_refiner.call(unmatched_t_nodes, unmatched_d_nodes, {
325
- template_analysis: @template_analysis,
326
- dest_analysis: @dest_analysis,
327
- })
328
-
329
- # Build result map: template node -> dest node
330
- matches.each_with_object({}) do |match, h|
331
- h[match.template_node] = match.dest_node
332
- end
333
- end
334
- end
335
- end
336
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Json
4
- module Merge
5
- # Debug logging utility for Json::Merge.
6
- # Extends the base Ast::Merge::DebugLogger with Json-specific configuration.
7
- #
8
- # @example Enable debug logging
9
- # ENV['JSON_MERGE_DEBUG'] = '1'
10
- # DebugLogger.debug("Processing node", {type: "pair", line: 5})
11
- #
12
- # @example Disable debug logging (default)
13
- # DebugLogger.debug("This won't be printed", {})
14
- module DebugLogger
15
- extend Ast::Merge::DebugLogger
16
-
17
- # Json-specific configuration
18
- self.env_var_name = "JSON_MERGE_DEBUG"
19
- self.log_prefix = "[Json::Merge]"
20
-
21
- class << self
22
- # Override log_node to handle Json-specific node types.
23
- #
24
- # @param node [Object] Node to log information about
25
- # @param label [String] Label for the node
26
- def log_node(node, label: "Node")
27
- return unless enabled?
28
-
29
- info = case node
30
- when Json::Merge::NodeWrapper
31
- {type: node.type.to_s, lines: "#{node.start_line}..#{node.end_line}"}
32
- else
33
- extract_node_info(node)
34
- end
35
-
36
- debug(label, info)
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,163 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Json
4
- module Merge
5
- # Custom JSON emitter that preserves comments and formatting.
6
- # This class provides utilities for emitting JSON while maintaining
7
- # the original structure, comments, and style choices.
8
- #
9
- # Inherits common emitter functionality from Ast::Merge::EmitterBase.
10
- #
11
- # @example Basic usage
12
- # emitter = Emitter.new
13
- # emitter.emit_object_start
14
- # emitter.emit_pair("key", '"value"')
15
- # emitter.emit_object_end
16
- class Emitter < Ast::Merge::EmitterBase
17
- # @return [Boolean] Whether next item needs a comma
18
- attr_reader :needs_comma
19
-
20
- # Initialize subclass-specific state (comma tracking for JSON)
21
- def initialize_subclass_state(**options)
22
- @needs_comma = false
23
- end
24
-
25
- # Clear subclass-specific state
26
- def clear_subclass_state
27
- @needs_comma = false
28
- end
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
-
41
- # Emit a single-line comment
42
- #
43
- # @param text [String] Comment text (without //)
44
- # @param inline [Boolean] Whether this is an inline comment
45
- def emit_comment(text, inline: false)
46
- if inline
47
- # Inline comments are appended to the last line
48
- return if @lines.empty?
49
-
50
- @lines[-1] = "#{@lines[-1]} // #{text}"
51
- else
52
- @lines << "#{current_indent}// #{text}"
53
- end
54
- end
55
-
56
- # Emit a block comment
57
- #
58
- # @param text [String] Comment text
59
- def emit_block_comment(text)
60
- @lines << "#{current_indent}/* #{text} */"
61
- end
62
-
63
- # Emit object start
64
- def emit_object_start
65
- add_comma_if_needed
66
- @lines << "#{current_indent}{"
67
- indent
68
- @needs_comma = false
69
- end
70
-
71
- # Emit object end
72
- def emit_object_end
73
- dedent
74
- @lines << "#{current_indent}}"
75
- @needs_comma = true
76
- end
77
-
78
- # Emit array start
79
- #
80
- # @param key [String, nil] Key name if this array is a value in an object
81
- def emit_array_start(key = nil)
82
- add_comma_if_needed
83
- @lines << if key
84
- "#{current_indent}\"#{key}\": ["
85
- else
86
- "#{current_indent}["
87
- end
88
- indent
89
- @needs_comma = false
90
- end
91
-
92
- # Emit array end
93
- def emit_array_end
94
- dedent
95
- @lines << "#{current_indent}]"
96
- @needs_comma = true
97
- end
98
-
99
- # Emit a key-value pair
100
- #
101
- # @param key [String] Key name (without quotes)
102
- # @param value [String] Value (already formatted, e.g., '"string"', '123', 'true')
103
- # @param inline_comment [String, nil] Optional inline comment
104
- def emit_pair(key, value, inline_comment: nil)
105
- add_comma_if_needed
106
- line = "#{current_indent}\"#{key}\": #{value}"
107
- line += " // #{inline_comment}" if inline_comment
108
- @lines << line
109
- @needs_comma = true
110
- end
111
-
112
- # Emit an array element
113
- #
114
- # @param value [String] Value (already formatted)
115
- # @param inline_comment [String, nil] Optional inline comment
116
- def emit_array_element(value, inline_comment: nil)
117
- add_comma_if_needed
118
- line = "#{current_indent}#{value}"
119
- line += " // #{inline_comment}" if inline_comment
120
- @lines << line
121
- @needs_comma = true
122
- end
123
-
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
131
- end
132
-
133
- # Emit closing brace for nested object
134
- def emit_nested_object_end
135
- dedent
136
- @lines << "#{current_indent}}"
137
- @needs_comma = true
138
- end
139
-
140
- # Get the output as a JSON string
141
- #
142
- # @return [String]
143
- def to_json
144
- to_s
145
- end
146
-
147
- private
148
-
149
- def add_comma_if_needed
150
- return unless @needs_comma && @lines.any?
151
-
152
- # Add comma to the previous line if it doesn't already have one
153
- last_line = @lines.last
154
- return if last_line.strip.empty?
155
- return if last_line.rstrip.end_with?(",")
156
- return if last_line.rstrip.end_with?("{")
157
- return if last_line.rstrip.end_with?("[")
158
-
159
- @lines[-1] = "#{last_line},"
160
- end
161
- end
162
- end
163
- end