json-merge 1.1.2 → 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.
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Json
4
4
  module Merge
5
- # Version information for Json::Merge
5
+ # Version namespace for this gem.
6
6
  module Version
7
- # Current version of the json-merge gem
8
- VERSION = "1.1.2"
7
+ # Current gem version.
8
+ VERSION = '7.1.1'
9
9
  end
10
- VERSION = Version::VERSION # traditional location
10
+ # Current gem version exposed at the traditional constant location.
11
+ VERSION = Version::VERSION # Traditional Constant Location
11
12
  end
12
13
  end
data/lib/json/merge.rb CHANGED
@@ -1,125 +1,522 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # std libs
4
- require "set"
5
-
6
- # External gems
7
- # TreeHaver provides a unified cross-Ruby interface to tree-sitter.
8
- # It handles grammar discovery and backend selection automatically
9
- # via parser_for(:json). No manual registration needed.
10
- require "tree_haver"
11
- require "version_gem"
12
-
13
- # Shared merge infrastructure
14
- require "ast/merge"
15
-
16
- # This gem
17
- require_relative "merge/version"
18
-
19
- # Json::Merge provides a JSON file smart merge system using tree-sitter AST analysis.
20
- # It intelligently merges template and destination JSON files by identifying matching
21
- # keys and resolving differences using structural signatures.
22
- #
23
- # For JSONC (JSON with Comments) support, see the jsonc-merge gem which handles
24
- # configuration files that include comments
25
- # (like devcontainer.json, tsconfig.json, VS Code settings, etc.).
26
- #
27
- # @example Basic usage
28
- # template = File.read("template.json")
29
- # destination = File.read("destination.json")
30
- # merger = Json::Merge::SmartMerger.new(template, destination)
31
- # result = merger.merge
32
- #
33
- # @example With debug information
34
- # merger = Json::Merge::SmartMerger.new(template, destination)
35
- # debug_result = merger.merge_with_debug
36
- # puts debug_result[:content]
37
- # puts debug_result[:statistics]
3
+ require 'json'
4
+ require 'tree_haver'
5
+ require 'ast/merge'
6
+ require_relative 'merge/version'
7
+
38
8
  module Json
39
- # Smart merge system for JSON files using tree-sitter AST analysis.
40
- # Provides intelligent merging by understanding JSON structure
41
- # rather than treating files as plain text.
42
- #
43
- # For JSONC (JSON with Comments) support, use the jsonc-merge gem instead.
44
- #
45
- # @see SmartMerger Main entry point for merge operations
46
- # @see FileAnalysis Analyzes JSON structure
47
- # @see ConflictResolver Resolves content conflicts
48
9
  module Merge
49
- # Base error class for Json::Merge
50
- # Inherits from Ast::Merge::Error for consistency across merge gems.
10
+ PACKAGE_NAME = 'json-merge'
11
+ DESTINATION_WINS_ARRAY_POLICY = {
12
+ surface: 'array',
13
+ name: 'destination_wins_array'
14
+ }.freeze
15
+ TREE_SITTER_BACKEND = TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
16
+ BACKEND_REGISTRY = Struct.new(:registered, :mutex).new(false, Mutex.new)
17
+
51
18
  class Error < Ast::Merge::Error; end
52
19
 
53
- # Raised when a JSON file has parsing errors.
54
- # Inherits from Ast::Merge::ParseError for consistency across merge gems.
55
- #
56
- # @example Handling parse errors
57
- # begin
58
- # analysis = FileAnalysis.new(json_content)
59
- # rescue ParseError => e
60
- # puts "JSON syntax error: #{e.message}"
61
- # e.errors.each { |error| puts " #{error}" }
62
- # end
63
20
  class ParseError < Ast::Merge::ParseError
64
- # @param message [String, nil] Error message (auto-generated if nil)
65
- # @param content [String, nil] The JSON source that failed to parse
66
- # @param errors [Array] Parse errors from tree-sitter
67
21
  def initialize(message = nil, content: nil, errors: [])
68
22
  super(message, errors: errors, content: content)
69
23
  end
70
24
  end
71
25
 
