json-merge 7.0.0 → 7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '07183e1164d6af24dd6dad4a4ded1b57b591bb5e9197bdb2fab3e49af2b176a3'
4
- data.tar.gz: 3dc67caacf7ed48b31cf0f7860d9f6381fef38c68b7c5671b3a7a327c6e43240
3
+ metadata.gz: 8464cc00eee820f30fa56722aba993b0f661a3521c185da21e90035cc9dc967f
4
+ data.tar.gz: b70e0d898ba77416611c0b58280b68ec08431119c07ca154cf6e0e565b086b37
5
5
  SHA512:
6
- metadata.gz: 79d8e551876b7b8cca367a73ab5d5b0041d35f649737983bb1029b85a5c7388aaa5f6f80605004183f2d107a3dff34292ca5b15ecbe38380b0efb2651d3fd2d5
7
- data.tar.gz: e16ebd70f7f0b5776d1d21d290f3980031c87aa3adbd9b69c4870010f249ff6b04c5d391554390eb7dc08186b39b3f8f2efe1549a1b1e23c00586ad5d86dffd8
6
+ metadata.gz: cd3daa1695fff2cb7fab9287253f714efc90a5eb4502d6d545394326c44fa151e507375cb302bb0ddce1e998287c348d7feaa724b2aaf8835a6e175639dfe35c
7
+ data.tar.gz: 4314f63422ccb37ecceb31d576d9593195a1f2ace7ba0ddc019eb96419841a52ae816732ec208d1776465c4ee961b4cdf363d8b21e5735ebb25061413a4a0361
checksums.yaml.gz.sig CHANGED
Binary file
data/LICENSE.md ADDED
@@ -0,0 +1,13 @@
1
+ # License
2
+
3
+ This project is made available under the following licenses.
4
+ Choose the option that best fits your use case:
5
+
6
+ - [AGPL-3.0-only](https://github.com/structuredmerge/structuredmerge-ruby/blob/main/AGPL-3.0-only.md)
7
+ - [PolyForm-Small-Business-1.0.0](https://github.com/structuredmerge/structuredmerge-ruby/blob/main/PolyForm-Small-Business-1.0.0.md)
8
+
9
+ If none of the above licenses fit your use case, please [contact us](mailto:floss@galtzo.com) to discuss a custom commercial license.
10
+
11
+ ## Copyright Notice
12
+
13
+ - Required Notice: Copyright (c) 2026 Peter H. Boling
data/README.md ADDED
@@ -0,0 +1,608 @@
1
+ <a href="https://github.com/structuredmerge"><img alt="structuredmerge Logo by GitHub" src="https://github.com/structuredmerge.png?size=192" width="14%" align="right"/></a>
2
+
3
+ # ☯️ Json::Merge
4
+
5
+ [![Version][👽versioni]][👽version] [![Ruby Users Forum][✉️ruby-forum-top-img]][✉️ruby-forum] [![GitHub tag (latest SemVer)][⛳️tag-img]][⛳️tag] [![License: AGPL-3.0-only OR PolyForm-Small-Business-1.0.0][📄license-img]][📄license] [![Total downloads][👽dl-ranki]][👽dl-rank] [![CI Current][🚎11-c-wfi]][🚎11-c-wf]
6
+
7
+ `if ci_badges.map(&:color).detect { it != "green"}` ☝️ [let me know on Discord][✉️discord-invite] or [RubyForum][✉️ruby-forum], as I may have missed the notification.
8
+
9
+ ---
10
+
11
+ `if ci_badges.map(&:color).all? { it == "green"}` 👇️ send money so I can do more of this. FLOSS maintenance is now my full-time job.
12
+
13
+ [![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] [![Liberapay Goal Progress][⛳liberapay-img]][⛳liberapay] [![Donate on PayPal][🖇paypal-img]][🖇paypal] [![Buy me a coffee][🖇buyme-small-img]][🖇buyme] [![Donate at ko-fi.com][🖇kofi-img]][🖇kofi]
14
+
15
+ <details markdown="1">
16
+ <summary>👣 How will this project approach the September 2025 hostile takeover of RubyGems? 🚑️</summary>
17
+
18
+ I've summarized my thoughts in [this blog post](https://dev.to/galtzo/hostile-takeover-of-rubygems-my-thoughts-5hlo).
19
+
20
+ </details>
21
+
22
+ ## 🌻 Synopsis <a href="https://discord.gg/3qme4XHNKN"><img alt="Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/galtzo-floss/avatar-128px.svg" width="8%" align="right"/></a> <a href="https://ruby-toolbox.com"><img alt="ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5" src="https://logos.galtzo.com/assets/images/ruby-lang/avatar-128px.svg" width="8%" align="right"/></a>
23
+
24
+ Json::Merge intelligently merges two versions of a JSON, JSONC, or JSON5 file using the StructuredMerge Ruby stack. It is built on [ast-merge][ast-merge] and [tree_haver][tree_haver], with parser capability reported through the shared backend registry.
25
+
26
+ JSONC and JSON5 use the same `Json::Merge` API as JSON; use `require "json/merge"` for all three dialects.
27
+
28
+ ### JSONC Support
29
+
30
+ The JSON merge provider supports JSON, JSONC, and JSON5 dialects through `Json::Merge`. Pass JSONC content to the same merger API used for JSON.
31
+
32
+ JSONC-specific behavior:
33
+
34
+ - **Single API**: JSON and JSONC both use `Json::Merge`
35
+ - **Comment-Aware**: Preserves `//` and `/* */` comments when the parser exposes them
36
+ - **Trailing Commas**: Accepts trailing commas in JSONC objects and arrays
37
+ - **JSONC Boundaries**: Rejects JSON5-only syntax such as unquoted keys,
38
+ single-quoted strings, and JSON5 numeric literals
39
+ - **Freeze Blocks**: Uses the `json-merge` freeze token by default, with a custom token available when needed
40
+
41
+ ### JSON5 Support
42
+
43
+ JSON5 uses the same structural merge engine with `dialect: :json5`. It accepts
44
+ JSON5 syntax, including comments, trailing commas, unquoted object keys,
45
+ single-quoted strings, hexadecimal and signed numeric literals, `Infinity`, and
46
+ `NaN`. JSONC remains intentionally narrower: it accepts comments and trailing
47
+ commas, but rejects the JSON5-only forms.
48
+
49
+ ### Key Features
50
+
51
+ - **Tree-Sitter Powered**: Uses tree-sitter-json for strict JSON and the
52
+ normalized JSON5 tree for JSONC and JSON5 syntax
53
+ - **JSONC-Aware**: Preserves `//` and `/* */` comments when the parser exposes them
54
+ - **Intelligent**: Matches objects and arrays by structural signatures
55
+ - **Fuzzy Property Matching**: `ObjectMatchRefiner` matches similar property names
56
+ (e.g., `databaseUrl` ↔ `database_url`) using Levenshtein distance for naming convention differences
57
+ - **Full Provenance**: Tracks origin of every node
58
+ - **StructuredMerge Native**: Depends on `ast-merge` and `tree_haver`; parser availability comes from registered backend providers
59
+ - **Customizable**:
60
+ - `signature_generator` - callable custom signature generators
61
+ - `preference` - setting of `:template`, `:destination`, or a Hash for per-node-type preferences
62
+ - `node_splitter` - Hash mapping node types to callables for per-node-type merge customization (see [ast-merge][ast-merge] docs)
63
+ - `add_template_only_nodes` - setting to retain nodes that do not exist in destination
64
+ - `match_refiners` - array of refiners for fuzzy matching (e.g., `ObjectMatchRefiner`)
65
+
66
+ ### Supported Node Types
67
+
68
+ | Node Type | Signature Format | Matching Behavior |
69
+ | --- | --- | --- |
70
+ | Object | `[:object, key_signatures...]` | Objects match by their key structure |
71
+ | Array | `[:array, element_count]` | Arrays match by position and type |
72
+ | Pair | `[:pair, key_name]` | Key-value pairs match by key name |
73
+ | String | `[:string, value]` | Strings match by value |
74
+ | Number | `[:number, value]` | Numbers match by value |
75
+ | Boolean | `[:boolean, value]` | Booleans match by value |
76
+ | Null | `[:null]` | Null values always match |
77
+
78
+ ### Example
79
+
80
+ ```ruby
81
+ require "json/merge"
82
+
83
+ template = File.read("template.json")
84
+ destination = File.read("destination.json")
85
+
86
+ merger = Json::Merge::SmartMerger.new(template, destination)
87
+ result = merger.merge
88
+
89
+ File.write("merged.json", result.to_json)
90
+ ```
91
+
92
+ [ast-merge]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/ast-merge
93
+ [tree_haver]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/tree_haver
94
+
95
+ ## 💡 Info you can shake a stick at
96
+
97
+ | Tokens to Remember | [![Gem name][⛳️name-img]][⛳️gem-name] [![Gem namespace][⛳️namespace-img]][⛳️gem-namespace] |
98
+ |-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
99
+ | Works with MRI Ruby 4 | [![Ruby current Compat][💎ruby-c-i]][🚎11-c-wf]|
100
+ | Support & Community | [![Join Me on Daily.dev's RubyFriends][✉️ruby-friends-img]][✉️ruby-friends] [![Get help from RubyForum][✉️ruby-forum-img]][✉️ruby-forum] [![Live Chat on Discord][✉️discord-invite-img-ftb]][✉️discord-invite] [![Get help from me on Upwork][👨🏼‍🏫expsup-upwork-img]][👨🏼‍🏫expsup-upwork] [![Get help from me on Codementor][👨🏼‍🏫expsup-codementor-img]][👨🏼‍🏫expsup-codementor] |
101
+ | Source | [![Source on GitLab.com][📜src-gl-img]][📜src-gl] [![Source on CodeBerg.org][📜src-cb-img]][📜src-cb] [![Source on GitHub.com][📜src-gh-img]][📜src-gh] [![The best SHA: dQw4w9WgXcQ!][🧮kloc-img]][🧮kloc] |
102
+ | Documentation | [![Current release on RubyDoc.info][📜docs-cr-rd-img]][🚎yard-current] [![YARD on Galtzo.com][📜docs-head-rd-img]][🚎yard-head] [![Maintainer Blog][🚂maint-blog-img]][🚂maint-blog] [![GitLab Wiki][📜gl-wiki-img]][📜gl-wiki] [![GitHub Wiki][📜gh-wiki-img]][📜gh-wiki] |
103
+ | Compliance | [![License: AGPL-3.0-only OR PolyForm-Small-Business-1.0.0][📄license-img]][📄license] [![Apache license compatibility: Category X][📄license-compat-img]][📄license-compat] [![📄ilo-declaration-img]][📄ilo-declaration] [![Security Policy][🔐security-img]][🔐security] [![Contributor Covenant 2.1][🪇conduct-img]][🪇conduct] [![SemVer 2.0.0][📌semver-img]][📌semver] |
104
+ | Style | [![Enforced Code Style Linter][💎rlts-img]][💎rlts] [![Keep-A-Changelog 1.0.0][📗keep-changelog-img]][📗keep-changelog] [![Gitmoji Commits][📌gitmoji-img]][📌gitmoji] [![Compatibility appraised by: appraisal2][💎appraisal2-img]][💎appraisal2] |
105
+ | Maintainer 🎖️ | [![Follow Me on LinkedIn][💖🖇linkedin-img]][💖🖇linkedin] [![Follow Me on Ruby.Social][💖🐘ruby-mast-img]][💖🐘ruby-mast] [![Follow Me on Bluesky][💖🦋bluesky-img]][💖🦋bluesky] [![Contact Maintainer][🚂maint-contact-img]][🚂maint-contact] [![My technical writing][💖💁🏼‍♂️devto-img]][💖💁🏼‍♂️devto] |
106
+ | `...` 💖 | [![Find Me on WellFound:][💖✌️wellfound-img]][💖✌️wellfound] [![Find Me on CrunchBase][💖💲crunchbase-img]][💖💲crunchbase] [![My LinkTree][💖🌳linktree-img]][💖🌳linktree] [![More About Me][💖💁🏼‍♂️aboutme-img]][💖💁🏼‍♂️aboutme] [🧊][💖🧊berg] [🐙][💖🐙hub] [🛖][💖🛖hut] [🧪][💖🧪lab] |
107
+
108
+ ### Compatibility
109
+
110
+ Compatible with MRI Ruby 4.0.0+, and concordant releases of JRuby, and TruffleRuby.
111
+ CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
112
+ This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
113
+ may be higher than the gem's runtime compatibility floor when legacy Rubies are
114
+ not practical for the current toolchain.
115
+
116
+ <a href="https://github.com/kettle-dev"><img alt="kettle-dev Logo by Aboling0, CC BY-SA 4.0" src="https://logos.galtzo.com/assets/images/kettle-dev/avatar-128px.svg" width="14%" align="right"/></a>
117
+
118
+ The _amazing_ test matrix is powered by the kettle-dev stack.
119
+
120
+ <details markdown="1">
121
+ <summary>How kettle-dev manages complexity in tests</summary>
122
+
123
+ | Gem | Source | Role | Total downloads |
124
+ |-----|--------|------|---------------------|
125
+ | [appraisal2](https://clickgems.clickhouse.com/dashboard/appraisal2) | [GitHub](https://github.com/appraisal-rb/appraisal2) | multi-dependency Appraisal matrix generation | [![Total downloads for appraisal2](https://img.shields.io/gem/dt/appraisal2.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/appraisal2) |
126
+ | [appraisal2-rubocop](https://clickgems.clickhouse.com/dashboard/appraisal2-rubocop) | [GitHub](https://github.com/appraisal-rb/appraisal2-rubocop) | RuboCop Appraisal generator integration | [![Total downloads for appraisal2-rubocop](https://img.shields.io/gem/dt/appraisal2-rubocop.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/appraisal2-rubocop) |
127
+ | [kettle-dev](https://clickgems.clickhouse.com/dashboard/kettle-dev) | [GitHub](https://github.com/kettle-dev/kettle-dev) | development, release, and CI workflow tooling | [![Total downloads for kettle-dev](https://img.shields.io/gem/dt/kettle-dev.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-dev) |
128
+ | [kettle-jem](https://clickgems.clickhouse.com/dashboard/kettle-jem) | [GitHub](https://github.com/kettle-dev/kettle-jem) | Appraisals & CI workflow templates | [![Total downloads for kettle-jem](https://img.shields.io/gem/dt/kettle-jem.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-jem) |
129
+ | [kettle-soup-cover](https://clickgems.clickhouse.com/dashboard/kettle-soup-cover) | [GitHub](https://github.com/kettle-dev/kettle-soup-cover) | SimpleCov coverage policy and reporting | [![Total downloads for kettle-soup-cover](https://img.shields.io/gem/dt/kettle-soup-cover.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-soup-cover) |
130
+ | [kettle-test](https://clickgems.clickhouse.com/dashboard/kettle-test) | [GitHub](https://github.com/kettle-dev/kettle-test) | standard test runner and coverage harness | [![Total downloads for kettle-test](https://img.shields.io/gem/dt/kettle-test.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/kettle-test) |
131
+ | [rubocop-lts](https://clickgems.clickhouse.com/dashboard/rubocop-lts) | [GitHub](https://github.com/rubocop-lts/rubocop-lts) | Ruby-version-aware linting | [![Total downloads for rubocop-lts](https://img.shields.io/gem/dt/rubocop-lts.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/rubocop-lts) |
132
+ | [turbo_tests2](https://clickgems.clickhouse.com/dashboard/turbo_tests2) | [GitHub](https://github.com/galtzo-floss/turbo_tests2) | parallel test execution | [![Total downloads for turbo_tests2](https://img.shields.io/gem/dt/turbo_tests2.svg?style=flat-square)](https://clickgems.clickhouse.com/dashboard/turbo_tests2) |
133
+
134
+ </details>
135
+
136
+ ## ✨ Installation
137
+
138
+ Install the gem and add to the application's Gemfile by executing:
139
+
140
+ ```console
141
+ bundle add json-merge
142
+ ```
143
+
144
+ If bundler is not being used to manage dependencies, install the gem by executing:
145
+
146
+ ```console
147
+ gem install json-merge
148
+ ```
149
+
150
+ ## ⚙️ Configuration
151
+
152
+ ### Signature Match Preference
153
+
154
+ Control which version to use when nodes have matching signatures but different content:
155
+
156
+ ```ruby
157
+ # Use template version (for config updates)
158
+ merger = Json::Merge::SmartMerger.new(
159
+ template,
160
+ destination,
161
+ preference: :template,
162
+ )
163
+
164
+ # Use destination version (default - preserve customizations)
165
+ merger = Json::Merge::SmartMerger.new(
166
+ template,
167
+ destination,
168
+ preference: :destination,
169
+ )
170
+ ```
171
+
172
+ ### Template-Only Nodes
173
+
174
+ Control whether to add nodes that only exist in the template:
175
+
176
+ ```ruby
177
+ # Add template-only nodes
178
+ merger = Json::Merge::SmartMerger.new(
179
+ template,
180
+ destination,
181
+ add_template_only_nodes: true,
182
+ )
183
+ ```
184
+
185
+ ### Object Match Refiner
186
+
187
+ When JSON object properties (key-value pairs) don't match by exact key name, the
188
+ `ObjectMatchRefiner` uses fuzzy matching to pair entries with:
189
+
190
+ - Similar key names (e.g., `databaseUrl` vs `database_url`)
191
+ - Keys with typos or different naming conventions (camelCase vs snake\_case)
192
+ - Array elements with similar structure or content
193
+
194
+ <!-- end list -->
195
+
196
+ ```ruby
197
+ # Enable object fuzzy matching
198
+ merger = Json::Merge::SmartMerger.new(
199
+ template,
200
+ destination,
201
+ match_refiners: [
202
+ Json::Merge::ObjectMatchRefiner.new(threshold: 0.5),
203
+ ],
204
+ )
205
+ ```
206
+
207
+ #### ObjectMatchRefiner Options
208
+
209
+ | Option | Default | Description |
210
+ | --- | --- | --- |
211
+ | `threshold` | 0.5 | Minimum similarity score (0.0-1.0) to accept a match |
212
+ | `key_weight` | 0.7 | Weight for key name similarity |
213
+ | `value_weight` | 0.3 | Weight for value similarity |
214
+
215
+ ```ruby
216
+ # Custom weights for key-centric matching
217
+ refiner = Json::Merge::ObjectMatchRefiner.new(
218
+ threshold: 0.6,
219
+ key_weight: 0.8, # Focus more on key names
220
+ value_weight: 0.2, # Less focus on values
221
+ )
222
+ ```
223
+
224
+ ### Debug Logging
225
+
226
+ Enable debug logging to see merge decisions:
227
+
228
+ ```bash
229
+ export JSON_MERGE_DEBUG=1
230
+ ```
231
+
232
+ ### JSONC and JSON5 Options
233
+
234
+ JSONC and JSON5 files use the same options as JSON files. Set `dialect` when
235
+ constructing a direct merger:
236
+
237
+ ```ruby
238
+ merger = Json::Merge::SmartMerger.new(
239
+ template_content,
240
+ dest_content,
241
+ # Which version to prefer when nodes match
242
+ # :destination (default) - keep destination values
243
+ # :template - use template values
244
+ preference: :destination,
245
+
246
+ # Whether to add template-only nodes to the result
247
+ # false (default) - only include properties that exist in destination
248
+ # true - include all template properties
249
+ add_template_only_nodes: false,
250
+
251
+ # :json (strict), :jsonc (comments and trailing commas), or :json5
252
+ dialect: :jsonc,
253
+
254
+ # Token for freeze block markers
255
+ # Default: "json-merge"
256
+ # Looks for: // json-merge:freeze / // json-merge:unfreeze
257
+ freeze_token: "json-merge",
258
+
259
+ # Custom signature generator (optional)
260
+ # Receives a node, returns a signature array or nil
261
+ signature_generator: ->(node) { [:pair, node.key] if node.type == :pair },
262
+ )
263
+ ```
264
+
265
+ ## 🔧 Basic Usage
266
+
267
+ ### Merging Two JSON Files
268
+
269
+ ```ruby
270
+ require "json/merge"
271
+
272
+ template_content = File.read("template.json")
273
+ dest_content = File.read("destination.json")
274
+
275
+ merger = Json::Merge::SmartMerger.new(template_content, dest_content)
276
+ result = merger.merge
277
+
278
+ File.write("merged.json", result.to_json)
279
+ ```
280
+
281
+ ### Analyzing a JSON File
282
+
283
+ ```ruby
284
+ require "json/merge"
285
+
286
+ source = File.read("config.json")
287
+ analysis = Json::Merge::FileAnalysis.new(source)
288
+
289
+ # Iterate over all top-level nodes
290
+ analysis.statements.each do |node|
291
+ sig = analysis.generate_signature(node)
292
+ puts "#{node.class}: #{sig.inspect}"
293
+ end
294
+ ```
295
+
296
+ ### Fuzzy Property Matching
297
+
298
+ When property names differ between template and destination (e.g., naming convention changes),
299
+ use the `ObjectMatchRefiner`:
300
+
301
+ ```ruby
302
+ require "json/merge"
303
+
304
+ template = <<~JSON
305
+ {
306
+ "databaseUrl": "postgres://localhost/app",
307
+ "cacheTimeout": 3600,
308
+ "apiEndpoint": "https://api.example.com"
309
+ }
310
+ JSON
311
+
312
+ destination = <<~JSON
313
+ {
314
+ "database_url": "postgres://localhost/custom",
315
+ "cache_ttl": 7200,
316
+ "api_endpoint": "https://custom.example.com"
317
+ }
318
+ JSON
319
+
320
+ # Default merge won't match keys (names differ - camelCase vs snake_case)
321
+ # Use ObjectMatchRefiner for fuzzy matching
322
+ merger = Json::Merge::SmartMerger.new(
323
+ template,
324
+ destination,
325
+ match_refiners: [
326
+ Json::Merge::ObjectMatchRefiner.new(threshold: 0.5),
327
+ ],
328
+ )
329
+ result = merger.merge
330
+
331
+ # Properties are matched despite naming convention differences:
332
+ # - databaseUrl ↔ database_url (similar when normalized)
333
+ # - cacheTimeout ↔ cache_ttl (similar: "cache")
334
+ # - apiEndpoint ↔ api_endpoint (similar when normalized)
335
+ ```
336
+
337
+ ### Array Element Matching
338
+
339
+ The `ObjectMatchRefiner` also handles array elements with similar structure:
340
+
341
+ ```ruby
342
+ template = <<~JSON
343
+ {
344
+ "users": [
345
+ { "id": 1, "userName": "alice" },
346
+ { "id": 2, "userName": "bob" }
347
+ ]
348
+ }
349
+ JSON
350
+
351
+ destination = <<~JSON
352
+ {
353
+ "users": [
354
+ { "id": 1, "user_name": "alice_custom" },
355
+ { "id": 3, "user_name": "charlie" }
356
+ ]
357
+ }
358
+ JSON
359
+
360
+ merger = Json::Merge::SmartMerger.new(
361
+ template,
362
+ destination,
363
+ match_refiners: [
364
+ Json::Merge::ObjectMatchRefiner.new(threshold: 0.5),
365
+ ],
366
+ )
367
+ # Array elements with matching IDs or similar structure are paired
368
+ ```
369
+
370
+ ### Merging JSONC Files
371
+
372
+ ```ruby
373
+ require "json/merge"
374
+
375
+ template = File.read("template.jsonc")
376
+ destination = File.read("destination.jsonc")
377
+
378
+ merger = Json::Merge::SmartMerger.new(template, destination)
379
+ result = merger.merge
380
+
381
+ File.write("merged.jsonc", result)
382
+ ```
383
+
384
+ ### Merging JSON5 Files
385
+
386
+ ```ruby
387
+ require "json/merge"
388
+
389
+ template = File.read("template.json5")
390
+ destination = File.read("destination.json5")
391
+
392
+ merger = Json::Merge::SmartMerger.new(template, destination, dialect: :json5)
393
+ result = merger.merge
394
+
395
+ File.write("merged.json5", result)
396
+ ```
397
+
398
+ ### JSONC Freeze Blocks
399
+
400
+ Freeze blocks protect sections from being overwritten during merge:
401
+
402
+ ```jsonc
403
+ {
404
+ "name": "my-app",
405
+
406
+ // json-merge:freeze Secret configuration
407
+ "api_key": "my_production_api_key",
408
+ "api_secret": "super_secret_value",
409
+ // json-merge:unfreeze
410
+
411
+ "debug": false
412
+ }
413
+ ```
414
+
415
+ Content between `// json-merge:freeze` and `// json-merge:unfreeze` markers is preserved from the destination file, regardless of what the template contains.
416
+
417
+ ### Adding Template-Only JSONC Properties
418
+
419
+ ```ruby
420
+ merger = Json::Merge::SmartMerger.new(
421
+ template,
422
+ destination,
423
+ add_template_only_nodes: true,
424
+ )
425
+ result = merger.merge
426
+ # Result includes properties from template that do not exist in destination
427
+ ```
428
+
429
+ ## 🔐 Security
430
+
431
+ See [SECURITY.md][🔐security].
432
+
433
+ ## 🤝 Contributing
434
+
435
+ If you need some ideas of where to help, you could work on adding more code coverage,
436
+ or if it is already 💯 (see [below](#code-coverage)) check [issues][🤝gh-issues] or [PRs][🤝gh-pulls],
437
+ or use the gem and think about how it could be better.
438
+
439
+ We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
440
+
441
+ See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
442
+
443
+ ### Code Coverage
444
+
445
+ <details markdown="1">
446
+ <summary>Coverage service badges</summary>
447
+
448
+ </details>
449
+
450
+ ## 📌 Versioning
451
+
452
+ This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
453
+ For most applications, prefer the [Pessimistic Version Constraint][📌pvc] with two digits of precision.
454
+
455
+ For example:
456
+
457
+ ```ruby
458
+ spec.add_dependency("json-merge", "~> 7.0")
459
+ ```
460
+
461
+ <details markdown="1">
462
+ <summary>📌 Is "Platform Support" part of the public API? More details inside.</summary>
463
+
464
+ Dropping support for a platform can be a breaking change for affected users.
465
+ If a release changes supported platforms, it should be called out clearly in the changelog and versioned with that impact in mind.
466
+
467
+ To get a better understanding of how SemVer is intended to work over a project's lifetime,
468
+ read this article from the creator of SemVer:
469
+
470
+ - ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred]
471
+
472
+ </details>
473
+
474
+ See [CHANGELOG.md][📌changelog] for a list of releases.
475
+
476
+ ## 📄 License
477
+
478
+ The gem is available under the following licenses: [AGPL-3.0-only](https://github.com/structuredmerge/structuredmerge-ruby/blob/main/AGPL-3.0-only.md), [PolyForm-Small-Business-1.0.0](https://github.com/structuredmerge/structuredmerge-ruby/blob/main/PolyForm-Small-Business-1.0.0.md).
479
+ See [LICENSE.md][📄license] for details.
480
+
481
+ If none of the available licenses suit your use case, please [contact us](mailto:floss@galtzo.com) to discuss a custom commercial license.
482
+
483
+ [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay&color=a51611&style=flat
484
+ [⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/pboling.svg?style=for-the-badge&logo=liberapay&color=a51611
485
+ [⛳liberapay]: https://liberapay.com/pboling/donate
486
+ [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
487
+ [🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-pboling-blue?style=for-the-badge&logo=github
488
+ [🖇sponsor]: https://github.com/sponsors/pboling
489
+ [🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat
490
+ [🖇kofi]: https://ko-fi.com/pboling
491
+ [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat
492
+ [🖇buyme-img]: https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20latte&emoji=&slug=pboling&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff
493
+ [🖇buyme]: https://www.buymeacoffee.com/pboling
494
+ [🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal
495
+ [🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A
496
+ [🖇paypal]: https://www.paypal.com/paypalme/peterboling
497
+ [🖇floss-funding.dev]: https://floss-funding.dev
498
+ [🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding
499
+ [✉️discord-invite]: https://discord.gg/3qme4XHNKN
500
+ [✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord
501
+ [✉️ruby-friends-img]: https://img.shields.io/badge/daily.dev-%F0%9F%92%8E_Ruby_Friends-0A0A0A?style=for-the-badge&logo=dailydotdev&logoColor=white
502
+ [✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends
503
+ [✉️ruby-forum-top-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=flat&logo=discourse&label=Ruby%20Users%20Forum
504
+ [✉️ruby-forum-img]: https://img.shields.io/discourse/topics?server=https%3A%2F%2Fwww.rubyforum.org&style=for-the-badge&logo=discourse&label=Ruby%20Users%20Forum
505
+ [✉️ruby-forum]: https://www.rubyforum.org/tag/structuredmerge
506
+ [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
507
+ [⛳️gem-namespace]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/json-merge
508
+ [⛳️namespace-img]: https://img.shields.io/badge/namespace-Json::Merge-3C2D2D.svg?style=square&logo=ruby&logoColor=white
509
+ [⛳️gem-name]: https://clickgems.clickhouse.com/dashboard/json-merge
510
+ [⛳️name-img]: https://img.shields.io/badge/name-json--merge-3C2D2D.svg?style=square&logo=rubygems&logoColor=red
511
+ [⛳️tag-img]: https://img.shields.io/github/tag/structuredmerge/structuredmerge-ruby.svg
512
+ [⛳️tag]: https://github.com/structuredmerge/structuredmerge-ruby/releases
513
+ [🚂maint-blog]: http://www.railsbling.com/tags/json-merge
514
+ [🚂maint-blog-img]: https://img.shields.io/badge/blog-railsbling-0093D0.svg?style=for-the-badge&logo=rubyonrails&logoColor=orange
515
+ [🚂maint-contact]: http://www.railsbling.com/contact
516
+ [🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red
517
+ [💖🖇linkedin]: http://www.linkedin.com/in/peterboling
518
+ [💖🖇linkedin-img]: https://img.shields.io/badge/LinkedIn-Profile-0B66C2?style=flat&logo=newjapanprowrestling
519
+ [💖✌️wellfound]: https://wellfound.com/u/peter-boling
520
+ [💖✌️wellfound-img]: https://img.shields.io/badge/peter--boling-orange?style=flat&logo=wellfound
521
+ [💖💲crunchbase]: https://www.crunchbase.com/person/peter-boling
522
+ [💖💲crunchbase-img]: https://img.shields.io/badge/peter--boling-purple?style=flat&logo=crunchbase
523
+ [💖🐘ruby-mast]: https://ruby.social/@galtzo
524
+ [💖🐘ruby-mast-img]: https://img.shields.io/mastodon/follow/109447111526622197?domain=https://ruby.social&style=flat&logo=mastodon&label=Ruby%20@galtzo
525
+ [💖🦋bluesky]: https://bsky.app/profile/galtzo.com
526
+ [💖🦋bluesky-img]: https://img.shields.io/badge/@galtzo.com-0285FF?style=flat&logo=bluesky&logoColor=white
527
+ [💖🌳linktree]: https://linktr.ee/galtzo
528
+ [💖🌳linktree-img]: https://img.shields.io/badge/galtzo-purple?style=flat&logo=linktree
529
+ [💖💁🏼‍♂️devto]: https://dev.to/galtzo
530
+ [💖💁🏼‍♂️devto-img]: https://img.shields.io/badge/dev.to-0A0A0A?style=flat&logo=devdotto&logoColor=white
531
+ [💖💁🏼‍♂️aboutme]: https://about.me/peter.boling
532
+ [💖💁🏼‍♂️aboutme-img]: https://img.shields.io/badge/about.me-0A0A0A?style=flat&logo=aboutme&logoColor=white
533
+ [💖🧊berg]: https://codeberg.org/pboling
534
+ [💖🐙hub]: https://github.org/pboling
535
+ [💖🛖hut]: https://sr.ht/~galtzo/
536
+ [💖🧪lab]: https://gitlab.com/pboling
537
+ [👨🏼‍🏫expsup-upwork]: https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share
538
+ [👨🏼‍🏫expsup-upwork-img]: https://img.shields.io/badge/UpWork-13544E?style=for-the-badge&logo=Upwork&logoColor=white
539
+ [👨🏼‍🏫expsup-codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github
540
+ [👨🏼‍🏫expsup-codementor-img]: https://img.shields.io/badge/CodeMentor-Get_Help-1abc9c?style=for-the-badge&logo=CodeMentor&logoColor=white
541
+ [🏙️entsup-tidelift]: https://tidelift.com/subscription/pkg/rubygems-json-merge?utm_source=rubygems-json-merge&utm_medium=referral&utm_campaign=readme
542
+ [🏙️entsup-tidelift-img]: https://img.shields.io/badge/Tidelift_and_Sonar-Enterprise_Support-FD3456?style=for-the-badge&logo=sonar&logoColor=white
543
+ [🏙️entsup-tidelift-sonar]: https://blog.tidelift.com/tidelift-joins-sonar
544
+ [💁🏼‍♂️peterboling]: http://www.peterboling.com
545
+ [🚂railsbling]: http://www.railsbling.com
546
+ [📜src-gl-img]: https://img.shields.io/badge/GitLab-FBA326?style=for-the-badge&logo=Gitlab&logoColor=orange
547
+ [📜src-gl]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/tree/main/gems/json-merge
548
+ [📜src-cb-img]: https://img.shields.io/badge/CodeBerg-4893CC?style=for-the-badge&logo=CodeBerg&logoColor=blue
549
+ [📜src-cb]: https://codeberg.org/structuredmerge/structuredmerge-ruby/src/branch/main/gems/json-merge
550
+ [📜src-gh-img]: https://img.shields.io/badge/GitHub-238636?style=for-the-badge&logo=GitHub&logoColor=green
551
+ [📜src-gh]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/json-merge
552
+ [📜docs-cr-rd-img]: https://img.shields.io/badge/RubyDoc-Current_Release-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
553
+ [📜docs-head-rd-img]: https://img.shields.io/badge/YARD_on_Galtzo.com-HEAD-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
554
+ [📜gl-wiki]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/wikis/home
555
+ [📜gh-wiki]: https://github.com/structuredmerge/structuredmerge-ruby/wiki
556
+ [📜gl-wiki-img]: https://img.shields.io/badge/wiki-gitlab-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white
557
+ [📜gh-wiki-img]: https://img.shields.io/badge/wiki-github-943CD2.svg?style=for-the-badge&logo=github&logoColor=white
558
+ [👽dl-rank]: https://clickgems.clickhouse.com/dashboard/json-merge
559
+ [👽dl-ranki]: https://img.shields.io/gem/dt/json-merge.svg
560
+ [👽version]: https://clickgems.clickhouse.com/dashboard/json-merge
561
+ [👽versioni]: https://img.shields.io/gem/v/json-merge.svg
562
+ [🚎11-c-wf]: https://github.com/structuredmerge/structuredmerge-ruby/actions/workflows/current.yml
563
+ [🚎11-c-wfi]: https://github.com/structuredmerge/structuredmerge-ruby/actions/workflows/current.yml/badge.svg
564
+ [💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
565
+ [🤝gh-issues]: https://github.com/structuredmerge/structuredmerge-ruby/issues
566
+ [🤝gh-pulls]: https://github.com/structuredmerge/structuredmerge-ruby/pulls
567
+ [🤝gl-issues]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/issues
568
+ [🤝gl-pulls]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/merge_requests
569
+ [🤝cb-issues]: https://codeberg.org/structuredmerge/structuredmerge-ruby/issues
570
+ [🤝cb-pulls]: https://codeberg.org/structuredmerge/structuredmerge-ruby/pulls
571
+ [🤝cb-donate]: https://donate.codeberg.org/
572
+ [🤝contributing]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CONTRIBUTING.md
573
+ [🖐contrib-rocks]: https://contrib.rocks
574
+ [🖐contributors]: https://github.com/structuredmerge/structuredmerge-ruby/graphs/contributors
575
+ [🖐contributors-img]: https://contrib.rocks/image?repo=structuredmerge/structuredmerge-ruby
576
+ [🪇conduct]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CODE_OF_CONDUCT.md
577
+ [🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg
578
+ [📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
579
+ [📌semver]: https://semver.org/spec/v2.0.0.html
580
+ [📌semver-img]: https://img.shields.io/badge/semver-2.0.0-259D6C.svg?style=flat
581
+ [📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139
582
+ [📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html
583
+ [📌changelog]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CHANGELOG.md
584
+ [📗keep-changelog]: https://keepachangelog.com/en/1.0.0/
585
+ [📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-34495e.svg?style=flat
586
+ [📌gitmoji]: https://gitmoji.dev
587
+ [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
588
+ [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
589
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.658-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
590
+ [🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
591
+ [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
592
+ [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
593
+ [📄license]: LICENSE.md
594
+ [📄license-ref]: LICENSE.md
595
+ [📄license-img]: https://img.shields.io/badge/License-AGPL--3.0--only_OR_PolyForm--Small--Business--1.0.0-259D6C.svg
596
+ [📄license-compat]: https://www.apache.org/legal/resolved.html#category-x
597
+ [📄license-compat-img]: https://img.shields.io/badge/Apache_Incompatible:_Category_X-%E2%9C%97-C0392B.svg?style=flat&logo=Apache
598
+ [📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
599
+ [📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
600
+ [🚎yard-current]: http://rubydoc.info/gems/json-merge
601
+ [🚎yard-head]: https://json-merge.galtzo.com
602
+ [💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums
603
+ [💎SHA_checksums]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/tree/main/checksums
604
+ [💎rlts]: https://github.com/rubocop-lts/rubocop-lts
605
+ [💎rlts-img]: https://img.shields.io/badge/code_style_&_linting-rubocop--lts-34495e.svg?plastic&logo=ruby&logoColor=white
606
+ [💎appraisal2]: https://github.com/appraisal-rb/appraisal2
607
+ [💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
608
+ [💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Json
4
+ module Merge
5
+ # Extracts and tracks comments with their line numbers from JSON / JSONC
6
+ # source. The current tree-sitter JSON grammar can surface JSONC comments,
7
+ # so json-merge now uses the shared C-style comment tracker as well.
8
+ class CommentTracker < Ast::Merge::Comment::CStyleTrackerBase
9
+ # @param source [String] JSON / JSONC source code
10
+ def initialize(source)
11
+ @source = source
12
+ super(source.lines.map(&:chomp))
13
+ end
14
+
15
+ private
16
+
17
+ def owner_line_num(owner)
18
+ return owner.start_line if owner.respond_to?(:start_line) && owner.start_line
19
+ if owner.respond_to?(:key) && owner.key.respond_to?(:start_line) && owner.key.start_line
20
+ return owner.key.start_line
21
+ end
22
+
23
+ nil
24
+ end
25
+ end
26
+ end
27
+ end