metaschema 0.2.0 → 0.2.2

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,1583 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Metaschema
4
+ class ModelGenerator
5
+ # Creates assembly classes from Metaschema assembly definitions.
6
+ # Handles placeholder creation, population with XML/JSON mappings,
7
+ # model processing (fields, assemblies, choices), and custom
8
+ # serialization callbacks (BY_KEY, SINGLETON_OR_ARRAY, json-value-key-flag).
9
+ class AssemblyFactory
10
+ def initialize(assembly_def, generator)
11
+ @assembly_def = assembly_def
12
+ @g = generator
13
+ end
14
+
15
+ def create_placeholder
16
+ ad = @assembly_def
17
+ return unless ad.name
18
+
19
+ klass_name = "Assembly_#{ad.name.gsub('-', '_')}"
20
+ @g.classes[klass_name] ||= Class.new(Lutaml::Model::Serializable)
21
+ end
22
+
23
+ def populate
24
+ ad = @assembly_def
25
+ return unless ad.name
26
+
27
+ klass_name = "Assembly_#{ad.name.gsub('-', '_')}"
28
+ klass = @g.classes[klass_name]
29
+ return unless klass
30
+
31
+ @g.current_assembly_name = ad.name.gsub("-", "_")
32
+
33
+ ad.define_flag&.each { |f| @g.add_inline_flag(klass, f) }
34
+ ad.flag&.each { |f| @g.add_flag_reference(klass, f) }
35
+
36
+ process_model(klass, ad.model) if ad.model
37
+
38
+ root_name = ad.root_name&.content || ad.name
39
+ build_assembly_xml(klass, root_name, ad)
40
+ build_assembly_json(klass, root_name, ad)
41
+
42
+ if ad.root_name&.content
43
+ add_json_root_handling(klass, root_name)
44
+ end
45
+
46
+ @g.apply_constraint_validation(klass, ad.constraint)
47
+ klass.instance_variable_set(:@populated, true)
48
+ ensure
49
+ @g.current_assembly_name = nil
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :assembly_def
55
+
56
+ # ── XML Building ─────────────────────────────────────────────────
57
+
58
+ def build_assembly_xml(klass, root_name, assembly_def)
59
+ flag_defs = assembly_def.define_flag || []
60
+ flag_refs = assembly_def.flag || []
61
+ child_mappings = collect_child_mappings(assembly_def)
62
+
63
+ flag_attr_maps = flag_defs.filter_map do |f|
64
+ [f.name, Utils.safe_attr(f.name)] if f.name
65
+ end
66
+ flag_ref_maps = flag_refs.filter_map do |f|
67
+ [f.ref, Utils.safe_attr(f.ref)] if f.ref
68
+ end
69
+
70
+ unwrapped_mappings, wrapped_mappings = child_mappings.partition do |m|
71
+ m[:unwrapped]
72
+ end
73
+
74
+ klass.class_eval do
75
+ xml do
76
+ element root_name
77
+ ordered
78
+
79
+ flag_attr_maps.each do |xml_name, attr_name|
80
+ map_attribute xml_name, to: attr_name
81
+ end
82
+
83
+ flag_ref_maps.each do |xml_name, attr_name|
84
+ map_attribute xml_name, to: attr_name
85
+ end
86
+
87
+ wrapped_mappings.each do |mapping|
88
+ map_element mapping[:xml_name], to: mapping[:attr_name]
89
+ end
90
+ end
91
+ end
92
+
93
+ unwrapped_mappings.each do |mapping|
94
+ delegate_field_xml_mappings(mapping[:attr_name], klass)
95
+ end
96
+ end
97
+
98
+ def delegate_field_xml_mappings(attr_name, parent_klass)
99
+ attr_obj = parent_klass.attributes[attr_name]
100
+ field_type = attr_obj&.type if attr_obj
101
+ return unless field_type && field_type < Lutaml::Model::Serializable
102
+
103
+ mapping = field_type.mappings[:xml]
104
+ return unless mapping
105
+
106
+ mapping.attributes.each do |rule|
107
+ parent_klass.mappings[:xml].map_attribute rule.name, to: rule.to,
108
+ delegate: attr_name
109
+ end
110
+
111
+ content_rule = mapping.content_mapping
112
+ if content_rule
113
+ parent_klass.mappings[:xml].map_content to: content_rule.to,
114
+ delegate: attr_name
115
+ end
116
+
117
+ mapping.elements.each do |rule|
118
+ parent_klass.mappings[:xml].map_element rule.name, to: rule.to,
119
+ delegate: attr_name
120
+ end
121
+ end
122
+
123
+ def collect_child_mappings(assembly_def)
124
+ mappings = []
125
+ model = assembly_def.model
126
+ return mappings unless model
127
+
128
+ mappings.concat(collect_model_child_mappings(model))
129
+ mappings
130
+ end
131
+
132
+ def collect_model_child_mappings(model)
133
+ mappings = []
134
+
135
+ model.field&.each do |field_ref|
136
+ ref_name = field_ref.ref
137
+ next unless ref_name
138
+
139
+ xml_name = field_ref.use_name&.content || ref_name
140
+ group_as = field_ref.group_as
141
+ grouped = group_as&.in_xml == "GROUPED"
142
+ unwrapped = !grouped && field_ref.in_xml == "UNWRAPPED"
143
+
144
+ mappings << build_child_mapping(xml_name, group_as, grouped, ref_name,
145
+ unwrapped: unwrapped)
146
+ end
147
+
148
+ model.assembly&.each do |assembly_ref|
149
+ ref_name = assembly_ref.ref
150
+ next unless ref_name
151
+
152
+ xml_name = @g.assembly_xml_element_name(assembly_ref)
153
+ group_as = assembly_ref.group_as
154
+ grouped = group_as&.in_xml == "GROUPED"
155
+
156
+ attr_name = grouped ? Utils.safe_attr(group_as.name) : Utils.safe_attr(ref_name)
157
+ mappings << { xml_name: grouped ? group_as.name : xml_name,
158
+ attr_name: attr_name, grouped: grouped }
159
+ end
160
+
161
+ model.define_field&.each do |inline_def|
162
+ next unless inline_def.name
163
+
164
+ mappings << { xml_name: inline_def.name,
165
+ attr_name: Utils.safe_attr(inline_def.name), grouped: false }
166
+ end
167
+
168
+ model.define_assembly&.each do |inline_def|
169
+ next unless inline_def.name
170
+
171
+ mappings << { xml_name: inline_def.name,
172
+ attr_name: Utils.safe_attr(inline_def.name), grouped: false }
173
+ end
174
+
175
+ model.choice&.each do |c|
176
+ mappings.concat(collect_choice_child_mappings(c))
177
+ end
178
+ model.choice_group&.each do |cg|
179
+ mappings.concat(collect_choice_group_child_mappings(cg))
180
+ end
181
+
182
+ mappings
183
+ end
184
+
185
+ def collect_choice_child_mappings(choice)
186
+ mappings = []
187
+
188
+ choice.field&.each do |field_ref|
189
+ ref_name = field_ref.ref
190
+ next unless ref_name
191
+
192
+ xml_name = field_ref.use_name&.content || ref_name
193
+ group_as = field_ref.group_as
194
+ grouped = group_as&.in_xml == "GROUPED"
195
+ unwrapped = !grouped && field_ref.in_xml == "UNWRAPPED"
196
+
197
+ mappings << build_child_mapping(xml_name, group_as, grouped, ref_name,
198
+ unwrapped: unwrapped)
199
+ end
200
+
201
+ choice.assembly&.each do |assembly_ref|
202
+ ref_name = assembly_ref.ref
203
+ next unless ref_name
204
+
205
+ xml_name = @g.assembly_xml_element_name(assembly_ref)
206
+ group_as = assembly_ref.group_as
207
+ grouped = group_as&.in_xml == "GROUPED"
208
+
209
+ attr_name = grouped ? Utils.safe_attr(group_as.name) : Utils.safe_attr(ref_name)
210
+ mappings << { xml_name: grouped ? group_as.name : xml_name,
211
+ attr_name: attr_name, grouped: grouped }
212
+ end
213
+
214
+ choice.define_field&.each do |inline_def|
215
+ next unless inline_def.name
216
+
217
+ mappings << { xml_name: inline_def.name,
218
+ attr_name: Utils.safe_attr(inline_def.name), grouped: false }
219
+ end
220
+
221
+ choice.define_assembly&.each do |inline_def|
222
+ next unless inline_def.name
223
+
224
+ mappings << { xml_name: inline_def.name,
225
+ attr_name: Utils.safe_attr(inline_def.name), grouped: false }
226
+ end
227
+
228
+ mappings
229
+ end
230
+
231
+ def collect_choice_group_child_mappings(choice_group)
232
+ mappings = []
233
+
234
+ choice_group.field&.each do |field_ref|
235
+ ref_name = field_ref.ref
236
+ next unless ref_name
237
+
238
+ xml_name = field_ref.use_name&.content || ref_name
239
+ group_as = choice_group.group_as
240
+ grouped = group_as&.in_xml == "GROUPED"
241
+ unwrapped = !grouped && field_ref.in_xml == "UNWRAPPED"
242
+ mappings << build_child_mapping(xml_name, group_as, grouped, ref_name,
243
+ unwrapped: unwrapped)
244
+ end
245
+
246
+ choice_group.assembly&.each do |assembly_ref|
247
+ ref_name = assembly_ref.ref
248
+ next unless ref_name
249
+
250
+ xml_name = @g.assembly_xml_element_name(assembly_ref)
251
+ group_as = choice_group.group_as
252
+ grouped = group_as&.in_xml == "GROUPED"
253
+ attr_name = grouped ? Utils.safe_attr(group_as.name) : Utils.safe_attr(ref_name)
254
+ mappings << { xml_name: grouped ? group_as.name : xml_name,
255
+ attr_name: attr_name, grouped: grouped }
256
+ end
257
+
258
+ choice_group.define_field&.each do |inline_def|
259
+ next unless inline_def.name
260
+
261
+ mappings << { xml_name: inline_def.name,
262
+ attr_name: Utils.safe_attr(inline_def.name), grouped: false }
263
+ end
264
+
265
+ choice_group.define_assembly&.each do |inline_def|
266
+ next unless inline_def.name
267
+
268
+ mappings << { xml_name: inline_def.name,
269
+ attr_name: Utils.safe_attr(inline_def.name), grouped: false }
270
+ end
271
+
272
+ mappings
273
+ end
274
+
275
+ def build_child_mapping(xml_name, group_as, grouped, ref_name = nil,
276
+ unwrapped: false)
277
+ if grouped
278
+ { xml_name: group_as.name, attr_name: Utils.safe_attr(group_as.name),
279
+ grouped: true, unwrapped: false }
280
+ else
281
+ attr_name = Utils.safe_attr(ref_name || xml_name)
282
+ { xml_name: xml_name, attr_name: attr_name, grouped: false,
283
+ unwrapped: unwrapped }
284
+ end
285
+ end
286
+
287
+ # ── JSON Building ─────────────────────────────────────────────────
288
+
289
+ def build_assembly_json(klass, root_name, assembly_def)
290
+ flag_defs = assembly_def.define_flag || []
291
+ flag_refs = assembly_def.flag || []
292
+
293
+ flag_attr_maps = flag_defs.filter_map do |f|
294
+ [f.name, Utils.safe_attr(f.name)] if f.name
295
+ end
296
+ flag_ref_maps = flag_refs.filter_map do |f|
297
+ [f.ref, Utils.safe_attr(f.ref)] if f.ref
298
+ end
299
+
300
+ json_field_mappings = collect_json_field_mappings(assembly_def)
301
+ json_assembly_mappings = collect_json_assembly_mappings(assembly_def)
302
+
303
+ vk_flag_mappings = json_field_mappings.select { |m| m[:vk_flag] }
304
+ by_key_mappings = json_field_mappings.select { |m| m[:by_key] }
305
+ soa_mappings = json_field_mappings.select { |m| m[:singleton_or_array] }
306
+ regular_field_mappings = json_field_mappings.reject do |m|
307
+ m[:vk_flag] || m[:by_key] || m[:singleton_or_array]
308
+ end
309
+
310
+ assembly_by_key_mappings = json_assembly_mappings.select do |m|
311
+ m[:by_key]
312
+ end
313
+ assembly_soa_mappings = json_assembly_mappings.select do |m|
314
+ m[:singleton_or_array]
315
+ end
316
+ regular_assembly_mappings = json_assembly_mappings.reject do |m|
317
+ m[:by_key] || m[:singleton_or_array]
318
+ end
319
+
320
+ klass.class_eval do
321
+ key_value do
322
+ root root_name
323
+
324
+ flag_attr_maps.each do |xml_name, attr_name|
325
+ map xml_name, to: attr_name
326
+ end
327
+
328
+ flag_ref_maps.each do |xml_name, attr_name|
329
+ map xml_name, to: attr_name
330
+ end
331
+
332
+ regular_field_mappings.each do |mapping|
333
+ if mapping[:scalar]
334
+ map mapping[:json_name], to: mapping[:attr_name],
335
+ with: { to: mapping[:to_method], from: mapping[:from_method] }
336
+ else
337
+ map mapping[:json_name], to: mapping[:attr_name],
338
+ render_empty: true
339
+ end
340
+ end
341
+
342
+ regular_assembly_mappings.each do |mapping|
343
+ map mapping[:json_name], to: mapping[:attr_name],
344
+ render_empty: true
345
+ end
346
+ end
347
+ end
348
+
349
+ regular_field_mappings.each do |mapping|
350
+ next unless mapping[:scalar]
351
+
352
+ field_klass = mapping[:field_klass]
353
+ attr_sym = mapping[:attr_name]
354
+ has_flags = mapping[:has_flags]
355
+
356
+ klass.define_method(mapping[:from_method]) do |instance, value|
357
+ if value.is_a?(Array)
358
+ parsed = value.map do |v|
359
+ has_flags ? field_klass.of_json(v) : field_klass.new(content: v)
360
+ end
361
+ instance.instance_variable_set("@#{attr_sym}", parsed)
362
+ elsif value.is_a?(Hash)
363
+ if value.empty?
364
+ inst = field_klass.new(content: "")
365
+ inst.instance_variable_set(:@_was_empty_hash, true)
366
+ instance.instance_variable_set("@#{attr_sym}", inst)
367
+ else
368
+ instance.instance_variable_set("@#{attr_sym}",
369
+ field_klass.of_json(value))
370
+ end
371
+ elsif value
372
+ instance.instance_variable_set("@#{attr_sym}",
373
+ has_flags ? field_klass.of_json(value) : field_klass.new(content: value))
374
+ end
375
+ end
376
+
377
+ klass.define_method(mapping[:to_method]) do |instance, doc|
378
+ current = instance.instance_variable_get("@#{attr_sym}")
379
+ if current.is_a?(Array)
380
+ result = current.map do |item|
381
+ if has_flags && item.is_a?(Lutaml::Model::Serializable)
382
+ field_klass.as_json(item)
383
+ else
384
+ item.respond_to?(:content) ? item.content : item
385
+ end
386
+ end
387
+ doc[mapping[:json_name]] = result
388
+ elsif current
389
+ if current.instance_variable_get(:@_was_empty_hash)
390
+ doc[mapping[:json_name]] = {}
391
+ elsif has_flags && current.is_a?(Lutaml::Model::Serializable)
392
+ doc[mapping[:json_name]] = field_klass.as_json(current)
393
+ else
394
+ val = current.respond_to?(:content) ? current.content : current
395
+ doc[mapping[:json_name]] = val
396
+ end
397
+ end
398
+ end
399
+ end
400
+
401
+ soa_mappings.each do |mapping|
402
+ attr_sym = mapping[:attr_name]
403
+ json_name = mapping[:json_name]
404
+ from_m = mapping[:from_method]
405
+ to_m = mapping[:to_method]
406
+
407
+ klass.define_method(from_m) do |instance, value|
408
+ Services::FieldDeserializer.call(instance, attr_sym, :json, value,
409
+ group_as: "SINGLETON_OR_ARRAY", collapsible: false)
410
+ end
411
+
412
+ klass.define_method(to_m) do |instance, doc|
413
+ Services::FieldSerializer.call(instance, attr_sym, :json, doc,
414
+ group_as: "SINGLETON_OR_ARRAY", collapsible: false)
415
+ end
416
+
417
+ klass.class_eval do
418
+ key_value do
419
+ map json_name, to: attr_sym,
420
+ with: { to: to_m, from: from_m }
421
+ end
422
+ end
423
+
424
+ if mapping[:alt_json_name]
425
+ klass.class_eval do
426
+ key_value do
427
+ map mapping[:alt_json_name], to: attr_sym,
428
+ with: { to: to_m, from: from_m }
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ vk_flag_mappings.each do |mapping|
435
+ callbacks = build_vk_flag_field_callbacks(
436
+ klass, mapping[:field_klass], mapping[:json_name], mapping[:attr_name]
437
+ )
438
+ klass.class_eval do
439
+ key_value do
440
+ map mapping[:json_name], to: mapping[:attr_name],
441
+ with: { to: callbacks[:to_method], from: callbacks[:from_method] }
442
+ end
443
+ end
444
+ end
445
+
446
+ by_key_mappings.each do |mapping|
447
+ unless klass.attributes.key?(mapping[:attr_name])
448
+ klass.attribute mapping[:attr_name], mapping[:field_klass],
449
+ collection: true
450
+ end
451
+ callbacks = build_by_key_field_callbacks(
452
+ klass, mapping[:field_klass], mapping[:json_name],
453
+ mapping[:attr_name], mapping[:json_key_flag]
454
+ )
455
+ klass.class_eval do
456
+ key_value do
457
+ map mapping[:json_name], to: mapping[:attr_name],
458
+ with: { to: callbacks[:to_method], from: callbacks[:from_method] }
459
+ end
460
+ end
461
+ end
462
+
463
+ assembly_by_key_mappings.each do |mapping|
464
+ unless klass.attributes.key?(mapping[:attr_name])
465
+ asm_type = mapping[:asm_klass] || Lutaml::Model::Serializable
466
+ klass.attribute mapping[:attr_name], asm_type, collection: true
467
+ end
468
+ callbacks = build_by_key_assembly_callbacks(
469
+ klass, mapping[:asm_klass], mapping[:json_name],
470
+ mapping[:attr_name], mapping[:json_key_flag],
471
+ grouped: mapping[:grouped] || false,
472
+ child_attr: mapping[:child_attr]
473
+ )
474
+ klass.class_eval do
475
+ key_value do
476
+ map mapping[:json_name], to: mapping[:attr_name],
477
+ with: { to: callbacks[:to_method], from: callbacks[:from_method] }
478
+ end
479
+ end
480
+ end
481
+
482
+ assembly_soa_mappings.each do |mapping|
483
+ attr_sym = mapping[:attr_name]
484
+ json_name = mapping[:json_name]
485
+ from_m = mapping[:from_method]
486
+ to_m = mapping[:to_method]
487
+
488
+ klass.define_method(from_m) do |instance, value|
489
+ Services::FieldDeserializer.call(instance, attr_sym, :json, value,
490
+ group_as: "SINGLETON_OR_ARRAY", collapsible: false)
491
+ end
492
+
493
+ klass.define_method(to_m) do |instance, doc|
494
+ Services::FieldSerializer.call(instance, attr_sym, :json, doc,
495
+ group_as: "SINGLETON_OR_ARRAY", collapsible: false)
496
+ end
497
+
498
+ klass.class_eval do
499
+ key_value do
500
+ map json_name, to: attr_sym, render_empty: true,
501
+ with: { to: to_m, from: from_m }
502
+ end
503
+ end
504
+ end
505
+
506
+ if flag_defs.empty? && flag_refs.empty? &&
507
+ json_assembly_mappings.length == 1 &&
508
+ json_assembly_mappings.first[:by_key]
509
+
510
+ by_key_json_name = json_assembly_mappings.first[:json_name]
511
+
512
+ orig_of_json = klass.method(:of_json)
513
+ klass.define_singleton_method(:of_json) do |data, options = {}|
514
+ if data.is_a?(Hash) && !data.key?(by_key_json_name)
515
+ orig_of_json.call({ by_key_json_name => data }, options)
516
+ else
517
+ orig_of_json.call(data, options)
518
+ end
519
+ end
520
+
521
+ orig_as_json = klass.method(:as_json)
522
+ klass.define_singleton_method(:as_json) do |instance, options = {}|
523
+ result = orig_as_json.call(instance, options)
524
+ if result.is_a?(Hash) && result.key?(by_key_json_name)
525
+ result[by_key_json_name]
526
+ else
527
+ result
528
+ end
529
+ end
530
+ end
531
+ end
532
+
533
+ # ── JSON Mapping Collectors ────────────────────────────────────────
534
+
535
+ def collect_json_field_mappings(assembly_def)
536
+ mappings = []
537
+ model = assembly_def.model
538
+ return mappings unless model
539
+
540
+ mappings.concat(collect_model_json_field_mappings(model))
541
+ mappings
542
+ end
543
+
544
+ def collect_model_json_field_mappings(model)
545
+ mappings = []
546
+
547
+ model.field&.each { |fr| mappings << build_field_json_mapping(fr) }
548
+ model.define_field&.each do |fd|
549
+ mappings << build_inline_field_json_mapping(fd) if fd.name
550
+ end
551
+ model.choice&.each do |c|
552
+ c.field&.each { |fr| mappings << build_field_json_mapping(fr) }
553
+ c.define_field&.each do |fd|
554
+ mappings << build_inline_field_json_mapping(fd) if fd.name
555
+ end
556
+ end
557
+ model.choice_group&.each do |cg|
558
+ cg.field&.each do |fr|
559
+ mappings << build_field_json_mapping(fr, cg.group_as)
560
+ end
561
+ cg.define_field&.each do |fd|
562
+ mappings << build_inline_field_json_mapping(fd) if fd.name
563
+ end
564
+ end
565
+
566
+ mappings
567
+ end
568
+
569
+ def build_field_json_mapping(field_ref, override_group_as = nil)
570
+ ref_name = field_ref.ref
571
+ return nil unless ref_name
572
+
573
+ group_as = override_group_as || field_ref.group_as
574
+ field_def = @g.field_defs[ref_name]
575
+ field_klass = @g.classes["Field_#{ref_name.gsub('-', '_')}"]
576
+ has_flags = field_has_flags?(field_def)
577
+
578
+ json_name = if group_as
579
+ group_as.name
580
+ else
581
+ field_ref.use_name&.content || ref_name
582
+ end
583
+ attr_name = Utils.safe_attr(ref_name)
584
+
585
+ if group_as&.in_json == "BY_KEY"
586
+ json_key_flag = field_def&.json_key&.flag_ref
587
+ return {
588
+ json_name: json_name, attr_name: attr_name,
589
+ by_key: true, field_klass: field_klass,
590
+ json_key_flag: json_key_flag
591
+ }
592
+ end
593
+
594
+ if field_klass&.instance_variable_get(:@json_vk_flag_key_attr)
595
+ return {
596
+ json_name: json_name, attr_name: attr_name,
597
+ vk_flag: true, field_klass: field_klass
598
+ }
599
+ end
600
+
601
+ if has_flags
602
+ is_soa = group_as && ["SINGLETON_OR_ARRAY",
603
+ "ARRAY"].include?(group_as.in_json)
604
+ method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
605
+ if is_soa
606
+ result = {
607
+ json_name: json_name, attr_name: attr_name, scalar: false,
608
+ singleton_or_array: true, field_klass: field_klass,
609
+ to_method: :"json_soa_to_#{method_suffix}",
610
+ from_method: :"json_soa_from_#{method_suffix}"
611
+ }
612
+ if group_as && ref_name != json_name
613
+ result[:alt_json_name] = ref_name
614
+ end
615
+ result
616
+ else
617
+ {
618
+ json_name: json_name, attr_name: attr_name, scalar: true,
619
+ has_flags: true, field_klass: field_klass,
620
+ to_method: :"json_to_#{method_suffix}",
621
+ from_method: :"json_from_#{method_suffix}"
622
+ }
623
+ end
624
+ else
625
+ method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
626
+ {
627
+ json_name: json_name, attr_name: attr_name, scalar: true,
628
+ field_klass: field_klass,
629
+ to_method: :"json_to_#{method_suffix}",
630
+ from_method: :"json_from_#{method_suffix}"
631
+ }
632
+ end
633
+ end
634
+
635
+ def build_inline_field_json_mapping(field_def)
636
+ json_name = field_def.name
637
+ attr_name = Utils.safe_attr(field_def.name)
638
+ has_flags = field_has_flags?(field_def)
639
+
640
+ if has_flags
641
+ field_klass = @g.classes[@g.scoped_field_name(field_def.name)]
642
+ method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
643
+ {
644
+ json_name: json_name, attr_name: attr_name, scalar: false,
645
+ singleton_or_array: true, field_klass: field_klass,
646
+ to_method: :"json_soa_to_#{method_suffix}",
647
+ from_method: :"json_soa_from_#{method_suffix}"
648
+ }
649
+ else
650
+ { json_name: json_name, attr_name: attr_name, scalar: false }
651
+ end
652
+ end
653
+
654
+ def field_has_flags?(field_def, _field_ref = nil)
655
+ return false unless field_def
656
+
657
+ field_def.define_flag&.any? || field_def.flag&.any? || field_def.json_value_key || field_def.json_value_key_flag
658
+ end
659
+
660
+ def collect_json_assembly_mappings(assembly_def)
661
+ mappings = []
662
+ model = assembly_def.model
663
+ return mappings unless model
664
+
665
+ mappings.concat(collect_model_json_assembly_mappings(model))
666
+ mappings
667
+ end
668
+
669
+ def collect_model_json_assembly_mappings(model)
670
+ mappings = []
671
+
672
+ model.assembly&.each do |ar|
673
+ ref_name = ar.ref
674
+ next unless ref_name
675
+
676
+ group_as = ar.group_as
677
+ json_name = group_as&.name || ar.use_name&.content || ref_name
678
+ attr_name = group_as&.in_xml == "GROUPED" ? Utils.safe_attr(group_as.name) : Utils.safe_attr(ref_name)
679
+ mapping = { json_name: json_name, attr_name: attr_name }
680
+ if group_as&.in_json == "BY_KEY"
681
+ asm_def = @g.assembly_defs[ref_name]
682
+ json_key_flag = asm_def&.json_key&.flag_ref
683
+ asm_klass = @g.classes["Assembly_#{ref_name.gsub('-', '_')}"]
684
+ mapping[:by_key] = true
685
+ mapping[:asm_klass] = asm_klass
686
+ mapping[:json_key_flag] = json_key_flag
687
+ mapping[:grouped] = true if group_as&.in_xml == "GROUPED"
688
+ if group_as&.in_xml == "GROUPED"
689
+ mapping[:child_attr] = Utils.safe_attr(ref_name)
690
+ end
691
+ else
692
+ check_assembly_soa(mapping, group_as, attr_name, json_name)
693
+ end
694
+ mappings << mapping
695
+ end
696
+
697
+ model.define_assembly&.each do |ad|
698
+ next unless ad.name
699
+
700
+ group_as = ad.group_as
701
+ json_name = group_as&.name || ad.name
702
+ attr_name = Utils.safe_attr(ad.name)
703
+ mapping = { json_name: json_name, attr_name: attr_name }
704
+ if group_as&.in_json == "BY_KEY"
705
+ json_key_flag = ad.json_key&.flag_ref
706
+ mapping[:by_key] = true
707
+ mapping[:json_key_flag] = json_key_flag
708
+ else
709
+ check_assembly_soa(mapping, group_as, attr_name, json_name)
710
+ end
711
+ mappings << mapping
712
+ end
713
+
714
+ model.choice&.each do |c|
715
+ c.assembly&.each do |ar|
716
+ ref_name = ar.ref
717
+ next unless ref_name
718
+
719
+ group_as = ar.group_as
720
+ json_name = group_as&.name || ar.use_name&.content || ref_name
721
+ attr_name = Utils.safe_attr(ref_name)
722
+ mapping = { json_name: json_name, attr_name: attr_name }
723
+ check_assembly_soa(mapping, group_as, attr_name, json_name)
724
+ mappings << mapping
725
+ end
726
+ c.define_assembly&.each do |ad|
727
+ next unless ad.name
728
+
729
+ group_as = ad.group_as
730
+ json_name = group_as&.name || ad.name
731
+ attr_name = Utils.safe_attr(ad.name)
732
+ mapping = { json_name: json_name, attr_name: attr_name }
733
+ check_assembly_soa(mapping, group_as, attr_name, json_name)
734
+ mappings << mapping
735
+ end
736
+ end
737
+
738
+ model.choice_group&.each do |cg|
739
+ group_as = cg.group_as
740
+ json_name = group_as&.name
741
+ cg.assembly&.each do |ar|
742
+ ref_name = ar.ref
743
+ next unless ref_name
744
+
745
+ name = json_name || ar.use_name&.content || ref_name
746
+ attr_name = Utils.safe_attr(ref_name)
747
+ mapping = { json_name: name, attr_name: attr_name }
748
+ check_assembly_soa(mapping, group_as, attr_name, name)
749
+ mappings << mapping
750
+ end
751
+ cg.define_assembly&.each do |ad|
752
+ next unless ad.name
753
+
754
+ name = json_name || ad.name
755
+ attr_name = Utils.safe_attr(ad.name)
756
+ mapping = { json_name: name, attr_name: attr_name }
757
+ check_assembly_soa(mapping, group_as, attr_name, name)
758
+ mappings << mapping
759
+ end
760
+ end
761
+
762
+ mappings
763
+ end
764
+
765
+ def check_assembly_soa(mapping, group_as, attr_name, json_name)
766
+ is_soa = group_as&.in_json == "SINGLETON_OR_ARRAY" || group_as.nil?
767
+ return unless is_soa
768
+
769
+ method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
770
+ mapping[:singleton_or_array] = true
771
+ mapping[:to_method] = :"json_assembly_soa_to_#{method_suffix}"
772
+ mapping[:from_method] = :"json_assembly_soa_from_#{method_suffix}"
773
+ asm_klass = @g.classes["Assembly_#{attr_name.to_s.gsub('-', '_')}"]
774
+ mapping[:asm_klass] = asm_klass if asm_klass
775
+ end
776
+
777
+ # ── Custom Callback Builders ──────────────────────────────────────
778
+
779
+ def build_vk_flag_field_callbacks(parent_klass, field_klass, json_name,
780
+ attr_sym)
781
+ key_attr = field_klass.instance_variable_get(:@json_vk_flag_key_attr)
782
+ other_flag_maps = field_klass.instance_variable_get(:@json_vk_flag_other_flag_maps)
783
+ known_json_keys = other_flag_maps.map(&:first)
784
+
785
+ from_method = :"json_from_vkf_#{attr_sym}_#{json_name.gsub('-', '_')}"
786
+ to_method = :"json_to_vkf_#{attr_sym}_#{json_name.gsub('-', '_')}"
787
+
788
+ parent_klass.define_method(from_method) do |instance, value|
789
+ items = case value
790
+ when Array then value
791
+ when Hash then [value]
792
+ when nil then []
793
+ else [value]
794
+ end
795
+ parsed = items.map do |item|
796
+ item = item.dup
797
+ key_val = nil
798
+ content_val = nil
799
+ item.each do |k, v|
800
+ unless known_json_keys.include?(k)
801
+ key_val = k
802
+ content_val = v
803
+ end
804
+ end
805
+ obj = field_klass.allocate
806
+ obj.instance_variable_set(:@using_default, {})
807
+ obj.instance_variable_set(:@lutaml_register, :default)
808
+ obj.instance_variable_set("@#{key_attr}", key_val)
809
+ obj.instance_variable_set(:@content, content_val)
810
+ other_flag_maps.each do |json_key, attr_name|
811
+ if item.key?(json_key)
812
+ obj.instance_variable_set("@#{attr_name}", item[json_key])
813
+ end
814
+ end
815
+ obj
816
+ end
817
+ instance.instance_variable_set("@#{attr_sym}", parsed)
818
+ end
819
+
820
+ parent_klass.define_method(to_method) do |instance, doc|
821
+ current = instance.instance_variable_get("@#{attr_sym}")
822
+ if current.is_a?(Array)
823
+ doc[json_name] = current.map do |item|
824
+ key_val = item.instance_variable_get("@#{key_attr}")
825
+ content_val = item.instance_variable_get(:@content)
826
+ result = { key_val => content_val }
827
+ other_flag_maps.each do |json_key, attr_name|
828
+ val = item.instance_variable_get("@#{attr_name}")
829
+ result[json_key] = val if val
830
+ end
831
+ result
832
+ end
833
+ end
834
+ end
835
+
836
+ { from_method: from_method, to_method: to_method }
837
+ end
838
+
839
+ def build_by_key_field_callbacks(parent_klass, field_klass, json_name,
840
+ attr_sym, json_key_flag)
841
+ key_attr = Utils.safe_attr(json_key_flag)
842
+ field_klass && field_klass.instance_variable_get(:@json_vk_flag_key_attr).nil? &&
843
+ field_klass.attributes.any? do |k, _|
844
+ k != :content && k.to_s != key_attr.to_s
845
+ end
846
+
847
+ from_method = :"json_from_bykey_#{attr_sym}_#{json_name.gsub('-', '_')}"
848
+ to_method = :"json_to_bykey_#{attr_sym}_#{json_name.gsub('-', '_')}"
849
+
850
+ parent_klass.define_method(from_method) do |instance, value|
851
+ return unless value.is_a?(Hash)
852
+
853
+ parsed = value.map do |k, v|
854
+ obj = if field_klass
855
+ field_klass.allocate.tap do |o|
856
+ o.instance_variable_set(:@using_default, {})
857
+ o.instance_variable_set(:@lutaml_register, :default)
858
+ o.instance_variable_set("@#{key_attr}", k)
859
+ if v.is_a?(Hash)
860
+ v.each do |vk, vv|
861
+ attr_sym_local = vk.gsub("-", "_").to_sym
862
+ begin
863
+ o.instance_variable_set("@#{attr_sym_local}", vv)
864
+ rescue StandardError
865
+ end
866
+ end
867
+ else
868
+ o.instance_variable_set(:@content, v)
869
+ end
870
+ end
871
+ else
872
+ k
873
+ end
874
+ obj
875
+ end
876
+ instance.instance_variable_set("@#{attr_sym}", parsed)
877
+ end
878
+
879
+ parent_klass.define_method(to_method) do |instance, doc|
880
+ current = instance.instance_variable_get("@#{attr_sym}")
881
+ if current.is_a?(Array)
882
+ result = {}
883
+ current.each do |item|
884
+ if field_klass
885
+ key_val = item.instance_variable_get("@#{key_attr}")
886
+ content_val = item.instance_variable_get(:@content)
887
+ if field_klass.attributes.keys.any? do |k|
888
+ k != :content && k.to_s != key_attr.to_s && item.instance_variable_get("@#{k}")
889
+ end
890
+ obj = {}
891
+ field_klass.attributes.each_key do |attr_k|
892
+ next if attr_k.to_s == key_attr.to_s
893
+
894
+ v = item.instance_variable_get("@#{attr_k}")
895
+ obj[attr_k.to_s] = v if v
896
+ end
897
+ result[key_val] = obj
898
+ else
899
+ result[key_val] = content_val
900
+ end
901
+ end
902
+ end
903
+ doc[json_name] = result
904
+ end
905
+ end
906
+
907
+ { from_method: from_method, to_method: to_method }
908
+ end
909
+
910
+ def build_by_key_assembly_callbacks(parent_klass, asm_klass, json_name,
911
+ attr_sym, json_key_flag, grouped: false, child_attr: nil)
912
+ key_attr = Utils.safe_attr(json_key_flag)
913
+
914
+ from_method = :"json_from_bykey_asm_#{attr_sym}_#{json_name.gsub('-',
915
+ '_')}"
916
+ to_method = :"json_to_bykey_asm_#{attr_sym}_#{json_name.gsub('-', '_')}"
917
+
918
+ parent_klass.define_method(from_method) do |instance, value|
919
+ return unless value.is_a?(Hash)
920
+
921
+ parsed = value.map do |k, v|
922
+ if asm_klass
923
+ obj = if v.is_a?(Hash)
924
+ asm_klass.of_json(v)
925
+ else
926
+ asm_klass.new
927
+ end
928
+ obj.instance_variable_set("@#{key_attr}", k)
929
+ obj
930
+ else
931
+ k
932
+ end
933
+ end
934
+
935
+ if grouped && child_attr
936
+ wrapper = instance.instance_variable_get("@#{attr_sym}")
937
+ unless wrapper
938
+ attr_type = instance.class.attributes[attr_sym]
939
+ wrapper = attr_type.type.new
940
+ end
941
+ wrapper.instance_variable_set("@#{child_attr}", parsed)
942
+ instance.instance_variable_set("@#{attr_sym}", wrapper)
943
+ else
944
+ instance.instance_variable_set("@#{attr_sym}", parsed)
945
+ end
946
+ end
947
+
948
+ parent_klass.define_method(to_method) do |instance, doc|
949
+ current = instance.instance_variable_get("@#{attr_sym}")
950
+ items = if grouped && current && child_attr
951
+ current.send(child_attr)
952
+ else
953
+ current
954
+ end
955
+
956
+ if items.is_a?(Array)
957
+ result = {}
958
+ items.each do |item|
959
+ next unless asm_klass
960
+
961
+ key_val = item.instance_variable_get("@#{key_attr}")
962
+ if item.is_a?(Lutaml::Model::Serializable)
963
+ sub = asm_klass.as_json(item)
964
+ key_json_name = asm_klass.mappings_for(:json).instance_variable_get(:@mappings)
965
+ .find do |_map_key, rule|
966
+ rule.to.to_s == key_attr.to_s
967
+ end&.first
968
+ sub.delete(key_json_name) if key_json_name
969
+ result[key_val] = sub.empty? ? {} : sub
970
+ else
971
+ result[key_val] = {}
972
+ end
973
+ end
974
+ doc[json_name] = result
975
+ end
976
+ end
977
+
978
+ { from_method: from_method, to_method: to_method }
979
+ end
980
+
981
+ def define_scalar_field_callbacks(klass, field_mappings)
982
+ field_mappings.each do |mapping|
983
+ next unless mapping[:scalar]
984
+
985
+ field_klass = mapping[:field_klass]
986
+ attr_sym = mapping[:attr_name]
987
+
988
+ klass.define_method(mapping[:from_method]) do |instance, value|
989
+ if value.is_a?(Array)
990
+ instance.instance_variable_set("@#{attr_sym}", value.map do |v|
991
+ field_klass.new(content: v)
992
+ end)
993
+ elsif value
994
+ instance.instance_variable_set("@#{attr_sym}",
995
+ field_klass.new(content: value))
996
+ end
997
+ end
998
+
999
+ klass.define_method(mapping[:to_method]) do |instance, doc|
1000
+ current = instance.instance_variable_get("@#{attr_sym}")
1001
+ if current.is_a?(Array)
1002
+ doc[mapping[:json_name]] = current.map do |item|
1003
+ item.respond_to?(:content) ? item.content : item
1004
+ end
1005
+ elsif current
1006
+ doc[mapping[:json_name]] =
1007
+ current.respond_to?(:content) ? current.content : current
1008
+ end
1009
+ end
1010
+ end
1011
+ end
1012
+
1013
+ # ── Model Processing ──────────────────────────────────────────────
1014
+
1015
+ def process_model(klass, model)
1016
+ unless klass.instance_variable_defined?(:@occurrence_constraints)
1017
+ klass.instance_variable_set(:@occurrence_constraints, {})
1018
+ end
1019
+ occ = klass.instance_variable_get(:@occurrence_constraints)
1020
+
1021
+ model.field&.each do |fr|
1022
+ add_field_reference(klass, fr)
1023
+ record_occurrence_constraint(occ, fr)
1024
+ end
1025
+ model.assembly&.each do |ar|
1026
+ add_assembly_reference(klass, ar)
1027
+ record_occurrence_constraint(occ, ar)
1028
+ end
1029
+ model.define_field&.each { |fd| add_inline_field(klass, fd) }
1030
+ model.define_assembly&.each { |ad| add_inline_assembly(klass, ad) }
1031
+ model.choice&.each { |c| process_choice(klass, c) }
1032
+ model.choice_group&.each { |cg| process_choice_group(klass, cg) }
1033
+ add_any_content(klass) if model.any
1034
+
1035
+ unless klass.method_defined?(:validate_occurrences)
1036
+ occ_ref = klass.instance_variable_get(:@occurrence_constraints)
1037
+ klass.define_method(:validate_occurrences) do
1038
+ ConstraintValidator.validate_occurrences(self, occ_ref)
1039
+ end
1040
+ end
1041
+ end
1042
+
1043
+ def record_occurrence_constraint(occ, ref)
1044
+ ref_name = ref.ref
1045
+ return unless ref_name
1046
+
1047
+ attr_name = Utils.safe_attr(ref_name)
1048
+ min = ref.min_occurs.to_i
1049
+ max_raw = ref.max_occurs
1050
+ max = max_raw == "unbounded" ? nil : max_raw&.to_i
1051
+
1052
+ occ[attr_name] = { min: min, max: max } if min.positive? || max
1053
+ end
1054
+
1055
+ def add_field_reference(klass, field_ref)
1056
+ ref_name = field_ref.ref
1057
+ return unless ref_name
1058
+
1059
+ field_klass = @g.classes["Field_#{ref_name.gsub('-', '_')}"]
1060
+ return unless field_klass
1061
+
1062
+ collection = Utils.unbounded?(field_ref.max_occurs)
1063
+ group_as = field_ref.group_as
1064
+
1065
+ if group_as&.in_xml == "GROUPED"
1066
+ group_attr = Utils.safe_attr(group_as.name)
1067
+ wrapper_klass = Class.new(Lutaml::Model::Serializable)
1068
+ child_attr = Utils.safe_attr(ref_name)
1069
+ wrapper_klass.attribute child_attr, field_klass, collection: true
1070
+ wrapper_klass.class_eval do
1071
+ xml do
1072
+ element group_as.name
1073
+ map_element ref_name, to: child_attr
1074
+ end
1075
+ end
1076
+ klass.attribute group_attr, wrapper_klass
1077
+ else
1078
+ attr_name = Utils.safe_attr(ref_name)
1079
+ klass.attribute attr_name, field_klass, collection: collection
1080
+ end
1081
+ end
1082
+
1083
+ def add_assembly_reference(klass, assembly_ref)
1084
+ ref_name = assembly_ref.ref
1085
+ return unless ref_name
1086
+
1087
+ assembly_klass = @g.classes["Assembly_#{ref_name.gsub('-', '_')}"] ||
1088
+ @g.create_placeholder_assembly(ref_name)
1089
+
1090
+ collection = Utils.unbounded?(assembly_ref.max_occurs)
1091
+ group_as = assembly_ref.group_as
1092
+ xml_name = @g.assembly_xml_element_name(assembly_ref)
1093
+
1094
+ if group_as&.in_xml == "GROUPED"
1095
+ group_attr = Utils.safe_attr(group_as.name)
1096
+ child_attr = Utils.safe_attr(ref_name)
1097
+ wrapper_klass = Class.new(Lutaml::Model::Serializable)
1098
+ wrapper_klass.attribute child_attr, assembly_klass, collection: true
1099
+ wrapper_klass.class_eval do
1100
+ xml do
1101
+ element group_as.name
1102
+ map_element xml_name, to: child_attr
1103
+ end
1104
+ end
1105
+ klass.attribute group_attr, wrapper_klass
1106
+ else
1107
+ attr_name = Utils.safe_attr(ref_name)
1108
+ klass.attribute attr_name, assembly_klass, collection: collection
1109
+ end
1110
+ end
1111
+
1112
+ def add_inline_field(klass, field_def)
1113
+ return unless field_def.name
1114
+
1115
+ attr_name = Utils.safe_attr(field_def.name)
1116
+ is_markup = TypeMapper.markup?(field_def.as_type)
1117
+ is_multiline = TypeMapper.multiline?(field_def.as_type)
1118
+ content_type = TypeMapper.map(field_def.as_type)
1119
+ collection = Utils.unbounded?(field_def.max_occurs)
1120
+ has_flags = field_def.define_flag&.any? || field_def.flag&.any?
1121
+
1122
+ if is_markup || is_multiline
1123
+ inline_klass = Class.new(Lutaml::Model::Serializable)
1124
+ if is_multiline
1125
+ FieldFactory.apply_markup_multiline_attributes(inline_klass)
1126
+ else
1127
+ FieldFactory.apply_markup_attributes(inline_klass)
1128
+ end
1129
+
1130
+ field_def.define_flag&.each do |f|
1131
+ @g.add_inline_flag(inline_klass, f)
1132
+ end
1133
+ field_def.flag&.each { |f| @g.add_flag_reference(inline_klass, f) }
1134
+
1135
+ inline_name = field_def.name
1136
+ inline_flag_defs = field_def.define_flag || []
1137
+ inline_flag_refs = field_def.flag || []
1138
+ inline_flag_attr_maps = inline_flag_defs.filter_map do |f|
1139
+ [f.name, Utils.safe_attr(f.name)] if f.name
1140
+ end
1141
+ inline_flag_ref_maps = inline_flag_refs.filter_map do |f|
1142
+ [f.ref, Utils.safe_attr(f.ref)] if f.ref
1143
+ end
1144
+
1145
+ inline_klass.class_eval do
1146
+ xml do
1147
+ element inline_name
1148
+ mixed_content
1149
+ ordered
1150
+ map_content to: :content
1151
+ map_element "a", to: :a
1152
+ map_element "insert", to: :insert
1153
+ map_element "br", to: :br
1154
+ map_element "code", to: :code
1155
+ map_element "em", to: :em
1156
+ map_element "i", to: :i
1157
+ map_element "b", to: :b
1158
+ map_element "strong", to: :strong
1159
+ map_element "sub", to: :sub
1160
+ map_element "sup", to: :sup
1161
+ map_element "q", to: :q
1162
+ map_element "img", to: :img
1163
+
1164
+ if is_multiline
1165
+ map_element "p", to: :p
1166
+ map_element "h1", to: :h1
1167
+ map_element "h2", to: :h2
1168
+ map_element "h3", to: :h3
1169
+ map_element "h4", to: :h4
1170
+ map_element "h5", to: :h5
1171
+ map_element "h6", to: :h6
1172
+ map_element "ul", to: :ul
1173
+ map_element "ol", to: :ol
1174
+ map_element "pre", to: :pre
1175
+ map_element "hr", to: :hr
1176
+ map_element "blockquote", to: :blockquote
1177
+ map_element "table", to: :table
1178
+ end
1179
+
1180
+ inline_flag_attr_maps.each do |xml_name, attr_sym|
1181
+ map_attribute xml_name, to: attr_sym
1182
+ end
1183
+
1184
+ inline_flag_ref_maps.each do |xml_name, attr_sym|
1185
+ map_attribute xml_name, to: attr_sym
1186
+ end
1187
+ end
1188
+ end
1189
+
1190
+ klass.attribute attr_name, inline_klass, collection: collection
1191
+ elsif has_flags
1192
+ inline_klass = Class.new(Lutaml::Model::Serializable)
1193
+ inline_klass.attribute :content, content_type
1194
+ field_def.define_flag&.each do |f|
1195
+ @g.add_inline_flag(inline_klass, f)
1196
+ end
1197
+ field_def.flag&.each { |f| @g.add_flag_reference(inline_klass, f) }
1198
+
1199
+ flag_attr_maps = field_def.define_flag&.filter_map do |f|
1200
+ [f.name, Utils.safe_attr(f.name)] if f.name
1201
+ end || []
1202
+ flag_ref_maps = field_def.flag&.filter_map do |f|
1203
+ [f.ref, Utils.safe_attr(f.ref)] if f.ref
1204
+ end || []
1205
+
1206
+ inline_vk = field_def.json_value_key || TypeMapper.json_value_key(field_def.as_type)
1207
+ inline_name = field_def.name
1208
+ inline_klass.class_eval do
1209
+ xml do
1210
+ element inline_name
1211
+ map_content to: :content
1212
+ flag_attr_maps.each do |xml_name, attr_sym|
1213
+ map_attribute xml_name, to: attr_sym
1214
+ end
1215
+ flag_ref_maps.each do |xml_name, attr_sym|
1216
+ map_attribute xml_name, to: attr_sym
1217
+ end
1218
+ end
1219
+ key_value do
1220
+ root inline_name
1221
+ map inline_vk, to: :content
1222
+ flag_attr_maps.each do |xml_name, attr_sym|
1223
+ map xml_name, to: attr_sym
1224
+ end
1225
+ flag_ref_maps.each do |xml_name, attr_sym|
1226
+ map xml_name, to: attr_sym
1227
+ end
1228
+ end
1229
+ end
1230
+
1231
+ klass_name = @g.scoped_field_name(field_def.name)
1232
+ @g.classes[klass_name] = inline_klass
1233
+
1234
+ klass.attribute attr_name, inline_klass, collection: collection
1235
+ else
1236
+ klass.attribute attr_name, content_type, collection: collection
1237
+ end
1238
+ end
1239
+
1240
+ def add_inline_assembly(klass, assembly_def)
1241
+ return unless assembly_def.name
1242
+
1243
+ attr_name = Utils.safe_attr(assembly_def.name)
1244
+ collection = Utils.unbounded?(assembly_def.max_occurs)
1245
+
1246
+ inline_klass = Class.new(Lutaml::Model::Serializable)
1247
+
1248
+ assembly_def.define_flag&.each do |f|
1249
+ @g.add_inline_flag(inline_klass, f)
1250
+ end
1251
+ assembly_def.flag&.each { |f| @g.add_flag_reference(inline_klass, f) }
1252
+
1253
+ process_model(inline_klass, assembly_def.model) if assembly_def.model
1254
+
1255
+ inline_name = assembly_def.name
1256
+ inline_flag_defs = assembly_def.define_flag || []
1257
+ inline_flag_refs = assembly_def.flag || []
1258
+ inline_child_mappings = assembly_def.model ? collect_inline_child_mappings(assembly_def) : []
1259
+ inline_flag_attr_maps = inline_flag_defs.filter_map do |f|
1260
+ [f.name, Utils.safe_attr(f.name)] if f.name
1261
+ end
1262
+ inline_flag_ref_maps = inline_flag_refs.filter_map do |f|
1263
+ [f.ref, Utils.safe_attr(f.ref)] if f.ref
1264
+ end
1265
+
1266
+ inline_klass.class_eval do
1267
+ xml do
1268
+ element inline_name
1269
+ ordered
1270
+
1271
+ inline_flag_attr_maps.each do |xml_name, attr_name|
1272
+ map_attribute xml_name, to: attr_name
1273
+ end
1274
+
1275
+ inline_flag_ref_maps.each do |xml_name, attr_name|
1276
+ map_attribute xml_name, to: attr_name
1277
+ end
1278
+
1279
+ inline_child_mappings.each do |mapping|
1280
+ map_element mapping[:xml_name], to: mapping[:attr_name]
1281
+ end
1282
+ end
1283
+ end
1284
+
1285
+ klass.attribute attr_name, inline_klass, collection: collection
1286
+
1287
+ build_inline_assembly_json(klass, inline_klass, inline_name,
1288
+ assembly_def)
1289
+ end
1290
+
1291
+ def collect_inline_child_mappings(assembly_def)
1292
+ model = assembly_def.model
1293
+ return [] unless model
1294
+
1295
+ collect_model_child_mappings(model)
1296
+ end
1297
+
1298
+ def build_inline_assembly_json(_parent_klass, inline_klass, inline_name,
1299
+ assembly_def)
1300
+ flag_defs = assembly_def.define_flag || []
1301
+ flag_refs = assembly_def.flag || []
1302
+
1303
+ inline_flag_attr_maps = flag_defs.filter_map do |f|
1304
+ [f.name, Utils.safe_attr(f.name)] if f.name
1305
+ end
1306
+ inline_flag_ref_maps = flag_refs.filter_map do |f|
1307
+ [f.ref, Utils.safe_attr(f.ref)] if f.ref
1308
+ end
1309
+
1310
+ json_field_mappings = collect_json_field_mappings(assembly_def)
1311
+ json_assembly_mappings = collect_json_assembly_mappings(assembly_def)
1312
+
1313
+ has_nested_asm = json_assembly_mappings.any?
1314
+
1315
+ if has_nested_asm
1316
+ build_inline_assembly_json_custom(
1317
+ inline_klass, inline_name, inline_flag_attr_maps, inline_flag_ref_maps,
1318
+ json_field_mappings, json_assembly_mappings
1319
+ )
1320
+ else
1321
+ build_inline_assembly_json_standard(
1322
+ inline_klass, inline_name, inline_flag_attr_maps, inline_flag_ref_maps,
1323
+ json_field_mappings
1324
+ )
1325
+ end
1326
+ end
1327
+
1328
+ def build_inline_assembly_json_standard(inline_klass, inline_name,
1329
+ inline_flag_attr_maps, inline_flag_ref_maps,
1330
+ json_field_mappings)
1331
+ regular_field_mappings = json_field_mappings.reject do |m|
1332
+ m[:vk_flag] || m[:by_key]
1333
+ end
1334
+ vk_flag_mappings = json_field_mappings.select { |m| m[:vk_flag] }
1335
+ by_key_mappings = json_field_mappings.select { |m| m[:by_key] }
1336
+
1337
+ inline_klass.class_eval do
1338
+ key_value do
1339
+ root inline_name
1340
+
1341
+ inline_flag_attr_maps.each do |xml_name, attr_name|
1342
+ map xml_name, to: attr_name
1343
+ end
1344
+
1345
+ inline_flag_ref_maps.each do |xml_name, attr_name|
1346
+ map xml_name, to: attr_name
1347
+ end
1348
+
1349
+ regular_field_mappings.each do |mapping|
1350
+ if mapping[:scalar]
1351
+ map mapping[:json_name], to: mapping[:attr_name],
1352
+ with: { to: mapping[:to_method], from: mapping[:from_method] }
1353
+ else
1354
+ map mapping[:json_name], to: mapping[:attr_name],
1355
+ render_empty: true
1356
+ end
1357
+ end
1358
+ end
1359
+ end
1360
+
1361
+ define_scalar_field_callbacks(inline_klass, regular_field_mappings)
1362
+
1363
+ vk_flag_mappings.each do |mapping|
1364
+ callbacks = build_vk_flag_field_callbacks(
1365
+ inline_klass, mapping[:field_klass], mapping[:json_name], mapping[:attr_name]
1366
+ )
1367
+ inline_klass.class_eval do
1368
+ key_value do
1369
+ map mapping[:json_name], to: mapping[:attr_name],
1370
+ with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1371
+ end
1372
+ end
1373
+ end
1374
+
1375
+ by_key_mappings.each do |mapping|
1376
+ callbacks = build_by_key_field_callbacks(
1377
+ inline_klass, mapping[:field_klass], mapping[:json_name],
1378
+ mapping[:attr_name], mapping[:json_key_flag]
1379
+ )
1380
+ inline_klass.class_eval do
1381
+ key_value do
1382
+ map mapping[:json_name], to: mapping[:attr_name],
1383
+ with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1384
+ end
1385
+ end
1386
+ end
1387
+ end
1388
+
1389
+ def build_inline_assembly_json_custom(inline_klass, inline_name,
1390
+ inline_flag_attr_maps, inline_flag_ref_maps,
1391
+ json_field_mappings, json_assembly_mappings)
1392
+ regular_field_mappings = json_field_mappings.reject do |m|
1393
+ m[:vk_flag] || m[:by_key]
1394
+ end
1395
+ vk_flag_mappings = json_field_mappings.select { |m| m[:vk_flag] }
1396
+ by_key_mappings = json_field_mappings.select { |m| m[:by_key] }
1397
+
1398
+ json_assembly_mappings.each do |mapping|
1399
+ json_name = mapping[:json_name]
1400
+ attr_sym = mapping[:attr_name]
1401
+ mapping[:to_method] =
1402
+ :"json_to_asm_#{attr_sym}_#{json_name.gsub('-', '_')}"
1403
+ end
1404
+
1405
+ inline_klass.class_eval do
1406
+ key_value do
1407
+ root inline_name
1408
+
1409
+ inline_flag_attr_maps.each do |xml_name, attr_name|
1410
+ map xml_name, to: attr_name
1411
+ end
1412
+
1413
+ inline_flag_ref_maps.each do |xml_name, attr_name|
1414
+ map xml_name, to: attr_name
1415
+ end
1416
+
1417
+ regular_field_mappings.each do |mapping|
1418
+ if mapping[:scalar]
1419
+ map mapping[:json_name], to: mapping[:attr_name],
1420
+ with: { to: mapping[:to_method], from: mapping[:from_method] }
1421
+ else
1422
+ map mapping[:json_name], to: mapping[:attr_name],
1423
+ render_empty: true
1424
+ end
1425
+ end
1426
+
1427
+ json_assembly_mappings.each do |mapping|
1428
+ map mapping[:json_name], to: mapping[:attr_name],
1429
+ with: { to: mapping[:to_method] }
1430
+ end
1431
+ end
1432
+ end
1433
+
1434
+ define_scalar_field_callbacks(inline_klass, regular_field_mappings)
1435
+
1436
+ vk_flag_mappings.each do |mapping|
1437
+ callbacks = build_vk_flag_field_callbacks(
1438
+ inline_klass, mapping[:field_klass], mapping[:json_name], mapping[:attr_name]
1439
+ )
1440
+ inline_klass.class_eval do
1441
+ key_value do
1442
+ map mapping[:json_name], to: mapping[:attr_name],
1443
+ with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1444
+ end
1445
+ end
1446
+ end
1447
+
1448
+ by_key_mappings.each do |mapping|
1449
+ callbacks = build_by_key_field_callbacks(
1450
+ inline_klass, mapping[:field_klass], mapping[:json_name],
1451
+ mapping[:attr_name], mapping[:json_key_flag]
1452
+ )
1453
+ inline_klass.class_eval do
1454
+ key_value do
1455
+ map mapping[:json_name], to: mapping[:attr_name],
1456
+ with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1457
+ end
1458
+ end
1459
+ end
1460
+
1461
+ json_assembly_mappings.each do |mapping|
1462
+ attr_sym = mapping[:attr_name]
1463
+ to_method = mapping[:to_method]
1464
+ json_name = mapping[:json_name]
1465
+
1466
+ inline_klass.define_method(to_method) do |instance, doc|
1467
+ current = instance.instance_variable_get("@#{attr_sym}")
1468
+ if current
1469
+ if current.is_a?(Lutaml::Model::Serializable)
1470
+ sub = {}
1471
+ current.class.mappings_for(:json).instance_variable_get(:@mappings).each do |key, rule|
1472
+ val = current.send(rule.to)
1473
+ next if val.nil?
1474
+
1475
+ sub[key] = val.respond_to?(:content) ? val.content : val
1476
+ end
1477
+ doc[json_name] = sub.empty? ? {} : sub
1478
+ else
1479
+ doc[json_name] = current
1480
+ end
1481
+ end
1482
+ end
1483
+ end
1484
+ end
1485
+
1486
+ # ── Choice Handling ───────────────────────────────────────────────
1487
+
1488
+ def process_choice(klass, choice)
1489
+ choice.assembly&.each { |ar| add_assembly_reference(klass, ar) }
1490
+ choice.field&.each { |fr| add_field_reference(klass, fr) }
1491
+ choice.define_assembly&.each { |ad| add_inline_assembly(klass, ad) }
1492
+ choice.define_field&.each { |fd| add_inline_field(klass, fd) }
1493
+ end
1494
+
1495
+ def process_choice_group(klass, choice_group)
1496
+ choice_group.assembly&.each do |ar|
1497
+ add_grouped_assembly_reference(klass, ar)
1498
+ end
1499
+ choice_group.field&.each { |fr| add_grouped_field_reference(klass, fr) }
1500
+ choice_group.define_assembly&.each do |ad|
1501
+ add_inline_assembly(klass, ad)
1502
+ end
1503
+ choice_group.define_field&.each { |fd| add_inline_field(klass, fd) }
1504
+ end
1505
+
1506
+ def add_grouped_assembly_reference(klass, grouped_ref)
1507
+ ref_name = grouped_ref.ref
1508
+ return unless ref_name
1509
+
1510
+ assembly_klass = @g.classes["Assembly_#{ref_name.gsub('-', '_')}"] ||
1511
+ @g.create_placeholder_assembly(ref_name)
1512
+
1513
+ attr_name = Utils.safe_attr(ref_name)
1514
+ klass.attribute attr_name, assembly_klass
1515
+ end
1516
+
1517
+ def add_grouped_field_reference(klass, grouped_ref)
1518
+ ref_name = grouped_ref.ref
1519
+ return unless ref_name
1520
+
1521
+ field_klass = @g.classes["Field_#{ref_name.gsub('-', '_')}"]
1522
+ return unless field_klass
1523
+
1524
+ attr_name = Utils.safe_attr(ref_name)
1525
+ klass.attribute attr_name, field_klass
1526
+ end
1527
+
1528
+ def add_any_content(klass)
1529
+ klass.attribute :any_content, :string
1530
+ end
1531
+
1532
+ # ── JSON Root Handling ────────────────────────────────────────────
1533
+
1534
+ def add_json_root_handling(klass, json_root)
1535
+ klass.instance_variable_set(:@json_root_name, json_root)
1536
+ class << klass
1537
+ attr_reader :json_root_name
1538
+ end
1539
+
1540
+ original_of_json = klass.method(:of_json)
1541
+ klass.define_singleton_method(:of_json) do |doc, options = {}|
1542
+ if doc.is_a?(Hash) && doc.key?(json_root_name)
1543
+ original_of_json.call(doc[json_root_name], options)
1544
+ else
1545
+ original_of_json.call(doc, options)
1546
+ end
1547
+ end
1548
+
1549
+ original_to_json = klass.method(:to_json)
1550
+ klass.define_singleton_method(:to_json) do |instance, options = {}|
1551
+ json_str = original_to_json.call(instance, options)
1552
+ { json_root_name => JSON.parse(json_str) }.to_json
1553
+ end
1554
+
1555
+ klass.send(:define_method, :to_json) do |options = {}|
1556
+ self.class.to_json(self, options)
1557
+ end
1558
+
1559
+ original_of_yaml = klass.method(:of_yaml)
1560
+ klass.define_singleton_method(:of_yaml) do |doc, options = {}|
1561
+ if doc.is_a?(Hash) && doc.key?(json_root_name)
1562
+ original_of_yaml.call(doc[json_root_name], options)
1563
+ else
1564
+ original_of_yaml.call(doc, options)
1565
+ end
1566
+ end
1567
+
1568
+ original_to_yaml = klass.method(:to_yaml)
1569
+ klass.define_singleton_method(:to_yaml) do |instance, options = {}|
1570
+ yaml_str = original_to_yaml.call(instance, options)
1571
+ data = YAML.safe_load(yaml_str,
1572
+ permitted_classes: [Date, DateTime, Time,
1573
+ Symbol])
1574
+ { json_root_name => data }.to_yaml
1575
+ end
1576
+
1577
+ klass.send(:define_method, :to_yaml) do |options = {}|
1578
+ self.class.to_yaml(self, options)
1579
+ end
1580
+ end
1581
+ end
1582
+ end
1583
+ end