72
- # Raised when the template file has syntax errors.
73
- #
74
- # @example Handling template parse errors
75
- # begin
76
- # merger = SmartMerger.new(template, destination)
77
- # result = merger.merge
78
- # rescue TemplateParseError => e
79
- # puts "Template syntax error: #{e.message}"
80
- # e.errors.each do |error|
81
- # puts " #{error.message}"
82
- # end
83
- # end
84
26
  class TemplateParseError < ParseError; end
85
-
86
- # Raised when the destination file has syntax errors.
87
- #
88
- # @example Handling destination parse errors
89
- # begin
90
- # merger = SmartMerger.new(template, destination)
91
- # result = merger.merge
92
- # rescue DestinationParseError => e
93
- # puts "Destination syntax error: #{e.message}"
94
- # e.errors.each do |error|
95
- # puts " #{error.message}"
96
- # end
97
- # end
98
27
  class DestinationParseError < ParseError; end
28
+ class JsoncDialectError < ParseError; end
29
+ class CorruptionDetectedError < Error; end
30
+
31
+ autoload :CommentTracker, 'json/merge/comment_tracker'
32
+ autoload :DebugLogger, 'json/merge/debug_logger'
33
+ autoload :Emitter, 'json/merge/emitter'
34
+ autoload :FileAnalysis, 'json/merge/file_analysis'
35
+ autoload :FreezeNode, 'json/merge/freeze_node'
36
+ autoload :MergeResult, 'json/merge/merge_result'
37
+ autoload :NodeWrapper, 'json/merge/node_wrapper'
38
+ autoload :ConflictResolver, 'json/merge/conflict_resolver'
39
+ autoload :SmartMerger, 'json/merge/smart_merger'
40
+ autoload :ObjectMatchRefiner, 'json/merge/object_match_refiner'
41
+
42
+ class << self
43
+ def register_backend!
44
+ BACKEND_REGISTRY.mutex.synchronize do
45
+ return if BACKEND_REGISTRY.registered
46
+
47
+ TreeHaver::BackendRegistry.register(TREE_SITTER_BACKEND)
48
+
49
+ grammar_finder = TreeHaver::GrammarFinder.new(:json)
50
+ grammar_finder.register! if grammar_finder.available?
51
+
52
+ json5_grammar_finder = TreeHaver::GrammarFinder.new(:json5)
53
+ json5_grammar_finder.register! if json5_grammar_finder.available?
54
+
55
+ BACKEND_REGISTRY.registered = true
56
+ end
57
+ end
58
+ end
59
+
60
+ module_function
61
+
62
+ def json_feature_profile
63
+ {
64
+ family: 'json',
65
+ supported_dialects: %w[json jsonc json5],
66
+ supported_policies: [DESTINATION_WINS_ARRAY_POLICY]
67
+ }
68
+ end
69
+
70
+ def available_json_backends
71
+ json_backend_available_for_analysis?(TREE_SITTER_BACKEND.id) ? [TREE_SITTER_BACKEND] : []
72
+ end
73
+
74
+ def json_backend_feature_profile(backend: nil)
75
+ requested = requested_json_backend_id(backend)
76
+ unless available_json_backends.any? { |backend_ref| backend_ref.id == requested }
77
+ return unsupported_feature_result("Unsupported JSON backend #{requested}.")
78
+ end
79
+
80
+ json_feature_profile.merge(
81
+ backend: requested,
82
+ backend_ref: TREE_SITTER_BACKEND.to_h
83
+ )
84
+ end
85
+
86
+ def json_plan_context(backend: nil)
87
+ profile = json_backend_feature_profile(backend: backend)
88
+ return profile if profile[:ok] == false
89
+
90
+ {
91
+ family_profile: json_feature_profile,
92
+ feature_profile: {
93
+ backend: profile[:backend],
94
+ supports_dialects: true,
95
+ supported_policies: profile[:supported_policies]
96
+ }
97
+ }
98
+ end
99
+
100
+ def parse_json(source, dialect, backend: nil)
101
+ dialect = normalize_json_dialect(dialect)
102
+ requested = requested_json_backend_id(backend)
103
+ unless json_backend_available_for_analysis?(requested, dialect: dialect)
104
+ return unsupported_feature_result("Unsupported JSON backend #{requested}.")
105
+ end
106
+
107
+ if dialect == :json && detect_trailing_comma(source)
108
+ return parse_failure("Trailing commas are not supported for #{dialect}.")
109
+ end
110
+ if dialect == :json && detect_json_comments(source)
111
+ return parse_failure("Comments are not supported for #{dialect}.")
112
+ end
113
+
114
+ register_backend!
115
+ analysis = TreeHaver.with_backend(requested) { FileAnalysis.new(source, dialect: dialect) }
116
+ unless analysis.valid?
117
+ return parse_failure(analysis.errors.map do |error|
118
+ error.respond_to?(:message) ? error.message : error.inspect
119
+ end.join(', '))
120
+ end
121
+
122
+ {
123
+ ok: true,
124
+ diagnostics: [],
125
+ analysis: {
126
+ dialect: dialect.to_s,
127
+ allows_comments: %i[jsonc json5].include?(dialect),
128
+ root_kind: json_analysis_root_kind(analysis),
129
+ owners: collect_file_analysis_owners(analysis)
130
+ },
131
+ policies: []
132
+ }
133
+ rescue TreeHaver::Error, StandardError => e
134
+ parse_failure(e.message)
135
+ end
136
+
137
+ def match_json_owners(template, destination)
138
+ Ast::Merge::OwnerSelection.match_by_path(template, destination)
139
+ end
140
+
141
+ def merge_json(template_source, destination_source, dialect, backend: nil)
142
+ dialect = normalize_json_dialect(dialect)
143
+ requested = requested_json_backend_id(backend)
144
+ unless json_backend_available_for_analysis?(requested, dialect: dialect)
145
+ return unsupported_feature_result("Unsupported JSON backend #{requested}.")
146
+ end
147
+
148
+ template_result = parse_json(template_source, dialect, backend: requested)
149
+ return { ok: false, diagnostics: template_result[:diagnostics] } unless template_result[:ok]
150
+
151
+ destination_result = parse_json(destination_source, dialect, backend: requested)
152
+ if destination_result[:ok]
153
+ return {
154
+ ok: true,
155
+ diagnostics: [],
156
+ output: TreeHaver.with_backend(requested) do
157
+ merge_json_sources(template_source, destination_source, dialect: dialect)
158
+ end,
159
+ policies: [DESTINATION_WINS_ARRAY_POLICY]
160
+ }
161
+ end
162
+
163
+ {
164
+ ok: false,
165
+ diagnostics: destination_result[:diagnostics].map do |diagnostic|
166
+ diagnostic[:category] == 'parse_error' ? diagnostic.merge(category: 'destination_parse_error') : diagnostic
167
+ end
168
+ }
169
+ end
170
+
171
+ def merge_json_sources(template_source, destination_source, dialect:)
172
+ SmartMerger.new(
173
+ template_source,
174
+ destination_source,
175
+ dialect: dialect,
176
+ add_template_only_nodes: true,
177
+ merge_arrays: false,
178
+ preserve_atomic_formatting: true
179
+ ).merge_result.to_json
180
+ end
181
+ private_class_method :merge_json_sources
182
+
183
+ def json_value_for_source(source, dialect: 'json', backend: nil)
184
+ dialect = normalize_json_dialect(dialect)
185
+ requested = requested_json_backend_id(backend)
186
+ unless json_backend_available_for_analysis?(requested, dialect: dialect)
187
+ raise ParseError, "Unsupported JSON backend #{requested}."
188
+ end
189
+
190
+ if dialect == :json && detect_trailing_comma(source)
191
+ raise ParseError,
192
+ "Trailing commas are not supported for #{dialect}."
193
+ end
194
+ raise ParseError, "Comments are not supported for #{dialect}." if dialect == :json && detect_json_comments(source)
195
+
196
+ register_backend!
197
+ analysis = TreeHaver.with_backend(requested) { FileAnalysis.new(source, dialect: dialect) }
198
+ unless analysis.valid?
199
+ message = analysis.errors.map { |error| error.respond_to?(:message) ? error.message : error.inspect }.join(', ')
200
+ raise ParseError, message
201
+ end
202
+
203
+ json_value_for_node(analysis.root_node, dialect: dialect)
204
+ end
205
+
206
+ def json_value_for_node(node, dialect: :json)
207
+ case node.type.to_s
208
+ when 'document'
209
+ child = node.semantic_children.first
210
+ if child
211
+ json_value_for_node(NodeWrapper.new(child, lines: node.lines, source: node.source, dialect: dialect),
212
+ dialect: dialect)
213
+ end
214
+ when 'object'
215
+ node.pairs.each_with_object({}) do |pair, object|
216
+ key = pair.key_name
217
+ next unless key
218
+
219
+ object[key] = json_value_for_node(pair.value_node, dialect: dialect)
220
+ end
221
+ when 'array'
222
+ node.elements.map { |element| json_value_for_node(element, dialect: dialect) }
223
+ when 'string'
224
+ decode_string_literal(node.text, dialect: dialect)
225
+ when 'number'
226
+ parse_number_literal(node.text, dialect: dialect)
227
+ when 'true'
228
+ true
229
+ when 'false'
230
+ false
231
+ when 'null'
232
+ nil
233
+ end
234
+ end
235
+
236
+ def object_key_for_literal(text, dialect: :json)
237
+ literal = text.to_s
238
+ return literal if dialect == :json5 && !%w[' "].include?(literal[0])
239
+
240
+ decode_string_literal(literal, dialect: dialect)
241
+ end
242
+
243
+ def decode_string_literal(text, dialect: :json)
244
+ return decode_json_string_literal(text) unless dialect == :json5
245
+
246
+ decode_json5_string_literal(text)
247
+ end
248
+
249
+ def decode_json_string_literal(text)
250
+ literal = text.to_s
251
+ literal = literal[1...-1] if literal.start_with?('"') && literal.end_with?('"')
252
+ literal.gsub(%r{\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})}) do |escape|
253
+ case escape
254
+ when '\\"' then '"'
255
+ when '\\\\' then '\\'
256
+ when '\\/' then '/'
257
+ when '\\b' then "\b"
258
+ when '\\f' then "\f"
259
+ when '\\n' then "\n"
260
+ when '\\r' then "\r"
261
+ when '\\t' then "\t"
262
+ else
263
+ [escape[2..].to_i(16)].pack('U')
264
+ end
265
+ end
266
+ end
267
+ private_class_method :decode_json_string_literal
99
268
 
