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
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
|
data/lib/json/merge/emitter.rb
DELETED
|
@@ -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
|