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.
@@ -0,0 +1,696 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Json
4
+ module Merge
5
+ # Portable JSON-family provider for Ast::Merge dispatch.
6
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider operations keep request, rendering, and result-envelope logic together
7
+ class Provider
8
+ DIALECTS = %i[json jsonc json5].freeze
9
+ DEFAULT_PROFILE = :source_preserving
10
+
11
+ def provider_id
12
+ 'ruby.json'
13
+ end
14
+
15
+ def family
16
+ 'json'
17
+ end
18
+
19
+ def capabilities
20
+ {
21
+ operations: Ast::Merge::ProviderContract::OPERATIONS,
22
+ dialects: DIALECTS,
23
+ backends: [Json::Merge::TREE_SITTER_BACKEND.id.to_sym],
24
+ profiles: [DEFAULT_PROFILE],
25
+ role: :workflow,
26
+ source_preservation: %i[exact_source family_synthesis line_provenance reparse]
27
+ }.freeze
28
+ end
29
+
30
+ def analyze(request)
31
+ dialect = dialect_for(request)
32
+ parsed = Json::Merge.parse_json(request.fetch(:source), dialect, backend: request[:backend])
33
+ return parse_failure(:analyze, :source, parsed, request) unless parsed[:ok]
34
+
35
+ result(
36
+ :analyze,
37
+ request,
38
+ analysis: parsed.fetch(:analysis),
39
+ verification: { source_parsed: true }
40
+ )
41
+ end
42
+
43
+ def diff2(request)
44
+ before = parse_role!(:diff2, request, :before)
45
+ return before if provider_failure?(before)
46
+
47
+ after = parse_role!(:diff2, request, :after)
48
+ return after if provider_failure?(after)
49
+
50
+ changes = diff_values(before, after, path: '')
51
+ result(
52
+ :diff2,
53
+ request,
54
+ diff: { changes: changes, before: before, after: after },
55
+ changes: changes,
56
+ verification: { before_parsed: true, after_parsed: true }
57
+ )
58
+ end
59
+
60
+ def merge2(request)
61
+ dialect = dialect_for(request)
62
+ merged = Json::Merge.merge_json(
63
+ request.fetch(:incoming_source),
64
+ request.fetch(:current_source),
65
+ dialect,
66
+ backend: request[:backend]
67
+ )
68
+ return merge2_failure(merged, request) unless merged[:ok]
69
+
70
+ output = merged.fetch(:output)
71
+ verified = parse_output(output, request)
72
+ result(
73
+ :merge2,
74
+ request,
75
+ output: output,
76
+ changes: [],
77
+ render_report: { strategy: :json_smart_merger, synthesis: :family_emission },
78
+ verification: { output_reparsed: true, semantic_value: verified }
79
+ )
80
+ rescue Json::Merge::ParseError => e
81
+ failure(
82
+ :merge2,
83
+ request,
84
+ category: :render_failure,
85
+ message: "JSON family emitter produced invalid output: #{e.message}"
86
+ )
87
+ end
88
+
89
+ def merge3(request)
90
+ values = parse_merge3_roles(request)
91
+ return values if provider_failure?(values)
92
+
93
+ decision = ThreeWayDecision.new.call(**values)
94
+ rendered = render_merge3(decision, request, values)
95
+ return rendered if provider_failure?(rendered)
96
+
97
+ result(
98
+ :merge3,
99
+ request,
100
+ **rendered,
101
+ changes: decision.changes,
102
+ conflicts: decision.conflicts,
103
+ verification: rendered.fetch(:verification).merge(base_participated: true)
104
+ )
105
+ end
106
+
107
+ private
108
+
109
+ def parse_merge3_roles(request)
110
+ %i[base ours theirs].each_with_object({}) do |role, values|
111
+ parsed = parse_role!(:merge3, request, role)
112
+ return parsed if provider_failure?(parsed)
113
+
114
+ values[role] = parsed
115
+ end
116
+ end
117
+
118
+ def parse_role!(operation, request, role)
119
+ Json::Merge.json_value_for_source(
120
+ request.fetch(:"#{role}_source"),
121
+ dialect: dialect_for(request),
122
+ backend: request[:backend]
123
+ )
124
+ rescue Json::Merge::ParseError => e
125
+ failure(
126
+ operation,
127
+ request,
128
+ category: :parse_error,
129
+ message: "#{role} parse error: #{e.message}",
130
+ source_role: role
131
+ )
132
+ end
133
+
134
+ def render_merge3(decision, request, values)
135
+ return render_conflict(request, decision) if decision.conflicted?
136
+
137
+ exact_role = %i[ours theirs base].find { |role| values.fetch(role) == decision.value }
138
+ return render_exact_role(exact_role, request, decision) if exact_role
139
+
140
+ render_composite(request, decision)
141
+ end
142
+
143
+ def diff_values(before, after, path:)
144
+ return [] if before == after
145
+ return diff_objects(before, after, path: path) if before.is_a?(Hash) && after.is_a?(Hash)
146
+
147
+ [{ path: path, ours: :unchanged, theirs: :edited }.freeze]
148
+ end
149
+
150
+ def diff_objects(before, after, path:)
151
+ ordered_diff_keys(before, after).flat_map do |key|
152
+ child_path = "#{path}/#{escape_path_segment(key)}"
153
+ if !before.key?(key)
154
+ [{ path: child_path, ours: :unchanged, theirs: :added }.freeze]
155
+ elsif !after.key?(key)
156
+ [{ path: child_path, ours: :unchanged, theirs: :deleted }.freeze]
157
+ else
158
+ diff_values(before.fetch(key), after.fetch(key), path: child_path)
159
+ end
160
+ end.freeze
161
+ end
162
+
163
+ def ordered_diff_keys(before, after)
164
+ [*before.keys, *after.keys].uniq
165
+ end
166
+
167
+ def escape_path_segment(key)
168
+ key.to_s.gsub('~', '~0').gsub('/', '~1')
169
+ end
170
+
171
+ def render_exact_role(role, request, decision)
172
+ rendered = render_plan(
173
+ request,
174
+ [
175
+ source_fragment(role, request.fetch(:"#{role}_source"))
176
+ ]
177
+ )
178
+ {
179
+ output: rendered.content,
180
+ render_report: render_report(rendered, strategy: :exact_revision),
181
+ verification: verify_rendered(rendered.content, decision.value, request)
182
+ }
183
+ end
184
+
185
+ def render_composite(request, decision)
186
+ candidate = Json::Merge.merge_json(
187
+ request.fetch(:theirs_source),
188
+ request.fetch(:ours_source),
189
+ dialect_for(request),
190
+ backend: request[:backend]
191
+ )
192
+ emitted = verified_family_composite(candidate, request, decision)
193
+ return emitted if emitted
194
+
195
+ exact = render_exact_composite(request, decision)
196
+ return exact if exact
197
+
198
+ composite_failure(request, 'JSON family renderers did not reproduce the planned three-way value.')
199
+ end
200
+
201
+ def verified_family_composite(candidate, request, decision)
202
+ return unless candidate[:ok]
203
+
204
+ output = candidate.fetch(:output)
205
+ verification = verify_rendered(output, decision.value, request)
206
+ return unless verification[:semantic_match]
207
+
208
+ rendered = render_plan(
209
+ request,
210
+ [
211
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
212
+ content: output,
213
+ reason: :family_emission,
214
+ producer: provider_id
215
+ )
216
+ ]
217
+ )
218
+ {
219
+ output: rendered.content,
220
+ render_report: render_report(rendered, strategy: :family_composite),
221
+ verification: verification
222
+ }
223
+ end
224
+
225
+ def render_exact_composite(request, decision)
226
+ replacements = exact_composite_replacements(request, decision)
227
+ return unless replacements
228
+
229
+ rendered = render_plan(request, replacement_fragments(request, replacements))
230
+ verification = verify_rendered(rendered.content, decision.value, request)
231
+ return unless verification[:semantic_match]
232
+
233
+ {
234
+ output: rendered.content,
235
+ render_report: render_report(rendered, strategy: :exact_owner_composite),
236
+ fallbacks: [
237
+ {
238
+ from: :family_composite,
239
+ to: :exact_owner_composite,
240
+ reason: :semantic_verification_failed
241
+ }
242
+ ],
243
+ verification: verification
244
+ }
245
+ end
246
+
247
+ def exact_composite_replacements(request, decision)
248
+ locators = conflict_locators(request)
249
+ changes = decision.changes.select do |change|
250
+ change[:ours] == :unchanged && change[:theirs] != :unchanged
251
+ end
252
+ replacements = []
253
+ until changes.empty?
254
+ change = changes.shift
255
+ ours_range = locators.fetch(:ours).pair_range(change.fetch(:path))
256
+ return unless ours_range
257
+
258
+ theirs_range = if change[:theirs] == :deleted
259
+ nil
260
+ else
261
+ locators.fetch(:theirs).pair_range(change.fetch(:path))
262
+ end
263
+ return if change[:theirs] != :deleted && theirs_range.nil?
264
+
265
+ replacements << { ours: ours_range, theirs: theirs_range }
266
+ end
267
+ return unless non_overlapping_ours_ranges?(replacements)
268
+
269
+ replacements
270
+ end
271
+
272
+ def replacement_fragments(request, replacements)
273
+ fragments = []
274
+ cursor = 1
275
+ replacements.sort_by { |item| item.fetch(:ours).start_line }.each do |item|
276
+ ours_range = item.fetch(:ours)
277
+ fragments << source_range_fragment(:ours, cursor, ours_range.start_line - 1) if cursor < ours_range.start_line
278
+ theirs_range = item[:theirs]
279
+ fragments << source_range_fragment(:theirs, theirs_range.start_line, theirs_range.end_line) if theirs_range
280
+ cursor = ours_range.end_line + 1
281
+ end
282
+ final_line = request.fetch(:ours_source).lines.length
283
+ fragments << source_range_fragment(:ours, cursor, final_line) if cursor <= final_line
284
+ fragments
285
+ end
286
+
287
+ def render_conflict(request, decision)
288
+ localized = localized_conflict_plan(request, decision)
289
+ return localized if localized
290
+
291
+ synthesized = synthesized_conflict_plan(request, decision)
292
+ return synthesized if synthesized
293
+
294
+ render_full_file_conflict(request, decision)
295
+ end
296
+
297
+ def localized_conflict_plan(request, decision)
298
+ ranges = localized_conflict_ranges(request, decision)
299
+ return if ranges.nil?
300
+
301
+ rendered = render_plan(request, localized_conflict_fragments(request, decision, ranges))
302
+ conflict_failure(request, decision, rendered, strategy: :owner_localized_conflict)
303
+ end
304
+
305
+ def localized_conflict_ranges(request, decision)
306
+ locators = conflict_locators(request)
307
+ ranges = decision.conflicts.map do |conflict|
308
+ conflict_ranges(conflict, locators)
309
+ end
310
+ return if ranges.any?(&:nil?)
311
+ return unless non_overlapping_ours_ranges?(ranges)
312
+
313
+ ranges
314
+ end
315
+
316
+ def synthesized_conflict_plan(request, decision)
317
+ candidate = Json::Merge.merge_json(
318
+ request.fetch(:theirs_source),
319
+ request.fetch(:ours_source),
320
+ dialect_for(request),
321
+ backend: request[:backend]
322
+ )
323
+ return unless candidate[:ok]
324
+ return unless verify_rendered(candidate.fetch(:output), decision.value, request)[:semantic_match]
325
+
326
+ ranges = synthesized_conflict_ranges(request, decision, candidate.fetch(:output))
327
+ return unless ranges
328
+
329
+ rendered = render_plan(
330
+ request,
331
+ synthesized_conflict_fragments(request, decision, candidate.fetch(:output), ranges)
332
+ )
333
+ conflict_failure(
334
+ request,
335
+ decision,
336
+ rendered,
337
+ strategy: :synthesized_owner_localized_conflict,
338
+ fallbacks: [
339
+ {
340
+ from: :exact_owner_localization,
341
+ to: :synthesized_conflict_context,
342
+ reason: :missing_side_owner
343
+ }
344
+ ]
345
+ )
346
+ end
347
+
348
+ def synthesized_conflict_ranges(request, decision, candidate)
349
+ locators = conflict_locators(request)
350
+ candidate_locator = SourceLocator.new(
351
+ candidate,
352
+ dialect: dialect_for(request),
353
+ backend: request[:backend]
354
+ )
355
+ conflicts = decision.conflicts.dup
356
+ ranges = []
357
+ until conflicts.empty?
358
+ conflict = conflicts.shift
359
+ sides = conflict_source_ranges(conflict, locators)
360
+ context = candidate_locator.pair_range(conflict.fetch(:path))
361
+ return unless sides && context
362
+
363
+ ranges << sides.merge(context: context)
364
+ end
365
+ return unless non_overlapping_ranges?(ranges.map { |item| item.fetch(:context) })
366
+
367
+ ranges
368
+ end
369
+
370
+ def conflict_source_ranges(conflict, locators)
371
+ ranges = {}
372
+ roles = %i[base ours theirs]
373
+ until roles.empty?
374
+ role = roles.shift
375
+ state = conflict.fetch(role)
376
+ range = state.fetch(:present) ? locators.fetch(role).pair_range(conflict.fetch(:path)) : nil
377
+ return if state.fetch(:present) && range.nil?
378
+
379
+ ranges[role] = range
380
+ end
381
+ ranges
382
+ end
383
+
384
+ def synthesized_conflict_fragments(request, decision, candidate, ranges)
385
+ fragments = []
386
+ cursor = 1
387
+ decision.conflicts
388
+ .zip(ranges)
389
+ .sort_by { |_conflict, item| item.fetch(:context).start_line }
390
+ .each do |conflict, item|
391
+ context = item.fetch(:context)
392
+ append_synthesized_context(fragments, candidate, cursor, context.start_line - 1)
393
+ fragments << conflict_fragment(request, conflict, item)
394
+ cursor = context.end_line + 1
395
+ end
396
+ append_synthesized_context(fragments, candidate, cursor, candidate.lines.length)
397
+ fragments
398
+ end
399
+
400
+ def append_synthesized_context(fragments, source, start_line, end_line)
401
+ return if start_line > end_line
402
+
403
+ content = source.lines.slice(start_line - 1, end_line - start_line + 1).join
404
+ fragments << Ast::Merge::SourceRender::SynthesizedFragment.new(
405
+ content: content,
406
+ reason: :conflict_context,
407
+ producer: provider_id,
408
+ metadata: { candidate_line_start: start_line }
409
+ )
410
+ end
411
+
412
+ def conflict_locators(request)
413
+ %i[base ours theirs].to_h do |role|
414
+ [
415
+ role,
416
+ SourceLocator.new(
417
+ request.fetch(:"#{role}_source"),
418
+ dialect: dialect_for(request),
419
+ backend: request[:backend]
420
+ )
421
+ ]
422
+ end
423
+ end
424
+
425
+ def conflict_ranges(conflict, locators)
426
+ ranges = {}
427
+ roles = %i[base ours theirs]
428
+ until roles.empty?
429
+ role = roles.shift
430
+ state = conflict.fetch(role)
431
+ range = state.fetch(:present) ? locators.fetch(role).pair_range(conflict.fetch(:path)) : nil
432
+ return if state.fetch(:present) && range.nil?
433
+
434
+ ranges[role] = range
435
+ end
436
+ return if ranges[:ours].nil?
437
+
438
+ ranges
439
+ end
440
+
441
+ def non_overlapping_ours_ranges?(ranges)
442
+ non_overlapping_ranges?(ranges.map { |item| item.fetch(:ours) })
443
+ end
444
+
445
+ def non_overlapping_ranges?(ranges)
446
+ ranges
447
+ .sort_by(&:start_line)
448
+ .each_cons(2)
449
+ .none? { |left, right| left.end_line >= right.start_line }
450
+ end
451
+
452
+ def localized_conflict_fragments(request, decision, ranges)
453
+ fragments = []
454
+ cursor = 1
455
+ decision.conflicts
456
+ .zip(ranges)
457
+ .sort_by { |_conflict, item| item.fetch(:ours).start_line }
458
+ .each do |conflict, item|
459
+ ours_range = item.fetch(:ours)
460
+ if cursor < ours_range.start_line
461
+ fragments << source_range_fragment(:ours, cursor, ours_range.start_line - 1)
462
+ end
463
+ fragments << conflict_fragment(request, conflict, item)
464
+ cursor = ours_range.end_line + 1
465
+ end
466
+ final_line = request.fetch(:ours_source).lines.length
467
+ fragments << source_range_fragment(:ours, cursor, final_line) if cursor <= final_line
468
+ fragments
469
+ end
470
+
471
+ def conflict_fragment(request, conflict, ranges)
472
+ Ast::Merge::SourceRender::ConflictFragment.new(
473
+ conflict_id: conflict.fetch(:conflict_id),
474
+ base: conflict_side_fragment(:base, ranges[:base]),
475
+ ours: conflict_side_fragment(:ours, ranges[:ours]),
476
+ theirs: conflict_side_fragment(:theirs, ranges[:theirs]),
477
+ labels: request.fetch(:labels, {}),
478
+ marker_size: request.fetch(:conflict_marker_size, 7),
479
+ metadata: { path: conflict.fetch(:path), category: conflict.fetch(:category) }
480
+ )
481
+ end
482
+
483
+ def conflict_side_fragment(role, range)
484
+ range ? [source_range_fragment(role, range.start_line, range.end_line)] : []
485
+ end
486
+
487
+ def source_range_fragment(role, start_line, end_line)
488
+ Ast::Merge::SourceRender::SourceFragment.new(
489
+ revision: role,
490
+ start_line: start_line,
491
+ end_line: end_line,
492
+ metadata: { source_role: role }
493
+ )
494
+ end
495
+
496
+ def render_full_file_conflict(request, decision)
497
+ rendered = render_plan(
498
+ request,
499
+ [
500
+ Ast::Merge::SourceRender::ConflictFragment.new(
501
+ conflict_id: decision.conflicts.first.fetch(:conflict_id),
502
+ base: [conflict_source_fragment(:base, request.fetch(:base_source))],
503
+ ours: [conflict_source_fragment(:ours, request.fetch(:ours_source))],
504
+ theirs: [conflict_source_fragment(:theirs, request.fetch(:theirs_source))],
505
+ labels: request.fetch(:labels, {}),
506
+ marker_size: request.fetch(:conflict_marker_size, 7),
507
+ metadata: { conflicts: decision.conflicts.map { |conflict| conflict[:conflict_id] } }
508
+ )
509
+ ]
510
+ )
511
+ conflict_failure(
512
+ request,
513
+ decision,
514
+ rendered,
515
+ strategy: :full_file_conflict,
516
+ fallbacks: [
517
+ {
518
+ from: :owner_localization,
519
+ to: :full_file_conflict,
520
+ reason: :owner_not_whole_line_addressable
521
+ }
522
+ ]
523
+ )
524
+ end
525
+
526
+ def conflict_failure(request, decision, rendered, strategy:, fallbacks: [])
527
+ failure(
528
+ :merge3,
529
+ request,
530
+ category: :merge_conflict,
531
+ message: "#{decision.conflicts.length} unresolved JSON merge conflict(s).",
532
+ changes: decision.changes,
533
+ conflicts: decision.conflicts,
534
+ fallbacks: fallbacks,
535
+ conflicted_output: rendered.content,
536
+ render_report: render_report(rendered, strategy: strategy)
537
+ )
538
+ end
539
+
540
+ def render_plan(request, fragments)
541
+ plan = Ast::Merge::SourceRender::Plan.new(
542
+ sources: {
543
+ base: request[:base_source].to_s,
544
+ ours: request[:ours_source].to_s,
545
+ theirs: request[:theirs_source].to_s
546
+ },
547
+ fragments: fragments,
548
+ metadata: { provider_id: provider_id, dialect: dialect_for(request) }
549
+ )
550
+ Ast::Merge::SourceRender::Renderer.new.render(plan)
551
+ end
552
+
553
+ def source_fragment(role, source)
554
+ line_count = source.lines.length
555
+ raise Ast::Merge::SourceRender::InvalidPlanError, "#{role} source has no renderable lines" if line_count.zero?
556
+
557
+ Ast::Merge::SourceRender::SourceFragment.new(
558
+ revision: role,
559
+ start_line: 1,
560
+ end_line: line_count,
561
+ metadata: { source_role: role }
562
+ )
563
+ end
564
+
565
+ def conflict_source_fragment(role, source)
566
+ return source_fragment(role, source) if source.end_with?("\n")
567
+
568
+ Ast::Merge::SourceRender::SynthesizedFragment.new(
569
+ content: "#{source}\n",
570
+ reason: :conflict_line_boundary,
571
+ producer: provider_id,
572
+ metadata: { source_role: role, copied_source: true }
573
+ )
574
+ end
575
+
576
+ def verify_rendered(output, expected_value, request)
577
+ actual = parse_output(output, request)
578
+ {
579
+ output_reparsed: true,
580
+ semantic_match: actual == expected_value
581
+ }
582
+ rescue Json::Merge::ParseError
583
+ {
584
+ output_reparsed: false,
585
+ semantic_match: false
586
+ }
587
+ end
588
+
589
+ def parse_output(output, request)
590
+ Json::Merge.json_value_for_source(
591
+ output,
592
+ dialect: dialect_for(request),
593
+ backend: request[:backend]
594
+ )
595
+ end
596
+
597
+ def result(
598
+ operation, request, verification:, changes: [], conflicts: [], fallbacks: [], render_report: {}, **payload
599
+ )
600
+ Ast::Merge::ProviderResult.build(
601
+ operation: operation,
602
+ success: true,
603
+ envelope: {
604
+ provider: provider_metadata(request),
605
+ profile: profile_metadata(request),
606
+ changes: changes,
607
+ conflicts: conflicts,
608
+ fallbacks: fallbacks,
609
+ render_report: render_report,
610
+ verification: verification
611
+ },
612
+ **payload
613
+ )
614
+ end
615
+
616
+ def failure(
617
+ operation, request, category:, message:, changes: [], conflicts: [], fallbacks: [], render_report: {}, **payload
618
+ )
619
+ Ast::Merge::ProviderResult.build(
620
+ operation: operation,
621
+ success: false,
622
+ envelope: {
623
+ provider: provider_metadata(request),
624
+ profile: profile_metadata(request),
625
+ diagnostics: [{ category: category, severity: :error, message: message, blocking: true }],
626
+ changes: changes,
627
+ conflicts: conflicts,
628
+ fallbacks: fallbacks,
629
+ render_report: render_report,
630
+ verification: { base_participated: operation == :merge3 }
631
+ },
632
+ **payload
633
+ )
634
+ end
635
+
636
+ def parse_failure(operation, role, parsed, request)
637
+ diagnostic = parsed.fetch(:diagnostics).first || {}
638
+ failure(
639
+ operation,
640
+ request,
641
+ category: :parse_error,
642
+ message: "#{role} parse error: #{diagnostic[:message]}",
643
+ source_role: role
644
+ )
645
+ end
646
+
647
+ def merge2_failure(merged, request)
648
+ diagnostic = merged.fetch(:diagnostics).first || {}
649
+ failure(
650
+ :merge2,
651
+ request,
652
+ category: diagnostic[:category] || :merge_failed,
653
+ message: diagnostic[:message] || 'JSON two-way merge failed.'
654
+ )
655
+ end
656
+
657
+ def composite_failure(request, message)
658
+ failure(:merge3, request, category: :render_failure, message: message)
659
+ end
660
+
661
+ def provider_failure?(value)
662
+ value.is_a?(Hash) && value[:ok] == false
663
+ end
664
+
665
+ def render_report(rendered, strategy:)
666
+ {
667
+ strategy: strategy,
668
+ line_records: rendered.line_records,
669
+ synthesized_fragments: rendered.synthesized_fragments,
670
+ conflicts: rendered.conflicts,
671
+ verification_input: rendered.verification_input
672
+ }
673
+ end
674
+
675
+ def provider_metadata(request)
676
+ {
677
+ provider_id: provider_id,
678
+ family: family,
679
+ dialect: dialect_for(request),
680
+ backend: request[:backend],
681
+ package: Json::Merge::PACKAGE_NAME,
682
+ package_version: Json::Merge::Version::VERSION
683
+ }.compact
684
+ end
685
+
686
+ def profile_metadata(request)
687
+ { profile_id: request[:profile_id] || DEFAULT_PROFILE }
688
+ end
689
+
690
+ def dialect_for(request)
691
+ (request[:dialect] || :json).to_sym
692
+ end
693
+ end
694
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
695
+ end
696
+ end