json-merge 7.0.0 → 7.1.3

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: ff5d2c1fa09edee00c444aedc35377b040f6dc6e07ae160e31243ffa07ebbe70
4
+ data.tar.gz: 8601741a2313c127bb861081460a4ae6e817f247f804e78721a162dfeeb2010b
5
5
  SHA512:
6
- metadata.gz: 79d8e551876b7b8cca367a73ab5d5b0041d35f649737983bb1029b85a5c7388aaa5f6f80605004183f2d107a3dff34292ca5b15ecbe38380b0efb2651d3fd2d5
7
- data.tar.gz: e16ebd70f7f0b5776d1d21d290f3980031c87aa3adbd9b69c4870010f249ff6b04c5d391554390eb7dc08186b39b3f8f2efe1549a1b1e23c00586ad5d86dffd8
6
+ metadata.gz: ce19e05de329325e569cca1b0fbb80a4933ff2b8a690bcabf8d68111202793f46fc16f6c1434c01497b607d1e5d1440bb13d4e923ab4b1f941a08faf99706037
7
+ data.tar.gz: 0eba7ac69972809823b4478640a66e49b068829a830066fb9c00465298cba8d0b9c23b7cfb4ebd04e582f3aa0f086289fe84efaa4da777d7b4c3026df7d3199d
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,600 @@
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+, 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
+ check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], or use the gem and think about how it could be better.
437
+
438
+ We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
439
+
440
+ See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
441
+
442
+ ## 📌 Versioning
443
+
444
+ This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
445
+ For most applications, prefer the [Pessimistic Version Constraint][📌pvc] with two digits of precision.
446
+
447
+ For example:
448
+
449
+ ```ruby
450
+ spec.add_dependency("json-merge", "~> 7.0")
451
+ ```
452
+
453
+ <details markdown="1">
454
+ <summary>📌 Is "Platform Support" part of the public API? More details inside.</summary>
455
+
456
+ Dropping support for a platform can be a breaking change for affected users.
457
+ If a release changes supported platforms, it should be called out clearly in the changelog and versioned with that impact in mind.
458
+
459
+ To get a better understanding of how SemVer is intended to work over a project's lifetime,
460
+ read this article from the creator of SemVer:
461
+
462
+ - ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred]
463
+
464
+ </details>
465
+
466
+ See [CHANGELOG.md][📌changelog] for a list of releases.
467
+
468
+ ## 📄 License
469
+
470
+ 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).
471
+ See [LICENSE.md][📄license] for details.
472
+
473
+ If none of the available licenses suit your use case, please [contact us](mailto:floss@galtzo.com) to discuss a custom commercial license.
474
+
475
+ [⛳liberapay-img]: https://img.shields.io/liberapay/goal/pboling.svg?logo=liberapay&color=a51611&style=flat
476
+ [⛳liberapay-bottom-img]: https://img.shields.io/liberapay/goal/pboling.svg?style=for-the-badge&logo=liberapay&color=a51611
477
+ [⛳liberapay]: https://liberapay.com/pboling/donate
478
+ [🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github
479
+ [🖇sponsor-bottom-img]: https://img.shields.io/badge/Sponsor_Me!-pboling-blue?style=for-the-badge&logo=github
480
+ [🖇sponsor]: https://github.com/sponsors/pboling
481
+ [🖇kofi-img]: https://img.shields.io/badge/ko--fi-%E2%9C%93-a51611.svg?style=flat
482
+ [🖇kofi]: https://ko-fi.com/pboling
483
+ [🖇buyme-small-img]: https://img.shields.io/badge/buy_me_a_coffee-%E2%9C%93-a51611.svg?style=flat
484
+ [🖇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
485
+ [🖇buyme]: https://www.buymeacoffee.com/pboling
486
+ [🖇paypal-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=flat&logo=paypal
487
+ [🖇paypal-bottom-img]: https://img.shields.io/badge/donate-paypal-a51611.svg?style=for-the-badge&logo=paypal&color=0A0A0A
488
+ [🖇paypal]: https://www.paypal.com/paypalme/peterboling
489
+ [🖇floss-funding.dev]: https://floss-funding.dev
490
+ [🖇floss-funding-gem]: https://github.com/galtzo-floss/floss_funding
491
+ [✉️discord-invite]: https://discord.gg/3qme4XHNKN
492
+ [✉️discord-invite-img-ftb]: https://img.shields.io/discord/1373797679469170758?style=for-the-badge&logo=discord
493
+ [✉️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
494
+ [✉️ruby-friends]: https://app.daily.dev/squads/rubyfriends
495
+ [✉️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
496
+ [✉️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
497
+ [✉️ruby-forum]: https://www.rubyforum.org/tag/structuredmerge
498
+ [✇bundle-group-pattern]: https://gist.github.com/pboling/4564780
499
+ [⛳️gem-namespace]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/json-merge
500
+ [⛳️namespace-img]: https://img.shields.io/badge/namespace-Json::Merge-3C2D2D.svg?style=square&logo=ruby&logoColor=white
501
+ [⛳️gem-name]: https://clickgems.clickhouse.com/dashboard/json-merge
502
+ [⛳️name-img]: https://img.shields.io/badge/name-json--merge-3C2D2D.svg?style=square&logo=rubygems&logoColor=red
503
+ [⛳️tag-img]: https://img.shields.io/github/tag/structuredmerge/structuredmerge-ruby.svg
504
+ [⛳️tag]: https://github.com/structuredmerge/structuredmerge-ruby/releases
505
+ [🚂maint-blog]: http://www.railsbling.com/tags/json-merge
506
+ [🚂maint-blog-img]: https://img.shields.io/badge/blog-railsbling-0093D0.svg?style=for-the-badge&logo=rubyonrails&logoColor=orange
507
+ [🚂maint-contact]: http://www.railsbling.com/contact
508
+ [🚂maint-contact-img]: https://img.shields.io/badge/Contact-Maintainer-0093D0.svg?style=flat&logo=rubyonrails&logoColor=red
509
+ [💖🖇linkedin]: http://www.linkedin.com/in/peterboling
510
+ [💖🖇linkedin-img]: https://img.shields.io/badge/LinkedIn-Profile-0B66C2?style=flat&logo=newjapanprowrestling
511
+ [💖✌️wellfound]: https://wellfound.com/u/peter-boling
512
+ [💖✌️wellfound-img]: https://img.shields.io/badge/peter--boling-orange?style=flat&logo=wellfound
513
+ [💖💲crunchbase]: https://www.crunchbase.com/person/peter-boling
514
+ [💖💲crunchbase-img]: https://img.shields.io/badge/peter--boling-purple?style=flat&logo=crunchbase
515
+ [💖🐘ruby-mast]: https://ruby.social/@galtzo
516
+ [💖🐘ruby-mast-img]: https://img.shields.io/mastodon/follow/109447111526622197?domain=https://ruby.social&style=flat&logo=mastodon&label=Ruby%20@galtzo
517
+ [💖🦋bluesky]: https://bsky.app/profile/galtzo.com
518
+ [💖🦋bluesky-img]: https://img.shields.io/badge/@galtzo.com-0285FF?style=flat&logo=bluesky&logoColor=white
519
+ [💖🌳linktree]: https://linktr.ee/galtzo
520
+ [💖🌳linktree-img]: https://img.shields.io/badge/galtzo-purple?style=flat&logo=linktree
521
+ [💖💁🏼‍♂️devto]: https://dev.to/galtzo
522
+ [💖💁🏼‍♂️devto-img]: https://img.shields.io/badge/dev.to-0A0A0A?style=flat&logo=devdotto&logoColor=white
523
+ [💖💁🏼‍♂️aboutme]: https://about.me/peter.boling
524
+ [💖💁🏼‍♂️aboutme-img]: https://img.shields.io/badge/about.me-0A0A0A?style=flat&logo=aboutme&logoColor=white
525
+ [💖🧊berg]: https://codeberg.org/pboling
526
+ [💖🐙hub]: https://github.org/pboling
527
+ [💖🛖hut]: https://sr.ht/~galtzo/
528
+ [💖🧪lab]: https://gitlab.com/pboling
529
+ [👨🏼‍🏫expsup-upwork]: https://www.upwork.com/freelancers/~014942e9b056abdf86?mp_source=share
530
+ [👨🏼‍🏫expsup-upwork-img]: https://img.shields.io/badge/UpWork-13544E?style=for-the-badge&logo=Upwork&logoColor=white
531
+ [👨🏼‍🏫expsup-codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github
532
+ [👨🏼‍🏫expsup-codementor-img]: https://img.shields.io/badge/CodeMentor-Get_Help-1abc9c?style=for-the-badge&logo=CodeMentor&logoColor=white
533
+ [🏙️entsup-tidelift]: https://tidelift.com/subscription/pkg/rubygems-json-merge?utm_source=rubygems-json-merge&utm_medium=referral&utm_campaign=readme
534
+ [🏙️entsup-tidelift-img]: https://img.shields.io/badge/Tidelift_and_Sonar-Enterprise_Support-FD3456?style=for-the-badge&logo=sonar&logoColor=white
535
+ [🏙️entsup-tidelift-sonar]: https://blog.tidelift.com/tidelift-joins-sonar
536
+ [💁🏼‍♂️peterboling]: http://www.peterboling.com
537
+ [🚂railsbling]: http://www.railsbling.com
538
+ [📜src-gl-img]: https://img.shields.io/badge/GitLab-FBA326?style=for-the-badge&logo=Gitlab&logoColor=orange
539
+ [📜src-gl]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/tree/main/gems/json-merge
540
+ [📜src-cb-img]: https://img.shields.io/badge/CodeBerg-4893CC?style=for-the-badge&logo=CodeBerg&logoColor=blue
541
+ [📜src-cb]: https://codeberg.org/structuredmerge/structuredmerge-ruby/src/branch/main/gems/json-merge
542
+ [📜src-gh-img]: https://img.shields.io/badge/GitHub-238636?style=for-the-badge&logo=GitHub&logoColor=green
543
+ [📜src-gh]: https://github.com/structuredmerge/structuredmerge-ruby/tree/main/gems/json-merge
544
+ [📜docs-cr-rd-img]: https://img.shields.io/badge/RubyDoc-Current_Release-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
545
+ [📜docs-head-rd-img]: https://img.shields.io/badge/YARD_on_Galtzo.com-HEAD-943CD2?style=for-the-badge&logo=readthedocs&logoColor=white
546
+ [📜gl-wiki]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/wikis/home
547
+ [📜gh-wiki]: https://github.com/structuredmerge/structuredmerge-ruby/wiki
548
+ [📜gl-wiki-img]: https://img.shields.io/badge/wiki-gitlab-943CD2.svg?style=for-the-badge&logo=gitlab&logoColor=white
549
+ [📜gh-wiki-img]: https://img.shields.io/badge/wiki-github-943CD2.svg?style=for-the-badge&logo=github&logoColor=white
550
+ [👽dl-rank]: https://clickgems.clickhouse.com/dashboard/json-merge
551
+ [👽dl-ranki]: https://img.shields.io/gem/dt/json-merge.svg
552
+ [👽version]: https://clickgems.clickhouse.com/dashboard/json-merge
553
+ [👽versioni]: https://img.shields.io/gem/v/json-merge.svg
554
+ [🚎11-c-wf]: https://github.com/structuredmerge/structuredmerge-ruby/actions/workflows/current.yml
555
+ [🚎11-c-wfi]: https://github.com/structuredmerge/structuredmerge-ruby/actions/workflows/current.yml/badge.svg
556
+ [💎ruby-c-i]: https://img.shields.io/badge/Ruby-current-CC342D?style=for-the-badge&logo=ruby&logoColor=green
557
+ [🤝gh-issues]: https://github.com/structuredmerge/structuredmerge-ruby/issues
558
+ [🤝gh-pulls]: https://github.com/structuredmerge/structuredmerge-ruby/pulls
559
+ [🤝gl-issues]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/issues
560
+ [🤝gl-pulls]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/merge_requests
561
+ [🤝cb-issues]: https://codeberg.org/structuredmerge/structuredmerge-ruby/issues
562
+ [🤝cb-pulls]: https://codeberg.org/structuredmerge/structuredmerge-ruby/pulls
563
+ [🤝cb-donate]: https://donate.codeberg.org/
564
+ [🤝contributing]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CONTRIBUTING.md
565
+ [🖐contrib-rocks]: https://contrib.rocks
566
+ [🖐contributors]: https://github.com/structuredmerge/structuredmerge-ruby/graphs/contributors
567
+ [🖐contributors-img]: https://contrib.rocks/image?repo=structuredmerge/structuredmerge-ruby
568
+ [🪇conduct]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CODE_OF_CONDUCT.md
569
+ [🪇conduct-img]: https://img.shields.io/badge/Contributor_Covenant-2.1-259D6C.svg
570
+ [📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint
571
+ [📌semver]: https://semver.org/spec/v2.0.0.html
572
+ [📌semver-img]: https://img.shields.io/badge/semver-2.0.0-259D6C.svg?style=flat
573
+ [📌semver-breaking]: https://github.com/semver/semver/issues/716#issuecomment-869336139
574
+ [📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html
575
+ [📌changelog]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/CHANGELOG.md
576
+ [📗keep-changelog]: https://keepachangelog.com/en/1.0.0/
577
+ [📗keep-changelog-img]: https://img.shields.io/badge/keep--a--changelog-1.0.0-34495e.svg?style=flat
578
+ [📌gitmoji]: https://gitmoji.dev
579
+ [📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
580
+ [🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
581
+ [🧮kloc-img]: https://img.shields.io/badge/KLOC-1.658-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
582
+ [🔐security]: https://github.com/structuredmerge/structuredmerge-ruby/blob/main/SECURITY.md
583
+ [🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
584
+ [📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
585
+ [📄license]: LICENSE.md
586
+ [📄license-ref]: LICENSE.md
587
+ [📄license-img]: https://img.shields.io/badge/License-AGPL--3.0--only_OR_PolyForm--Small--Business--1.0.0-259D6C.svg
588
+ [📄license-compat]: https://www.apache.org/legal/resolved.html#category-x
589
+ [📄license-compat-img]: https://img.shields.io/badge/Apache_Incompatible:_Category_X-%E2%9C%97-C0392B.svg?style=flat&logo=Apache
590
+ [📄ilo-declaration]: https://www.ilo.org/declaration/lang--en/index.htm
591
+ [📄ilo-declaration-img]: https://img.shields.io/badge/ILO_Fundamental_Principles-✓-259D6C.svg?style=flat
592
+ [🚎yard-current]: http://rubydoc.info/gems/json-merge
593
+ [🚎yard-head]: https://json-merge.galtzo.com
594
+ [💎stone_checksums]: https://github.com/galtzo-floss/stone_checksums
595
+ [💎SHA_checksums]: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/tree/main/checksums
596
+ [💎rlts]: https://github.com/rubocop-lts/rubocop-lts
597
+ [💎rlts-img]: https://img.shields.io/badge/code_style_&_linting-rubocop--lts-34495e.svg?plastic&logo=ruby&logoColor=white
598
+ [💎appraisal2]: https://github.com/appraisal-rb/appraisal2
599
+ [💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
600
+ [💎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