json-merge 1.0.0 → 1.1.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/CHANGELOG.md +57 -1
- data/README.md +106 -27
- data/lib/json/merge/conflict_resolver.rb +141 -83
- data/lib/json/merge/emitter.rb +42 -62
- data/lib/json/merge/node_wrapper.rb +35 -7
- data/lib/json/merge/version.rb +1 -1
- data/lib/json/merge.rb +12 -0
- data.tar.gz.sig +0 -0
- metadata +12 -12
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db4707dba4ede9ef1ecab447fafe24788891d076c69714fc700f17a35d8f5bae
|
|
4
|
+
data.tar.gz: 5f88a2d90a1695c3c7ffe4d4f08c83090fae455f6cd1810a29b331045f636f31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4810ca402f5af373769e6a69a73ace769f6d6f11e61c3d7a2a1b1186672387bc41f92b40fc74a327648a856d38547a60b120583fddd3329b17941b0e0ea51e9
|
|
7
|
+
data.tar.gz: 1e04ca747b1f07f3177626e351947c9dd40cd78f810d1b285925a3c0bee0bbeecfca2891872b53b288d33ab66fb171b0f4ad3d84dc919f2ddf75b0d181b3fbb4
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,60 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.1.0] - 2026-01-12
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.1.0][1.1.0t]
|
|
36
|
+
- COVERAGE: 95.77% -- 589/615 lines in 10 files
|
|
37
|
+
- BRANCH COVERAGE: 78.82% -- 227/288 branches in 10 files
|
|
38
|
+
- 96.63% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- bin/rspec-ffi to run FFI isolated specs
|
|
43
|
+
- Also bin/rake ffi_specs
|
|
44
|
+
- FFI backend isolation for test suite
|
|
45
|
+
- Added `bin/rspec-ffi` script to run FFI specs in isolation (before MRI backend loads)
|
|
46
|
+
- Added `spec/spec_ffi_helper.rb` for FFI-specific test configuration
|
|
47
|
+
- Updated Rakefile with `ffi_specs` and `remaining_specs` tasks
|
|
48
|
+
- The `:test` task now runs FFI specs first, then remaining specs
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- ast-merge v4.0.2
|
|
53
|
+
- Includes Ast::Merge::EmitterBase
|
|
54
|
+
- tree_haver v5.0.1
|
|
55
|
+
- Many Backend improvements
|
|
56
|
+
- Many error handling improvements
|
|
57
|
+
- **Simplified dependency_tags.rb**: Removed redundant debug code
|
|
58
|
+
- Removed `JSON_MERGE_DEBUG` env var handling (use `TREE_HAVER_DEBUG` instead)
|
|
59
|
+
- tree_haver's debug output now respects blocked backends via `compute_blocked_backends`
|
|
60
|
+
- Avoids accidentally loading MRI backend during FFI-only test runs
|
|
61
|
+
|
|
62
|
+
### Removed
|
|
63
|
+
|
|
64
|
+
- **Obsolete Tests**: Removed 3 obsolete integration tests
|
|
65
|
+
- Tests for `add_node_to_result` and `add_wrapper_to_result` methods
|
|
66
|
+
- These methods don't exist in the `:batch` strategy (ConflictResolver now uses Emitter)
|
|
67
|
+
- Tests were for old `:node` strategy pattern
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **NodeWrapper signature tests**: Updated tests to expect `:root_object`/`:root_array` for root-level containers
|
|
72
|
+
- Root-level objects now correctly return `[:root_object, ...]` instead of `[:object, ...]`
|
|
73
|
+
- Root-level arrays now correctly return `[:root_array]` instead of `[:array, count]`
|
|
74
|
+
- Added `:parent` method stubs to mock node tests for `root_level_container?` compatibility
|
|
75
|
+
- **ConflictResolver#emit_node**: Fixed handling of pair nodes with object values
|
|
76
|
+
- When emitting a pair like `"features": {...}`, the value was treated as raw text
|
|
77
|
+
- Now correctly detects when a pair's value is an object container
|
|
78
|
+
- Recursively emits object structure using `emit_nested_object_start/end`
|
|
79
|
+
- Treats arrays as atomic values (emits as raw text)
|
|
80
|
+
- Prevents double key emission and invalid JSON output in nested merges
|
|
81
|
+
- **ConflictResolver#merge_matched_nodes_to_emitter**: Fixed array handling in merge logic
|
|
82
|
+
- Arrays are now treated atomically and replaced based on preference setting
|
|
83
|
+
- Only objects (not arrays) are recursively merged
|
|
84
|
+
- Fixes potential "expected object key, got number" errors when merging arrays
|
|
85
|
+
- Arrays like `[1,2,3]` are now correctly replaced with `[4,5]` based on preference
|
|
86
|
+
|
|
33
87
|
## [1.0.0] - 2026-01-06
|
|
34
88
|
|
|
35
89
|
- TAG: [v1.0.0][1.0.0t]
|
|
@@ -43,6 +97,8 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
43
97
|
|
|
44
98
|
### Security
|
|
45
99
|
|
|
46
|
-
[Unreleased]: https://github.com/kettle-rb/json-merge/compare/v1.
|
|
100
|
+
[Unreleased]: https://github.com/kettle-rb/json-merge/compare/v1.1.0...HEAD
|
|
101
|
+
[1.1.0]: https://github.com/kettle-rb/json-merge/compare/v1.0.0...v1.1.0
|
|
102
|
+
[1.1.0t]: https://github.com/kettle-rb/json-merge/releases/tag/v1.1.0
|
|
47
103
|
[1.0.0]: https://github.com/kettle-rb/json-merge/compare/f1cc25b1d9b79c598270e3aa203fa56787e6c6fc...v1.0.0
|
|
48
104
|
[1.0.0t]: https://github.com/kettle-rb/json-merge/tags/v1.0.0
|
data/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
| 📍 NOTE |
|
|
2
2
|
| --- |
|
|
3
|
-
| RubyGems (the [GitHub org]
|
|
4
|
-
| Ultimately [4 maintainers]
|
|
5
|
-
| It is a [complicated story]
|
|
6
|
-
| Simply put - there was active policy for adding or removing maintainers/owners of [rubygems]
|
|
7
|
-
| I'm adding notes like this to gems because I [don't condone theft]
|
|
3
|
+
| RubyGems (the [GitHub org][rubygems-org], not the website) [suffered][draper-security] a [hostile takeover][ellen-takeover] in September 2025. |
|
|
4
|
+
| Ultimately [4 maintainers][simi-removed] were [hard removed][martin-removed] and a reason has been given for only 1 of those, while 2 others resigned in protest. |
|
|
5
|
+
| It is a [complicated story][draper-takeover] which is difficult to [parse quickly][draper-lies]. |
|
|
6
|
+
| Simply put - there was active policy for adding or removing maintainers/owners of [rubygems][rubygems-maint-policy] and [bundler][bundler-maint-policy], and those [policies were not followed][policy-fail]. |
|
|
7
|
+
| I'm adding notes like this to gems because I [don't condone theft][draper-theft] of repositories or gems from their rightful owners. |
|
|
8
8
|
| If a similar theft happened with my repos/gems, I'd hope some would stand up for me. |
|
|
9
|
-
| Disenfranchised former-maintainers have started [gem.coop]
|
|
9
|
+
| Disenfranchised former-maintainers have started [gem.coop][gem-coop]. |
|
|
10
10
|
| Once available I will publish there exclusively; unless RubyCentral makes amends with the community. |
|
|
11
|
-
| The ["Technology for Humans: Joel Draper"]
|
|
12
|
-
| See [here]
|
|
13
|
-
| What I'm doing: A (WIP) proposal for [bundler/gem scopes]
|
|
11
|
+
| The ["Technology for Humans: Joel Draper"][reinteractive-podcast] podcast episode by [reinteractive][reinteractive] is the most cogent summary I'm aware of. |
|
|
12
|
+
| See [here][gem-naming], [here][gem-coop] and [here][martin-ann] for more info on what comes next. |
|
|
13
|
+
| What I'm doing: A (WIP) proposal for [bundler/gem scopes][gem-scopes], and a (WIP) proposal for a federated [gem server][gem-server]. |
|
|
14
14
|
|
|
15
15
|
[rubygems-org]: https://github.com/rubygems/
|
|
16
16
|
[draper-security]: https://joel.drapper.me/p/ruby-central-security-measures/
|
|
@@ -99,7 +99,7 @@ File.write("merged.json", result.to_json)
|
|
|
99
99
|
|
|
100
100
|
### The `*-merge` Gem Family
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
The `*-merge` gem family provides intelligent, AST-based merging for various file formats. At the foundation is [tree_haver][tree_haver], which provides a unified cross-Ruby parsing API that works seamlessly across MRI, JRuby, and TruffleRuby.
|
|
103
103
|
|
|
104
104
|
| Gem | Language<br>/ Format | Parser Backend(s) | Description |
|
|
105
105
|
|------------------------------------------|----------------------|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
|
|
@@ -117,12 +117,36 @@ This gem is part of a family of gems that provide intelligent merging for variou
|
|
|
117
117
|
| [rbs-merge][rbs-merge] | RBS | [tree-sitter-bash][ts-rbs] (via tree_haver), [RBS][rbs] (`rbs` std lib gem) | Smart merge for Ruby type signatures |
|
|
118
118
|
| [toml-merge][toml-merge] | TOML | [Citrus + toml-rb][toml-rb] (default, via tree_haver), [tree-sitter-toml][ts-toml] (via tree_haver) | Smart merge for TOML files |
|
|
119
119
|
|
|
120
|
+
#### Backend Platform Compatibility
|
|
121
|
+
|
|
122
|
+
tree_haver supports multiple parsing backends, but not all backends work on all Ruby platforms:
|
|
123
|
+
|
|
124
|
+
| Platform 👉️<br> TreeHaver Backend 👇️ | MRI | JRuby | TruffleRuby | Notes |
|
|
125
|
+
|------------------------------------------------|:---:|:-----:|:-----------:|-----------------------------------------------------|
|
|
126
|
+
| **MRI** ([ruby_tree_sitter][ruby_tree_sitter]) | ✅ | ❌ | ❌ | C extension, MRI only |
|
|
127
|
+
| **Rust** ([tree_stump][tree_stump]) | ✅ | ❌ | ❌ | Rust extension via magnus/rb-sys, MRI only |
|
|
128
|
+
| **FFI** | ✅ | ✅ | ❌ | TruffleRuby's FFI doesn't support `STRUCT_BY_VALUE` |
|
|
129
|
+
| **Java** ([jtreesitter][jtreesitter]) | ❌ | ✅ | ❌ | JRuby only, requires grammar JARs |
|
|
130
|
+
| **Prism** | ✅ | ✅ | ✅ | Ruby parsing, stdlib in Ruby 3.4+ |
|
|
131
|
+
| **Psych** | ✅ | ✅ | ✅ | YAML parsing, stdlib |
|
|
132
|
+
| **Citrus** | ✅ | ✅ | ✅ | Pure Ruby, no native dependencies |
|
|
133
|
+
| **Commonmarker** | ✅ | ❌ | ❓ | Rust extension for Markdown |
|
|
134
|
+
| **Markly** | ✅ | ❌ | ❓ | C extension for Markdown |
|
|
135
|
+
|
|
136
|
+
**Legend**: ✅ = Works, ❌ = Does not work, ❓ = Untested
|
|
137
|
+
|
|
138
|
+
**Why some backends don't work on certain platforms**:
|
|
139
|
+
|
|
140
|
+
- **JRuby**: Runs on the JVM; cannot load native C/Rust extensions (`.so` files)
|
|
141
|
+
- **TruffleRuby**: Has C API emulation via Sulong/LLVM, but it doesn't expose all MRI internals that native extensions require (e.g., `RBasic.flags`, `rb_gc_writebarrier`)
|
|
142
|
+
- **FFI on TruffleRuby**: TruffleRuby's FFI implementation doesn't support returning structs by value, which tree-sitter's C API requires
|
|
143
|
+
|
|
120
144
|
**Example implementations** for the gem templating use case:
|
|
121
145
|
|
|
122
|
-
| Gem
|
|
123
|
-
|
|
124
|
-
| [kettle-dev]
|
|
125
|
-
| [kettle-jem]
|
|
146
|
+
| Gem | Purpose | Description |
|
|
147
|
+
|--------------------------|-----------------|-----------------------------------------------|
|
|
148
|
+
| [kettle-dev][kettle-dev] | Gem Development | Gem templating tool using `*-merge` gems |
|
|
149
|
+
| [kettle-jem][kettle-jem] | Gem Templating | Gem template library with smart merge support |
|
|
126
150
|
|
|
127
151
|
[tree_haver]: https://github.com/kettle-rb/tree_haver
|
|
128
152
|
[ast-merge]: https://github.com/kettle-rb/ast-merge
|
|
@@ -139,22 +163,82 @@ This gem is part of a family of gems that provide intelligent merging for variou
|
|
|
139
163
|
[commonmarker-merge]: https://github.com/kettle-rb/commonmarker-merge
|
|
140
164
|
[kettle-dev]: https://github.com/kettle-rb/kettle-dev
|
|
141
165
|
[kettle-jem]: https://github.com/kettle-rb/kettle-jem
|
|
166
|
+
[tree_haver-gem]: https://bestgems.org/gems/tree_haver
|
|
167
|
+
[ast-merge-gem]: https://bestgems.org/gems/ast-merge
|
|
168
|
+
[prism-merge-gem]: https://bestgems.org/gems/prism-merge
|
|
169
|
+
[psych-merge-gem]: https://bestgems.org/gems/psych-merge
|
|
170
|
+
[json-merge-gem]: https://bestgems.org/gems/json-merge
|
|
171
|
+
[jsonc-merge-gem]: https://bestgems.org/gems/jsonc-merge
|
|
172
|
+
[bash-merge-gem]: https://bestgems.org/gems/bash-merge
|
|
173
|
+
[rbs-merge-gem]: https://bestgems.org/gems/rbs-merge
|
|
174
|
+
[dotenv-merge-gem]: https://bestgems.org/gems/dotenv-merge
|
|
175
|
+
[toml-merge-gem]: https://bestgems.org/gems/toml-merge
|
|
176
|
+
[markdown-merge-gem]: https://bestgems.org/gems/markdown-merge
|
|
177
|
+
[markly-merge-gem]: https://bestgems.org/gems/markly-merge
|
|
178
|
+
[commonmarker-merge-gem]: https://bestgems.org/gems/commonmarker-merge
|
|
179
|
+
[kettle-dev-gem]: https://bestgems.org/gems/kettle-dev
|
|
180
|
+
[kettle-jem-gem]: https://bestgems.org/gems/kettle-jem
|
|
181
|
+
[tree_haver-gem-i]: https://img.shields.io/gem/v/tree_haver.svg
|
|
182
|
+
[ast-merge-gem-i]: https://img.shields.io/gem/v/ast-merge.svg
|
|
183
|
+
[prism-merge-gem-i]: https://img.shields.io/gem/v/prism-merge.svg
|
|
184
|
+
[psych-merge-gem-i]: https://img.shields.io/gem/v/psych-merge.svg
|
|
185
|
+
[json-merge-gem-i]: https://img.shields.io/gem/v/json-merge.svg
|
|
186
|
+
[jsonc-merge-gem-i]: https://img.shields.io/gem/v/jsonc-merge.svg
|
|
187
|
+
[bash-merge-gem-i]: https://img.shields.io/gem/v/bash-merge.svg
|
|
188
|
+
[rbs-merge-gem-i]: https://img.shields.io/gem/v/rbs-merge.svg
|
|
189
|
+
[dotenv-merge-gem-i]: https://img.shields.io/gem/v/dotenv-merge.svg
|
|
190
|
+
[toml-merge-gem-i]: https://img.shields.io/gem/v/toml-merge.svg
|
|
191
|
+
[markdown-merge-gem-i]: https://img.shields.io/gem/v/markdown-merge.svg
|
|
192
|
+
[markly-merge-gem-i]: https://img.shields.io/gem/v/markly-merge.svg
|
|
193
|
+
[commonmarker-merge-gem-i]: https://img.shields.io/gem/v/commonmarker-merge.svg
|
|
194
|
+
[kettle-dev-gem-i]: https://img.shields.io/gem/v/kettle-dev.svg
|
|
195
|
+
[kettle-jem-gem-i]: https://img.shields.io/gem/v/kettle-jem.svg
|
|
196
|
+
[tree_haver-ci-i]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml/badge.svg
|
|
197
|
+
[ast-merge-ci-i]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml/badge.svg
|
|
198
|
+
[prism-merge-ci-i]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml/badge.svg
|
|
199
|
+
[psych-merge-ci-i]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml/badge.svg
|
|
200
|
+
[json-merge-ci-i]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml/badge.svg
|
|
201
|
+
[jsonc-merge-ci-i]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml/badge.svg
|
|
202
|
+
[bash-merge-ci-i]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml/badge.svg
|
|
203
|
+
[rbs-merge-ci-i]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml/badge.svg
|
|
204
|
+
[dotenv-merge-ci-i]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml/badge.svg
|
|
205
|
+
[toml-merge-ci-i]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml/badge.svg
|
|
206
|
+
[markdown-merge-ci-i]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml/badge.svg
|
|
207
|
+
[markly-merge-ci-i]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml/badge.svg
|
|
208
|
+
[commonmarker-merge-ci-i]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml/badge.svg
|
|
209
|
+
[kettle-dev-ci-i]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml/badge.svg
|
|
210
|
+
[kettle-jem-ci-i]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml/badge.svg
|
|
211
|
+
[tree_haver-ci]: https://github.com/kettle-rb/tree_haver/actions/workflows/current.yml
|
|
212
|
+
[ast-merge-ci]: https://github.com/kettle-rb/ast-merge/actions/workflows/current.yml
|
|
213
|
+
[prism-merge-ci]: https://github.com/kettle-rb/prism-merge/actions/workflows/current.yml
|
|
214
|
+
[psych-merge-ci]: https://github.com/kettle-rb/psych-merge/actions/workflows/current.yml
|
|
215
|
+
[json-merge-ci]: https://github.com/kettle-rb/json-merge/actions/workflows/current.yml
|
|
216
|
+
[jsonc-merge-ci]: https://github.com/kettle-rb/jsonc-merge/actions/workflows/current.yml
|
|
217
|
+
[bash-merge-ci]: https://github.com/kettle-rb/bash-merge/actions/workflows/current.yml
|
|
218
|
+
[rbs-merge-ci]: https://github.com/kettle-rb/rbs-merge/actions/workflows/current.yml
|
|
219
|
+
[dotenv-merge-ci]: https://github.com/kettle-rb/dotenv-merge/actions/workflows/current.yml
|
|
220
|
+
[toml-merge-ci]: https://github.com/kettle-rb/toml-merge/actions/workflows/current.yml
|
|
221
|
+
[markdown-merge-ci]: https://github.com/kettle-rb/markdown-merge/actions/workflows/current.yml
|
|
222
|
+
[markly-merge-ci]: https://github.com/kettle-rb/markly-merge/actions/workflows/current.yml
|
|
223
|
+
[commonmarker-merge-ci]: https://github.com/kettle-rb/commonmarker-merge/actions/workflows/current.yml
|
|
224
|
+
[kettle-dev-ci]: https://github.com/kettle-rb/kettle-dev/actions/workflows/current.yml
|
|
225
|
+
[kettle-jem-ci]: https://github.com/kettle-rb/kettle-jem/actions/workflows/current.yml
|
|
142
226
|
[prism]: https://github.com/ruby/prism
|
|
143
227
|
[psych]: https://github.com/ruby/psych
|
|
144
228
|
[ts-json]: https://github.com/tree-sitter/tree-sitter-json
|
|
229
|
+
[ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
|
|
145
230
|
[ts-bash]: https://github.com/tree-sitter/tree-sitter-bash
|
|
231
|
+
[ts-rbs]: https://github.com/joker1007/tree-sitter-rbs
|
|
146
232
|
[ts-toml]: https://github.com/tree-sitter-grammars/tree-sitter-toml
|
|
233
|
+
[dotenv]: https://github.com/bkeepers/dotenv
|
|
147
234
|
[rbs]: https://github.com/ruby/rbs
|
|
148
235
|
[toml-rb]: https://github.com/emancu/toml-rb
|
|
236
|
+
[toml]: https://github.com/jm/toml
|
|
149
237
|
[markly]: https://github.com/ioquatix/markly
|
|
150
238
|
[commonmarker]: https://github.com/gjtorikian/commonmarker
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
[ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
|
|
157
|
-
[dotenv]: https://github.com/bkeepers/dotenv
|
|
239
|
+
[ruby_tree_sitter]: https://github.com/Faveod/ruby-tree-sitter
|
|
240
|
+
[tree_stump]: https://github.com/joker1007/tree_stump
|
|
241
|
+
[jtreesitter]: https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter
|
|
158
242
|
|
|
159
243
|
## 💡 Info you can shake a stick at
|
|
160
244
|
|
|
@@ -939,7 +1023,7 @@ Thanks for RTFM. ☺️
|
|
|
939
1023
|
[📌gitmoji]: https://gitmoji.dev
|
|
940
1024
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
941
1025
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
942
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.
|
|
1026
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.615-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
943
1027
|
[🔐security]: SECURITY.md
|
|
944
1028
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
945
1029
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -959,8 +1043,3 @@ Thanks for RTFM. ☺️
|
|
|
959
1043
|
[💎appraisal2]: https://github.com/appraisal-rb/appraisal2
|
|
960
1044
|
[💎appraisal2-img]: https://img.shields.io/badge/appraised_by-appraisal2-34495e.svg?plastic&logo=ruby&logoColor=white
|
|
961
1045
|
[💎d-in-dvcs]: https://railsbling.com/posts/dvcs/put_the_d_in_dvcs/
|
|
962
|
-
|
|
963
|
-
The `*-merge` gem family provides intelligent, AST-based merging for various file formats. At the foundation is [tree\_haver](https://github.com/kettle-rb/tree_haver), which provides a unified cross-Ruby parsing API that works seamlessly across MRI, JRuby, and TruffleRuby.
|
|
964
|
-
|
|
965
|
-
[ts-jsonc]: https://gitlab.com/WhyNotHugo/tree-sitter-jsonc
|
|
966
|
-
[dotenv]: https://github.com/bkeepers/dotenv
|
|
@@ -30,6 +30,7 @@ module Json
|
|
|
30
30
|
match_refiner: match_refiner,
|
|
31
31
|
**options
|
|
32
32
|
)
|
|
33
|
+
@emitter = Emitter.new
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
protected
|
|
@@ -42,15 +43,25 @@ module Json
|
|
|
42
43
|
template_statements = @template_analysis.statements
|
|
43
44
|
dest_statements = @dest_analysis.statements
|
|
44
45
|
|
|
45
|
-
#
|
|
46
|
-
|
|
46
|
+
# Clear emitter for fresh merge
|
|
47
|
+
@emitter.clear
|
|
48
|
+
|
|
49
|
+
# Merge root-level statements via emitter
|
|
50
|
+
merge_node_lists_to_emitter(
|
|
47
51
|
template_statements,
|
|
48
52
|
dest_statements,
|
|
49
53
|
@template_analysis,
|
|
50
54
|
@dest_analysis,
|
|
51
|
-
result,
|
|
52
55
|
)
|
|
53
56
|
|
|
57
|
+
# Transfer emitter output to result
|
|
58
|
+
emitted_content = @emitter.to_s
|
|
59
|
+
unless emitted_content.empty?
|
|
60
|
+
emitted_content.lines.each do |line|
|
|
61
|
+
result.add_line(line.chomp, decision: MergeResult::DECISION_MERGED, source: :merged)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
54
65
|
DebugLogger.debug("Conflict resolution complete", {
|
|
55
66
|
template_statements: template_statements.size,
|
|
56
67
|
dest_statements: dest_statements.size,
|
|
@@ -61,13 +72,12 @@ module Json
|
|
|
61
72
|
|
|
62
73
|
private
|
|
63
74
|
|
|
64
|
-
# Recursively merge two lists of nodes
|
|
75
|
+
# Recursively merge two lists of nodes, emitting to emitter
|
|
65
76
|
# @param template_nodes [Array<NodeWrapper>] Template nodes
|
|
66
77
|
# @param dest_nodes [Array<NodeWrapper>] Destination nodes
|
|
67
78
|
# @param template_analysis [FileAnalysis] Template analysis for line access
|
|
68
79
|
# @param dest_analysis [FileAnalysis] Destination analysis for line access
|
|
69
|
-
|
|
70
|
-
def merge_node_lists(template_nodes, dest_nodes, template_analysis, dest_analysis, result)
|
|
80
|
+
def merge_node_lists_to_emitter(template_nodes, dest_nodes, template_analysis, dest_analysis)
|
|
71
81
|
# Build signature maps for matching
|
|
72
82
|
template_by_sig = build_signature_map(template_nodes, template_analysis)
|
|
73
83
|
dest_by_sig = build_signature_map(dest_nodes, dest_analysis)
|
|
@@ -84,20 +94,13 @@ module Json
|
|
|
84
94
|
dest_nodes.each do |dest_node|
|
|
85
95
|
dest_sig = dest_analysis.generate_signature(dest_node)
|
|
86
96
|
|
|
87
|
-
# Freeze blocks from destination are always preserved
|
|
88
|
-
if freeze_node?(dest_node)
|
|
89
|
-
add_node_to_result(dest_node, result, :destination, MergeResult::DECISION_FREEZE_BLOCK, dest_analysis)
|
|
90
|
-
processed_dest_sigs << dest_sig if dest_sig
|
|
91
|
-
next
|
|
92
|
-
end
|
|
93
|
-
|
|
94
97
|
# Check for signature match
|
|
95
98
|
if dest_sig && template_by_sig[dest_sig]
|
|
96
99
|
template_info = template_by_sig[dest_sig].first
|
|
97
100
|
template_node = template_info[:node]
|
|
98
101
|
|
|
99
102
|
# Both have this node - merge them (recursively if containers)
|
|
100
|
-
|
|
103
|
+
merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
101
104
|
|
|
102
105
|
processed_dest_sigs << dest_sig
|
|
103
106
|
processed_template_sigs << dest_sig
|
|
@@ -107,13 +110,13 @@ module Json
|
|
|
107
110
|
template_sig = template_analysis.generate_signature(template_node)
|
|
108
111
|
|
|
109
112
|
# Merge matched nodes
|
|
110
|
-
|
|
113
|
+
merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
111
114
|
|
|
112
115
|
processed_dest_sigs << dest_sig if dest_sig
|
|
113
116
|
processed_template_sigs << template_sig if template_sig
|
|
114
117
|
else
|
|
115
118
|
# Destination-only node - always keep
|
|
116
|
-
|
|
119
|
+
emit_node(dest_node, dest_analysis)
|
|
117
120
|
processed_dest_sigs << dest_sig if dest_sig
|
|
118
121
|
end
|
|
119
122
|
end
|
|
@@ -127,35 +130,146 @@ module Json
|
|
|
127
130
|
# Skip if already processed
|
|
128
131
|
next if template_sig && processed_template_sigs.include?(template_sig)
|
|
129
132
|
|
|
130
|
-
# Skip freeze blocks from template
|
|
131
|
-
next if freeze_node?(template_node)
|
|
132
|
-
|
|
133
133
|
# Add template-only node
|
|
134
|
-
|
|
134
|
+
emit_node(template_node, template_analysis)
|
|
135
135
|
processed_template_sigs << template_sig if template_sig
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
# Merge two matched nodes - for containers, recursively merge children
|
|
140
|
+
# Emits to emitter instead of result
|
|
140
141
|
# @param template_node [NodeWrapper] Template node
|
|
141
142
|
# @param dest_node [NodeWrapper] Destination node
|
|
142
143
|
# @param template_analysis [FileAnalysis] Template analysis
|
|
143
144
|
# @param dest_analysis [FileAnalysis] Destination analysis
|
|
144
|
-
|
|
145
|
-
def merge_matched_nodes(template_node, dest_node, template_analysis, dest_analysis, result)
|
|
145
|
+
def merge_matched_nodes_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
146
146
|
if dest_node.container? && template_node.container?
|
|
147
147
|
# Both are containers - recursively merge their children
|
|
148
|
-
|
|
148
|
+
merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
149
|
+
elsif dest_node.pair? && template_node.pair?
|
|
150
|
+
# Both are pairs - check if their values are OBJECTS (not arrays) that need recursive merge
|
|
151
|
+
template_value = template_node.value_node
|
|
152
|
+
dest_value = dest_node.value_node
|
|
153
|
+
|
|
154
|
+
# Only recursively merge if BOTH values are objects (not arrays)
|
|
155
|
+
# Arrays are replaced atomically based on preference
|
|
156
|
+
if template_value&.type == :object && dest_value&.type == :object &&
|
|
157
|
+
template_value.container? && dest_value.container?
|
|
158
|
+
# Both values are objects - recursively merge
|
|
159
|
+
@emitter.emit_nested_object_start(dest_node.key_name)
|
|
160
|
+
|
|
161
|
+
# Recursively merge the value objects
|
|
162
|
+
merge_node_lists_to_emitter(
|
|
163
|
+
template_value.mergeable_children,
|
|
164
|
+
dest_value.mergeable_children,
|
|
165
|
+
template_analysis,
|
|
166
|
+
dest_analysis,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Emit closing brace
|
|
170
|
+
@emitter.emit_nested_object_end
|
|
171
|
+
elsif @preference == :destination
|
|
172
|
+
# Values are not both objects, or one/both are arrays - use preference and emit
|
|
173
|
+
# Arrays are always replaced, not merged
|
|
174
|
+
emit_node(dest_node, dest_analysis)
|
|
175
|
+
else
|
|
176
|
+
emit_node(template_node, template_analysis)
|
|
177
|
+
end
|
|
149
178
|
elsif @preference == :destination
|
|
150
179
|
# Leaf nodes or mismatched types - use preference
|
|
151
|
-
|
|
180
|
+
emit_node(dest_node, dest_analysis)
|
|
152
181
|
else
|
|
153
|
-
|
|
182
|
+
emit_node(template_node, template_analysis)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Merge container nodes by emitting via emitter
|
|
187
|
+
# @param template_node [NodeWrapper] Template container node
|
|
188
|
+
# @param dest_node [NodeWrapper] Destination container node
|
|
189
|
+
# @param template_analysis [FileAnalysis] Template analysis
|
|
190
|
+
# @param dest_analysis [FileAnalysis] Destination analysis
|
|
191
|
+
def merge_container_to_emitter(template_node, dest_node, template_analysis, dest_analysis)
|
|
192
|
+
# Emit opening bracket
|
|
193
|
+
if dest_node.object?
|
|
194
|
+
@emitter.emit_object_start
|
|
195
|
+
elsif dest_node.array?
|
|
196
|
+
@emitter.emit_array_start
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Recursively merge the children
|
|
200
|
+
template_children = template_node.mergeable_children
|
|
201
|
+
dest_children = dest_node.mergeable_children
|
|
202
|
+
|
|
203
|
+
merge_node_lists_to_emitter(
|
|
204
|
+
template_children,
|
|
205
|
+
dest_children,
|
|
206
|
+
template_analysis,
|
|
207
|
+
dest_analysis,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Emit closing bracket
|
|
211
|
+
if dest_node.object?
|
|
212
|
+
@emitter.emit_object_end
|
|
213
|
+
elsif dest_node.array?
|
|
214
|
+
@emitter.emit_array_end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Emit a single node to the emitter
|
|
219
|
+
# @param node [NodeWrapper] Node to emit
|
|
220
|
+
# @param analysis [FileAnalysis] Analysis for accessing source
|
|
221
|
+
def emit_node(node, analysis)
|
|
222
|
+
# Emit the node content
|
|
223
|
+
if node.pair?
|
|
224
|
+
# Emit as pair
|
|
225
|
+
key = node.key_name
|
|
226
|
+
value_node = node.value_node
|
|
227
|
+
|
|
228
|
+
if value_node
|
|
229
|
+
# Check if value is an object (not array) and needs recursive emission
|
|
230
|
+
if value_node.type == :object && value_node.container?
|
|
231
|
+
# Object value - emit structure recursively
|
|
232
|
+
@emitter.emit_nested_object_start(key)
|
|
233
|
+
# Recursively emit object children
|
|
234
|
+
value_node.mergeable_children.each do |child|
|
|
235
|
+
emit_node(child, analysis)
|
|
236
|
+
end
|
|
237
|
+
@emitter.emit_nested_object_end
|
|
238
|
+
else
|
|
239
|
+
# Leaf value or array - get its text and emit as simple pair
|
|
240
|
+
# Arrays are emitted as raw text (not recursively)
|
|
241
|
+
value_text = if value_node.start_line == value_node.end_line
|
|
242
|
+
value_node.text
|
|
243
|
+
else
|
|
244
|
+
# Multi-line value - get all lines
|
|
245
|
+
lines = []
|
|
246
|
+
(value_node.start_line..value_node.end_line).each do |ln|
|
|
247
|
+
lines << analysis.line_at(ln)
|
|
248
|
+
end
|
|
249
|
+
lines.join("\n")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
@emitter.emit_pair(key, value_text) if key && value_text
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
elsif node.start_line && node.end_line
|
|
256
|
+
# Emit raw content for non-pair nodes
|
|
257
|
+
if node.start_line == node.end_line
|
|
258
|
+
# Single line - add directly
|
|
259
|
+
@emitter.lines << node.text
|
|
260
|
+
else
|
|
261
|
+
# Multi-line - collect and emit
|
|
262
|
+
lines = []
|
|
263
|
+
(node.start_line..node.end_line).each do |ln|
|
|
264
|
+
line = analysis.line_at(ln)
|
|
265
|
+
lines << line if line
|
|
266
|
+
end
|
|
267
|
+
@emitter.emit_raw_lines(lines)
|
|
268
|
+
end
|
|
154
269
|
end
|
|
155
270
|
end
|
|
156
271
|
|
|
157
|
-
# Build a map of refined matches from template node to destination node
|
|
158
|
-
# Uses the match_refiner to find additional pairings for nodes that didn't match by signature.
|
|
272
|
+
# Build a map of refined matches from template node to destination node
|
|
159
273
|
# @param template_nodes [Array<NodeWrapper>] Template nodes
|
|
160
274
|
# @param dest_nodes [Array<NodeWrapper>] Destination nodes
|
|
161
275
|
# @param template_by_sig [Hash] Template signature map
|
|
@@ -188,62 +302,6 @@ module Json
|
|
|
188
302
|
h[match.template_node] = match.dest_node
|
|
189
303
|
end
|
|
190
304
|
end
|
|
191
|
-
|
|
192
|
-
# Merge two container nodes by emitting opening, recursively merging children, then closing
|
|
193
|
-
# @param template_node [NodeWrapper] Template container node
|
|
194
|
-
# @param dest_node [NodeWrapper] Destination container node
|
|
195
|
-
# @param template_analysis [FileAnalysis] Template analysis
|
|
196
|
-
# @param dest_analysis [FileAnalysis] Destination analysis
|
|
197
|
-
# @param result [MergeResult] Result to populate
|
|
198
|
-
def merge_container_nodes(template_node, dest_node, template_analysis, dest_analysis, result)
|
|
199
|
-
# Use destination's opening line (or template if dest doesn't have one)
|
|
200
|
-
opening = dest_node.opening_line || template_node.opening_line
|
|
201
|
-
result.add_line(opening, decision: MergeResult::DECISION_MERGED, source: :merged) if opening
|
|
202
|
-
|
|
203
|
-
# Recursively merge the children
|
|
204
|
-
template_children = template_node.mergeable_children
|
|
205
|
-
dest_children = dest_node.mergeable_children
|
|
206
|
-
|
|
207
|
-
merge_node_lists(
|
|
208
|
-
template_children,
|
|
209
|
-
dest_children,
|
|
210
|
-
template_analysis,
|
|
211
|
-
dest_analysis,
|
|
212
|
-
result,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Use destination's closing line (or template if dest doesn't have one)
|
|
216
|
-
closing = dest_node.closing_line || template_node.closing_line
|
|
217
|
-
result.add_line(closing, decision: MergeResult::DECISION_MERGED, source: :merged) if closing
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Add a node to the result (non-container or leaf node)
|
|
221
|
-
# @param node [NodeWrapper] Node to add
|
|
222
|
-
# @param result [MergeResult] Result to populate
|
|
223
|
-
# @param source [Symbol] :template or :destination
|
|
224
|
-
# @param decision [String] Decision constant
|
|
225
|
-
# @param analysis [FileAnalysis] Analysis for line access
|
|
226
|
-
def add_node_to_result(node, result, source, decision, analysis)
|
|
227
|
-
if freeze_node?(node)
|
|
228
|
-
result.add_freeze_block(node)
|
|
229
|
-
elsif node.is_a?(NodeWrapper)
|
|
230
|
-
add_wrapper_to_result(node, result, source, decision, analysis)
|
|
231
|
-
else
|
|
232
|
-
DebugLogger.debug("Unknown node type", {node_type: node.class.name})
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def add_wrapper_to_result(wrapper, result, source, decision, analysis)
|
|
237
|
-
return unless wrapper.start_line && wrapper.end_line
|
|
238
|
-
|
|
239
|
-
# Add the node content line by line
|
|
240
|
-
(wrapper.start_line..wrapper.end_line).each do |line_num|
|
|
241
|
-
line = analysis.line_at(line_num)
|
|
242
|
-
next unless line
|
|
243
|
-
|
|
244
|
-
result.add_line(line.chomp, decision: decision, source: source, original_line: line_num)
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
305
|
end
|
|
248
306
|
end
|
|
249
307
|
end
|
data/lib/json/merge/emitter.rb
CHANGED
|
@@ -6,31 +6,38 @@ module Json
|
|
|
6
6
|
# This class provides utilities for emitting JSON while maintaining
|
|
7
7
|
# the original structure, comments, and style choices.
|
|
8
8
|
#
|
|
9
|
+
# Inherits common emitter functionality from Ast::Merge::EmitterBase.
|
|
10
|
+
#
|
|
9
11
|
# @example Basic usage
|
|
10
12
|
# emitter = Emitter.new
|
|
11
13
|
# emitter.emit_object_start
|
|
12
14
|
# emitter.emit_pair("key", '"value"')
|
|
13
15
|
# emitter.emit_object_end
|
|
14
|
-
class Emitter
|
|
15
|
-
# @return [
|
|
16
|
-
attr_reader :
|
|
17
|
-
|
|
18
|
-
# @return [Integer] Current indentation level
|
|
19
|
-
attr_reader :indent_level
|
|
16
|
+
class Emitter < Ast::Merge::EmitterBase
|
|
17
|
+
# @return [Boolean] Whether next item needs a comma
|
|
18
|
+
attr_reader :needs_comma
|
|
20
19
|
|
|
21
|
-
#
|
|
22
|
-
|
|
20
|
+
# Initialize subclass-specific state (comma tracking for JSON)
|
|
21
|
+
def initialize_subclass_state(**options)
|
|
22
|
+
@needs_comma = false
|
|
23
|
+
end
|
|
23
24
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
# @param indent_size [Integer] Number of spaces per indent level
|
|
27
|
-
def initialize(indent_size: 2)
|
|
28
|
-
@lines = []
|
|
29
|
-
@indent_level = 0
|
|
30
|
-
@indent_size = indent_size
|
|
25
|
+
# Clear subclass-specific state
|
|
26
|
+
def clear_subclass_state
|
|
31
27
|
@needs_comma = false
|
|
32
28
|
end
|
|
33
29
|
|
|
30
|
+
# Emit a tracked comment from CommentTracker
|
|
31
|
+
# @param comment [Hash] Comment with :text, :indent, :block
|
|
32
|
+
def emit_tracked_comment(comment)
|
|
33
|
+
indent = " " * (comment[:indent] || 0)
|
|
34
|
+
@lines << if comment[:block]
|
|
35
|
+
"#{indent}/* #{comment[:text]} */"
|
|
36
|
+
else
|
|
37
|
+
"#{indent}// #{comment[:text]}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
# Emit a single-line comment
|
|
35
42
|
#
|
|
36
43
|
# @param text [String] Comment text (without //)
|
|
@@ -53,36 +60,17 @@ module Json
|
|
|
53
60
|
@lines << "#{current_indent}/* #{text} */"
|
|
54
61
|
end
|
|
55
62
|
|
|
56
|
-
# Emit leading comments
|
|
57
|
-
#
|
|
58
|
-
# @param comments [Array<Hash>] Comment hashes from CommentTracker
|
|
59
|
-
def emit_leading_comments(comments)
|
|
60
|
-
comments.each do |comment|
|
|
61
|
-
indent = " " * (comment[:indent] || 0)
|
|
62
|
-
@lines << if comment[:block]
|
|
63
|
-
"#{indent}/* #{comment[:text]} */"
|
|
64
|
-
else
|
|
65
|
-
"#{indent}// #{comment[:text]}"
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Emit a blank line
|
|
71
|
-
def emit_blank_line
|
|
72
|
-
@lines << ""
|
|
73
|
-
end
|
|
74
|
-
|
|
75
63
|
# Emit object start
|
|
76
64
|
def emit_object_start
|
|
77
65
|
add_comma_if_needed
|
|
78
66
|
@lines << "#{current_indent}{"
|
|
79
|
-
|
|
67
|
+
indent
|
|
80
68
|
@needs_comma = false
|
|
81
69
|
end
|
|
82
70
|
|
|
83
71
|
# Emit object end
|
|
84
72
|
def emit_object_end
|
|
85
|
-
|
|
73
|
+
dedent
|
|
86
74
|
@lines << "#{current_indent}}"
|
|
87
75
|
@needs_comma = true
|
|
88
76
|
end
|
|
@@ -97,13 +85,13 @@ module Json
|
|
|
97
85
|
else
|
|
98
86
|
"#{current_indent}["
|
|
99
87
|
end
|
|
100
|
-
|
|
88
|
+
indent
|
|
101
89
|
@needs_comma = false
|
|
102
90
|
end
|
|
103
91
|
|
|
104
92
|
# Emit array end
|
|
105
93
|
def emit_array_end
|
|
106
|
-
|
|
94
|
+
dedent
|
|
107
95
|
@lines << "#{current_indent}]"
|
|
108
96
|
@needs_comma = true
|
|
109
97
|
end
|
|
@@ -133,39 +121,31 @@ module Json
|
|
|
133
121
|
@needs_comma = true
|
|
134
122
|
end
|
|
135
123
|
|
|
136
|
-
# Emit
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
124
|
+
# Emit a key with opening brace for nested object
|
|
125
|
+
# @param key [String] Key name
|
|
126
|
+
def emit_nested_object_start(key)
|
|
127
|
+
add_comma_if_needed
|
|
128
|
+
@lines << "#{current_indent}\"#{key}\": {"
|
|
129
|
+
indent
|
|
130
|
+
@needs_comma = false
|
|
141
131
|
end
|
|
142
132
|
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
content += "\n" unless content.empty? || content.end_with?("\n")
|
|
149
|
-
content
|
|
133
|
+
# Emit closing brace for nested object
|
|
134
|
+
def emit_nested_object_end
|
|
135
|
+
dedent
|
|
136
|
+
@lines << "#{current_indent}}"
|
|
137
|
+
@needs_comma = true
|
|
150
138
|
end
|
|
151
139
|
|
|
152
|
-
#
|
|
140
|
+
# Get the output as a JSON string
|
|
141
|
+
#
|
|
153
142
|
# @return [String]
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Clear the output
|
|
157
|
-
def clear
|
|
158
|
-
@lines = []
|
|
159
|
-
@indent_level = 0
|
|
160
|
-
@needs_comma = false
|
|
143
|
+
def to_json
|
|
144
|
+
to_s
|
|
161
145
|
end
|
|
162
146
|
|
|
163
147
|
private
|
|
164
148
|
|
|
165
|
-
def current_indent
|
|
166
|
-
" " * (@indent_level * @indent_size)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
149
|
def add_comma_if_needed
|
|
170
150
|
return unless @needs_comma && @lines.any?
|
|
171
151
|
|
|
@@ -74,6 +74,7 @@ module Json
|
|
|
74
74
|
|
|
75
75
|
# In JSON tree-sitter, pair has key and value children
|
|
76
76
|
key_node = find_child_by_field("key")
|
|
77
|
+
|
|
77
78
|
return unless key_node
|
|
78
79
|
|
|
79
80
|
# Key is typically a string, extract its content without quotes using byte positions
|
|
@@ -88,6 +89,7 @@ module Json
|
|
|
88
89
|
return unless pair?
|
|
89
90
|
|
|
90
91
|
value = find_child_by_field("value")
|
|
92
|
+
|
|
91
93
|
return unless value
|
|
92
94
|
|
|
93
95
|
NodeWrapper.new(value, lines: @lines, source: @source)
|
|
@@ -148,6 +150,19 @@ module Json
|
|
|
148
150
|
object? || array?
|
|
149
151
|
end
|
|
150
152
|
|
|
153
|
+
# Check if this is a root-level container (direct child of document)
|
|
154
|
+
# Root-level containers get a generic signature so they always match.
|
|
155
|
+
# @return [Boolean]
|
|
156
|
+
def root_level_container?
|
|
157
|
+
return false unless container?
|
|
158
|
+
|
|
159
|
+
# Check if parent is a document node
|
|
160
|
+
parent_node = @node.parent if @node.respond_to?(:parent)
|
|
161
|
+
return false unless parent_node
|
|
162
|
+
|
|
163
|
+
parent_node.type.to_s == "document"
|
|
164
|
+
end
|
|
165
|
+
|
|
151
166
|
# Get the opening line for a container node (the line with { or [)
|
|
152
167
|
# Returns the full line content including any leading whitespace
|
|
153
168
|
# @return [String, nil]
|
|
@@ -226,14 +241,26 @@ module Json
|
|
|
226
241
|
child_type = child&.type&.to_s
|
|
227
242
|
[:document, child_type]
|
|
228
243
|
when "object"
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
244
|
+
# For root-level objects (direct child of document), use a generic signature
|
|
245
|
+
# that always matches so merging happens at the pair level.
|
|
246
|
+
# This is critical for JSON merging - there's typically only one root object/array.
|
|
247
|
+
if root_level_container?
|
|
248
|
+
[:root_object]
|
|
249
|
+
else
|
|
250
|
+
# Nested objects identified by their keys
|
|
251
|
+
keys = extract_object_keys(node)
|
|
252
|
+
[:object, keys.sort]
|
|
253
|
+
end
|
|
232
254
|
when "array"
|
|
233
|
-
#
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
255
|
+
# For root-level arrays (direct child of document), use a generic signature
|
|
256
|
+
if root_level_container?
|
|
257
|
+
[:root_array]
|
|
258
|
+
else
|
|
259
|
+
# Nested arrays identified by their length and first few elements
|
|
260
|
+
elements_count = 0
|
|
261
|
+
node.each { |c| elements_count += 1 unless %w[comment , \[ \]].include?(c.type.to_s) }
|
|
262
|
+
[:array, elements_count]
|
|
263
|
+
end
|
|
237
264
|
when "pair"
|
|
238
265
|
# Pairs identified by their key name
|
|
239
266
|
key = key_name
|
|
@@ -267,6 +294,7 @@ module Json
|
|
|
267
294
|
next unless child.type.to_s == "pair"
|
|
268
295
|
|
|
269
296
|
key_node = child.respond_to?(:child_by_field_name) ? child.child_by_field_name("key") : nil
|
|
297
|
+
|
|
270
298
|
next unless key_node
|
|
271
299
|
|
|
272
300
|
key_text = node_text(key_node)&.gsub(/\A"|"\z/, "")
|
data/lib/json/merge/version.rb
CHANGED
data/lib/json/merge.rb
CHANGED
|
@@ -108,6 +108,18 @@ module Json
|
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# Register with ast-merge's MergeGemRegistry for RSpec dependency tags
|
|
112
|
+
# Only register if MergeGemRegistry is loaded (i.e., in test environment)
|
|
113
|
+
if defined?(Ast::Merge::RSpec::MergeGemRegistry)
|
|
114
|
+
Ast::Merge::RSpec::MergeGemRegistry.register(
|
|
115
|
+
:json_merge,
|
|
116
|
+
require_path: "json/merge",
|
|
117
|
+
merger_class: "Json::Merge::SmartMerger",
|
|
118
|
+
test_source: '{"key": "value"}',
|
|
119
|
+
category: :data,
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
111
123
|
Json::Merge::Version.class_eval do
|
|
112
124
|
extend VersionGem::Basic
|
|
113
125
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: json-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -43,40 +43,40 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
46
|
+
version: '5.0'
|
|
47
47
|
- - ">="
|
|
48
48
|
- !ruby/object:Gem::Version
|
|
49
|
-
version:
|
|
49
|
+
version: 5.0.1
|
|
50
50
|
type: :runtime
|
|
51
51
|
prerelease: false
|
|
52
52
|
version_requirements: !ruby/object:Gem::Requirement
|
|
53
53
|
requirements:
|
|
54
54
|
- - "~>"
|
|
55
55
|
- !ruby/object:Gem::Version
|
|
56
|
-
version: '
|
|
56
|
+
version: '5.0'
|
|
57
57
|
- - ">="
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
|
-
version:
|
|
59
|
+
version: 5.0.1
|
|
60
60
|
- !ruby/object:Gem::Dependency
|
|
61
61
|
name: ast-merge
|
|
62
62
|
requirement: !ruby/object:Gem::Requirement
|
|
63
63
|
requirements:
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
|
-
version: '
|
|
66
|
+
version: '4.0'
|
|
67
67
|
- - ">="
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
|
-
version:
|
|
69
|
+
version: 4.0.2
|
|
70
70
|
type: :runtime
|
|
71
71
|
prerelease: false
|
|
72
72
|
version_requirements: !ruby/object:Gem::Requirement
|
|
73
73
|
requirements:
|
|
74
74
|
- - "~>"
|
|
75
75
|
- !ruby/object:Gem::Version
|
|
76
|
-
version: '
|
|
76
|
+
version: '4.0'
|
|
77
77
|
- - ">="
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version:
|
|
79
|
+
version: 4.0.2
|
|
80
80
|
- !ruby/object:Gem::Dependency
|
|
81
81
|
name: version_gem
|
|
82
82
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -294,10 +294,10 @@ licenses:
|
|
|
294
294
|
- MIT
|
|
295
295
|
metadata:
|
|
296
296
|
homepage_uri: https://json-merge.galtzo.com/
|
|
297
|
-
source_code_uri: https://github.com/kettle-rb/json-merge/tree/v1.
|
|
298
|
-
changelog_uri: https://github.com/kettle-rb/json-merge/blob/v1.
|
|
297
|
+
source_code_uri: https://github.com/kettle-rb/json-merge/tree/v1.1.0
|
|
298
|
+
changelog_uri: https://github.com/kettle-rb/json-merge/blob/v1.1.0/CHANGELOG.md
|
|
299
299
|
bug_tracker_uri: https://github.com/kettle-rb/json-merge/issues
|
|
300
|
-
documentation_uri: https://www.rubydoc.info/gems/json-merge/1.
|
|
300
|
+
documentation_uri: https://www.rubydoc.info/gems/json-merge/1.1.0
|
|
301
301
|
funding_uri: https://github.com/sponsors/pboling
|
|
302
302
|
wiki_uri: https://github.com/kettle-rb/json-merge/wiki
|
|
303
303
|
news_uri: https://www.railsbling.com/tags/json-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|