100
- autoload :DebugLogger, "json/merge/debug_logger"
101
- autoload :Emitter, "json/merge/emitter"
102
- autoload :FileAnalysis, "json/merge/file_analysis"
103
- autoload :MergeResult, "json/merge/merge_result"
104
- autoload :NodeWrapper, "json/merge/node_wrapper"
105
- autoload :ConflictResolver, "json/merge/conflict_resolver"
106
- autoload :SmartMerger, "json/merge/smart_merger"
107
- autoload :ObjectMatchRefiner, "json/merge/object_match_refiner"
269
+ def decode_json5_string_literal(text)
270
+ literal = text.to_s
271
+ return literal unless literal.length >= 2 && %w[' "].include?(literal[0]) && literal[-1] == literal[0]
272
+
273
+ body = literal[1...-1]
274
+ result = +''
275
+ index = 0
276
+ while index < body.length
277
+ char = body[index]
278
+ unless char == '\\'
279
+ result << char
280
+ index += 1
281
+ next
282
+ end
283
+
284
+ index += 1
285
+ escape = body[index]
286
+ break unless escape
287
+
288
+ case escape
289
+ when "\n"
290
+ # JSON5 line continuations contribute no character.
291
+ when "\r"
292
+ index += 1 if body[index + 1] == "\n"
293
+ when 'b' then result << "\b"
294
+ when 'f' then result << "\f"
295
+ when 'n' then result << "\n"
296
+ when 'r' then result << "\r"
297
+ when 't' then result << "\t"
298
+ when 'v' then result << "\v"
299
+ when '0' then result << "\0"
300
+ when 'x'
301
+ hex = body[(index + 1), 2]
302
+ result << hex.to_i(16).chr(Encoding::UTF_8) if hex&.match?(/\A[0-9a-fA-F]{2}\z/)
303
+ index += 2
304
+ when 'u'
305
+ hex = body[(index + 1), 4]
306
+ result << [hex.to_i(16)].pack('U') if hex&.match?(/\A[0-9a-fA-F]{4}\z/)
307
+ index += 4
308
+ else
309
+ result << escape
310
+ end
311
+ index += 1
312
+ end
313
+ result
314
+ end
315
+ private_class_method :decode_json5_string_literal
316
+
317
+ def parse_number_literal(text, dialect: :json)
318
+ return parse_json_number(text) unless dialect == :json5
319
+
320
+ literal = text.to_s.delete('_')
321
+ return Float::INFINITY if ['Infinity', '+Infinity'].include?(literal)
322
+ return -Float::INFINITY if literal == '-Infinity'
323
+ return Float::NAN if %w[NaN +NaN -NaN].include?(literal)
324
+
325
+ Integer(literal, 0)
326
+ rescue ArgumentError
327
+ Float(literal)
328
+ end
329
+
330
+ def parse_json_number(text)
331
+ number = text.to_s
332
+ return number.to_f if number.match?(/[.eE]/)
333
+
334
+ number.to_i
335
+ end
336
+ private_class_method :parse_json_number
337
+
338
+ def parse_failure(message)
339
+ {
340
+ ok: false,
341
+ diagnostics: [parse_error(message)]
342
+ }
343
+ end
344
+ private_class_method :parse_failure
345
+
346
+ def requested_json_backend_id(backend)
347
+ return backend.to_s unless backend.to_s.empty?
348
+
349
+ current = TreeHaver.current_backend_id
350
+ return current if json_backend_available_for_analysis?(current)
351
+
352
+ available_json_backends.find do |backend_ref|
353
+ json_backend_available_for_analysis?(backend_ref.id)
354
+ end&.id || TREE_SITTER_BACKEND.id
355
+ end
356
+ private_class_method :requested_json_backend_id
357
+
358
+ def json_backend_available_for_analysis?(backend_id, dialect: :json)
359
+ register_backend!
360
+ languages = %i[jsonc json5].include?(dialect) ? [:json5] : [:json]
361
+ case backend_id.to_s
362
+ when TREE_SITTER_BACKEND.id
363
+ languages.all? do |language|
364
+ registrations = TreeHaver.registered_languages(language)
365
+ registrations.key?(:tree_sitter) || registrations.key?(:tslp)
366
+ end
367
+ else
368
+ false
369
+ end
370
+ end
371
+ private_class_method :json_backend_available_for_analysis?
372
+
373
+ def normalize_json_dialect(dialect)
374
+ normalized = dialect.to_s.downcase
375
+ return normalized.to_sym if %w[json jsonc json5].include?(normalized)
376
+
377
+ raise ArgumentError, "Unsupported JSON dialect #{dialect.inspect}. Expected json, jsonc, or json5."
378
+ end
379
+ private_class_method :normalize_json_dialect
380
+
381
+ def parse_error(message)
382
+ { severity: 'error', category: 'parse_error', message: message }
383
+ end
384
+ private_class_method :parse_error
385
+
386
+ def unsupported_feature(message)
387
+ { severity: 'error', category: 'unsupported_feature', message: message }
388
+ end
389
+ private_class_method :unsupported_feature
390
+
391
+ def unsupported_feature_result(message)
392
+ { ok: false, diagnostics: [unsupported_feature(message)], policies: [] }
393
+ end
394
+ private_class_method :unsupported_feature_result
395
+
396
+ def json_analysis_root_kind(analysis)
397
+ root = analysis.root_object || analysis.root_node
398
+ return 'object' if root&.object?
399
+ return 'array' if root&.array?
400
+
401
+ 'scalar'
402
+ end
403
+ private_class_method :json_analysis_root_kind
404
+
405
+ def collect_file_analysis_owners(analysis)
406
+ root = analysis.root_object || analysis.root_node
407
+ return [] unless root
408
+
409
+ collect_json_node_owners(root, '')
410
+ .sort_by { |owner| owner.fetch(:path) }
411
+ end
412
+ private_class_method :collect_file_analysis_owners
413
+
414
+ def collect_json_node_owners(node, path)
415
+ if node.object?
416
+ node.pairs.flat_map do |pair|
417
+ key = pair.key_name
418
+ next [] unless key
419
+
420
+ owner_path = "#{path}/#{key}"
421
+ value = pair.value_node
422
+ [{ path: owner_path, owner_kind: 'member', match_key: key }] +
423
+ (value ? collect_json_node_owners(value, owner_path) : [])
424
+ end
425
+ elsif node.array?
426
+ node.elements.each_with_index.flat_map do |element, index|
427
+ owner_path = "#{path}/#{index}"
428
+ [{ path: owner_path, owner_kind: 'element' }] +
429
+ collect_json_node_owners(element, owner_path)
430
+ end
431
+ else
432
+ []
433
+ end
434
+ end
435
+ private_class_method :collect_json_node_owners
436
+
437
+ def detect_trailing_comma(source)
438
+ state = scanner_state
439
+ source.each_char.with_index do |char, index|
440
+ next_char = source[index + 1]
441
+ advance_scanner_state(state, char, next_char)
442
+ next if state[:in_line_comment] || state[:in_block_comment] || state[:in_string]
443
+
444
+ if char == ','
445
+ lookahead = source[(index + 1)..]
446
+ next unless lookahead
447
+
448
+ trimmed = lookahead.lstrip
449
+ return true if trimmed.start_with?(']', '}')
450
+ end
451
+ end
452
+ false
453
+ end
454
+ private_class_method :detect_trailing_comma
455
+
456
+ def detect_json_comments(source)
457
+ state = scanner_state
458
+ source.each_char.with_index do |char, index|
459
+ next_char = source[index + 1]
460
+ return true if !state[:in_string] && char == '/' && %w[/ *].include?(next_char)
461
+
462
+ advance_scanner_state(state, char, next_char)
463
+ end
464
+ false
465
+ end
466
+ private_class_method :detect_json_comments
467
+
468
+ def scanner_state
469
+ {
470
+ in_string: false,
471
+ in_line_comment: false,
472
+ in_block_comment: false,
473
+ escaped: false
474
+ }
475
+ end
476
+ private_class_method :scanner_state
477
+
478
+ def advance_scanner_state(state, char, next_char)
479
+ if state[:in_line_comment]
480
+ state[:in_line_comment] = false if char == "\n"
481
+ return
482
+ end
483
+
484
+ if state[:in_block_comment]
485
+ state[:in_block_comment] = false if char == '*' && next_char == '/'
486
+ return
487
+ end
488
+
489
+ if state[:in_string]
490
+ if state[:escaped]
491
+ state[:escaped] = false
492
+ elsif char == '\\'
493
+ state[:escaped] = true
494
+ elsif char == '"'
495
+ state[:in_string] = false
496
+ end
497
+ return
498
+ end
499
+
500
+ if char == '"'
501
+ state[:in_string] = true
502
+ elsif char == '/' && next_char == '/'
503
+ state[:in_line_comment] = true
504
+ elsif char == '/' && next_char == '*'
505
+ state[:in_block_comment] = true
506
+ end
507
+ end
508
+ private_class_method :advance_scanner_state
108
509
  end
109
510
  end
110
511
 
111
- # Register with ast-merge's MergeGemRegistry for RSpec dependency tags
112
- # Only register if MergeGemRegistry is loaded (i.e., in test environment)
512
+ Json::Merge.register_backend!
513
+
113
514
  if defined?(Ast::Merge::RSpec::MergeGemRegistry)
114
515
  Ast::Merge::RSpec::MergeGemRegistry.register(
115
516
  :json_merge,
116
- require_path: "json/merge",
117
- merger_class: "Json::Merge::SmartMerger",
517
+ require_path: 'json/merge',
518
+ merger_class: 'Json::Merge::SmartMerger',
118
519
  test_source: '{"key": "value"}',
119
- category: :data,
520
+ category: :data
120
521
  )
121
522
  end
122
-
123
- Json::Merge::Version.class_eval do
124
- extend VersionGem::Basic
125
- end
data/lib/json-merge.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # For technical reasons, if we move to Zeitwerk, this cannot be require_relative.
4
- # See: https://github.com/fxn/zeitwerk#for_gem_extension
5
- # Hook for other libraries to load this library (e.g. via bundler)
6
- require "json/merge"
3
+ require_relative 'json/merge'
4
+ require 'version_gem'
5
+ require_relative 'json/merge/version'
6
+
7
+ Json::Merge::Version.class_eval do
8
+ extend VersionGem::Basic
9
+ end