metaschema 0.2.1 → 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.
@@ -1,6 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "model_generator/utils"
4
+ require_relative "model_generator/field_factory"
5
+ require_relative "model_generator/assembly_factory"
6
+ require_relative "model_generator/services/collapsibles_collapser"
7
+ require_relative "model_generator/services/field_serializer"
8
+ require_relative "model_generator/services/field_deserializer"
9
+
3
10
  module Metaschema
11
+ # Generates Ruby classes (Lutaml::Model::Serializable subclasses) from
12
+ # NIST Metaschema definitions. The generated classes support XML and JSON
13
+ # round-tripping with full fidelity.
14
+ #
15
+ # Delegates field class creation to FieldFactory and assembly class
16
+ # creation to AssemblyFactory. This class handles import resolution,
17
+ # augment application, and shared utilities.
4
18
  class ModelGenerator
5
19
  class << self
6
20
  def generate_from_file(metaschema_path, base_path: nil)
@@ -25,8 +39,9 @@ split: false)
25
39
  end
26
40
  end
27
41
 
28
- RESERVED_WORDS = %i[class module method hash object_id nil? is_a? kind_of?
29
- instance_of? respond_to? send].freeze
42
+ # Shared state accessed by FieldFactory and AssemblyFactory via @g
43
+ attr_reader :classes, :field_defs, :assembly_defs, :flag_defs
44
+ attr_accessor :current_assembly_name
30
45
 
31
46
  def generate(metaschema, base_path: nil)
32
47
  @classes = {}
@@ -34,6 +49,7 @@ split: false)
34
49
  @assembly_defs = {}
35
50
  @field_defs = {}
36
51
  @namespace = metaschema.namespace
52
+ @current_assembly_name = nil
37
53
 
38
54
  # Resolve imports — merge definitions from imported modules
39
55
  resolve_and_merge_imports(metaschema, base_path)
@@ -46,27 +62,96 @@ split: false)
46
62
 
47
63
  # Phase 1: Create field classes for all definitions (top-level + imported)
48
64
  @field_defs.each_value do |fd|
49
- create_field_class(fd) unless @classes.key?("Field_#{safe_attr(fd.name)}")
65
+ next if @classes.key?("Field_#{Utils.safe_attr(fd.name)}")
66
+
67
+ FieldFactory.new(fd, self).create
50
68
  end
51
69
 
52
- # Phase 1: Create assembly placeholders for all definitions (top-level + imported)
70
+ # Phase 1: Create assembly placeholders for all definitions
71
+ # Phase 2: Populate assembly classes for all definitions
53
72
  @assembly_defs.each_value do |ad|
54
- create_assembly_placeholder(ad) unless @classes.key?("Assembly_#{safe_attr(ad.name)}")
55
-
56
- # Phase 2: Populate assembly classes for all definitions
57
- populate_assembly_class(ad) unless @classes["Assembly_#{safe_attr(ad.name)}"]&.instance_variable_get(:@populated)
73
+ factory = AssemblyFactory.new(ad, self)
74
+ factory.create_placeholder
75
+ factory.populate
58
76
  end
59
77
 
60
78
  @classes
61
79
  end
62
80
 
63
- private
81
+ # ── XML Element Name Resolution ──────────────────────────────────
82
+
83
+ def assembly_xml_element_name(assembly_ref)
84
+ ref_name = assembly_ref.ref
85
+ return ref_name unless ref_name
86
+
87
+ return assembly_ref.use_name.content if assembly_ref.use_name&.content
88
+
89
+ defn = @assembly_defs[ref_name]
90
+ return defn.use_name.content if defn&.use_name&.content
91
+
92
+ ref_name
93
+ end
94
+
95
+ def field_xml_element_name(field_ref)
96
+ ref_name = field_ref.ref
97
+ return ref_name unless ref_name
98
+
99
+ return field_ref.use_name.content if field_ref.use_name&.content
100
+
101
+ defn = @field_defs[ref_name]
102
+ return defn.use_name.content if defn&.use_name&.content
103
+
104
+ ref_name
105
+ end
106
+
107
+ # ── Shared Utilities (used by both factories) ──────────────────────
108
+
109
+ def add_inline_flag(klass, flag_def)
110
+ return unless flag_def.name
111
+
112
+ attr_name = Utils.safe_attr(flag_def.name)
113
+ type = TypeMapper.map(flag_def.as_type)
114
+ klass.attribute attr_name, type
115
+ end
116
+
117
+ def add_flag_reference(klass, flag_ref)
118
+ return unless flag_ref.ref
119
+
120
+ flag_name = flag_ref.ref
121
+ flag_def = @flag_defs[flag_name]
122
+ attr_name = Utils.safe_attr(flag_name)
123
+ type = flag_def ? TypeMapper.map(flag_def.as_type) : :string
124
+ klass.attribute attr_name, type
125
+ end
126
+
127
+ def scoped_field_name(field_name)
128
+ base = "Field_#{field_name.gsub('-', '_')}"
129
+ @current_assembly_name ? "#{base}_in_#{@current_assembly_name}" : base
130
+ end
131
+
132
+ def create_placeholder_assembly(name)
133
+ key = "Assembly_#{name.gsub('-', '_')}"
134
+ @classes[key] ||= Class.new(Lutaml::Model::Serializable)
135
+ end
136
+
137
+ # ── Constraint Validation Integration ──────────────────────────────
138
+
139
+ def apply_constraint_validation(klass, constraint_def)
140
+ return unless constraint_def
141
+
142
+ klass.instance_variable_set(:@metaschema_constraints, constraint_def)
143
+ klass.define_singleton_method(:metaschema_constraints) do
144
+ @metaschema_constraints
145
+ end
64
146
 
65
- def safe_attr(name)
66
- sym = name.gsub("-", "_").to_sym
67
- RESERVED_WORDS.include?(sym) ? :"#{sym}_attr" : sym
147
+ klass.define_method(:validate_constraints) do
148
+ validator = ConstraintValidator.new
149
+ validator.validate(self, self.class.metaschema_constraints)
150
+ end
68
151
  end
69
152
 
153
+ private
154
+
70
155
  # ── Import Resolution ──────────────────────────────────────────────
71
156
 
72
157
  def resolve_and_merge_imports(metaschema, base_path)
@@ -144,22 +229,18 @@ split: false)
144
229
  end
145
230
 
146
231
  def apply_augment_docs(target, augment)
147
- # Add formal-name if provided and target doesn't have one
148
232
  if augment.formal_name && !target.formal_name
149
233
  target.formal_name = augment.formal_name
150
234
  end
151
235
 
152
- # Add description if provided and target doesn't have one
153
236
  if augment.description && (!target.respond_to?(:description) || !target.description) && target.respond_to?(:description=)
154
237
  target.description = augment.description
155
238
  end
156
239
  end
157
240
 
158
241
  def apply_augment_flags(target, augment)
159
- # Add flag references to assembly/field definitions
160
242
  return unless augment.flag&.any? || augment.define_flag&.any?
161
243
 
162
- # Add flag references
163
244
  if target.respond_to?(:flag)
164
245
  existing_refs = (target.flag || []).map(&:ref)
165
246
  augment.flag.each do |fr|
@@ -169,7 +250,6 @@ split: false)
169
250
  end
170
251
  end
171
252
 
172
- # Add inline flag definitions
173
253
  if target.respond_to?(:define_flag)
174
254
  existing_names = (target.define_flag || []).map(&:name)
175
255
  augment.define_flag.each do |fd|
@@ -180,7 +260,7 @@ split: false)
180
260
  end
181
261
  end
182
262
 
183
- # ── Flag Definitions ──────────────────────────────────────────────
263
+ # ── Definition Collection ──────────────────────────────────────────
184
264
 
185
265
  def collect_flag_definitions(metaschema)
186
266
  metaschema.define_flag&.each do |flag_def|
@@ -196,1980 +276,5 @@ split: false)
196
276
  @field_defs[fd.name] = fd if fd.name
197
277
  end
198
278
  end
199
-
200
- # Resolve the XML element name for an assembly reference
201
- def assembly_xml_element_name(assembly_ref)
202
- ref_name = assembly_ref.ref
203
- return ref_name unless ref_name
204
-
205
- # Local override takes priority
206
- return assembly_ref.use_name.content if assembly_ref.use_name&.content
207
-
208
- # Check definition's use_name
209
- defn = @assembly_defs[ref_name]
210
- return defn.use_name.content if defn&.use_name&.content
211
-
212
- ref_name
213
- end
214
-
215
- # Resolve the XML element name for a field reference
216
- def field_xml_element_name(field_ref)
217
- ref_name = field_ref.ref
218
- return ref_name unless ref_name
219
-
220
- return field_ref.use_name.content if field_ref.use_name&.content
221
-
222
- defn = @field_defs[ref_name]
223
- return defn.use_name.content if defn&.use_name&.content
224
-
225
- ref_name
226
- end
227
-
228
- # ── Field Class Generation ────────────────────────────────────────
229
-
230
- def create_field_class(field_def)
231
- return unless field_def.name
232
-
233
- klass_name = "Field_#{field_def.name.gsub('-', '_')}"
234
- klass = Class.new(Lutaml::Model::Serializable)
235
- @classes[klass_name] = klass
236
-
237
- is_markup = TypeMapper.markup?(field_def.as_type)
238
- is_multiline = TypeMapper.multiline?(field_def.as_type)
239
- content_type = TypeMapper.map(field_def.as_type)
240
-
241
- if is_multiline
242
- apply_markup_multiline_attributes(klass)
243
- elsif is_markup
244
- apply_markup_attributes(klass)
245
- elsif field_def.collapsible == "yes"
246
- klass.attribute :content, content_type, collection: true
247
- else
248
- klass.attribute :content, content_type
249
- end
250
-
251
- field_def.define_flag&.each { |f| add_inline_flag(klass, f) }
252
- field_def.flag&.each { |f| add_flag_reference(klass, f) }
253
-
254
- build_field_xml(klass, field_def.name, is_markup || is_multiline,
255
- field_def, is_multiline)
256
- build_field_json(klass, field_def)
257
-
258
- # Allow string-based deserialization: lutaml-model's of_json expects a
259
- # Hash, but fields can appear as plain strings in JSON (when no flags are
260
- # set, per NIST convention). Override of_json/from_json to handle both.
261
- has_flags = field_def.define_flag&.any? || field_def.flag&.any?
262
- has_json_vk = field_def.json_value_key || field_def.json_value_key_flag
263
- is_collapsible = field_def.collapsible == "yes"
264
- value_key = field_def.json_value_key || "STRVALUE"
265
-
266
- klass.define_singleton_method(:of_json) do |data|
267
- if data.is_a?(String)
268
- new(content: data)
269
- else
270
- super(data)
271
- end
272
- end
273
-
274
- klass.define_singleton_method(:from_json) do |data|
275
- if data.is_a?(String)
276
- new(content: data)
277
- else
278
- super(data)
279
- end
280
- end
281
-
282
- if has_flags || has_json_vk || is_collapsible
283
- flag_attr_names = (field_def.define_flag || []).filter_map do |f|
284
- safe_attr(f.name) if f.name
285
- end +
286
- (field_def.flag || []).filter_map do |f|
287
- safe_attr(f.ref) if f.ref
288
- end
289
-
290
- orig_as_json = klass.method(:as_json)
291
- klass.define_singleton_method(:as_json) do |instance, options = {}|
292
- result = orig_as_json.call(instance, options)
293
-
294
- # Collapsible: unwrap single-element content arrays
295
- if is_collapsible && result.is_a?(Hash) && result[value_key].is_a?(Array) && result[value_key].length == 1
296
- result[value_key] = result[value_key].first
297
- end
298
-
299
- # Fields with flags: when no flags are set, serialize as plain value
300
- if (has_flags || has_json_vk) && result.is_a?(Hash) && result.key?(value_key)
301
- flags_present = flag_attr_names.any? do |attr|
302
- val = instance.send(attr)
303
- val && !(val.respond_to?(:using_default?) && val.using_default?)
304
- end
305
- unless flags_present
306
- return result[value_key]
307
- end
308
- end
309
-
310
- result
311
- end
312
- end
313
-
314
- apply_constraint_validation(klass, field_def.constraint)
315
- end
316
-
317
- def apply_markup_attributes(klass)
318
- klass.attribute :content, :string, collection: true
319
- klass.attribute :a, AnchorType, collection: true
320
- klass.attribute :insert, InsertType, collection: true
321
- klass.attribute :br, :string, collection: true
322
- klass.attribute :code, CodeType, collection: true
323
- klass.attribute :em, InlineMarkupType, collection: true
324
- klass.attribute :i, InlineMarkupType, collection: true
325
- klass.attribute :b, InlineMarkupType, collection: true
326
- klass.attribute :strong, InlineMarkupType, collection: true
327
- klass.attribute :sub, InlineMarkupType, collection: true
328
- klass.attribute :sup, InlineMarkupType, collection: true
329
- klass.attribute :q, InlineMarkupType, collection: true
330
- klass.attribute :img, ImageType, collection: true
331
- end
332
-
333
- def apply_markup_multiline_attributes(klass)
334
- apply_markup_attributes(klass)
335
- klass.attribute :p, InlineMarkupType, collection: true
336
- klass.attribute :h1, InlineMarkupType, collection: true
337
- klass.attribute :h2, InlineMarkupType, collection: true
338
- klass.attribute :h3, InlineMarkupType, collection: true
339
- klass.attribute :h4, InlineMarkupType, collection: true
340
- klass.attribute :h5, InlineMarkupType, collection: true
341
- klass.attribute :h6, InlineMarkupType, collection: true
342
- klass.attribute :ul, ListType, collection: true
343
- klass.attribute :ol, OrderedListType, collection: true
344
- klass.attribute :pre, PreformattedType, collection: true
345
- klass.attribute :hr, :string, collection: true
346
- klass.attribute :blockquote, BlockQuoteType, collection: true
347
- klass.attribute :table, TableType, collection: true
348
- end
349
-
350
- def build_field_xml(klass, xml_element, is_markup, field_def,
351
- is_multiline = false)
352
- flag_defs = field_def.define_flag || []
353
- flag_refs = field_def.flag || []
354
-
355
- # Precompute safe attribute names for XML mapping
356
- flag_attr_maps = flag_defs.filter_map do |f|
357
- [f.name, safe_attr(f.name)] if f.name
358
- end
359
- flag_ref_maps = flag_refs.filter_map do |f|
360
- [f.ref, safe_attr(f.ref)] if f.ref
361
- end
362
-
363
- klass.class_eval do
364
- xml do
365
- element xml_element
366
- mixed_content if is_markup
367
- ordered if is_markup
368
-
369
- map_content to: :content
370
-
371
- if is_markup
372
- map_element "a", to: :a
373
- map_element "insert", to: :insert
374
- map_element "br", to: :br
375
- map_element "code", to: :code
376
- map_element "em", to: :em
377
- map_element "i", to: :i
378
- map_element "b", to: :b
379
- map_element "strong", to: :strong
380
- map_element "sub", to: :sub
381
- map_element "sup", to: :sup
382
- map_element "q", to: :q
383
- map_element "img", to: :img
384
- end
385
-
386
- if is_multiline
387
- map_element "p", to: :p
388
- map_element "h1", to: :h1
389
- map_element "h2", to: :h2
390
- map_element "h3", to: :h3
391
- map_element "h4", to: :h4
392
- map_element "h5", to: :h5
393
- map_element "h6", to: :h6
394
- map_element "ul", to: :ul
395
- map_element "ol", to: :ol
396
- map_element "pre", to: :pre
397
- map_element "hr", to: :hr
398
- map_element "blockquote", to: :blockquote
399
- map_element "table", to: :table
400
- end
401
-
402
- flag_attr_maps.each do |xml_name, attr_name|
403
- map_attribute xml_name, to: attr_name
404
- end
405
-
406
- flag_ref_maps.each do |xml_name, attr_name|
407
- map_attribute xml_name, to: attr_name
408
- end
409
- end
410
- end
411
- end
412
-
413
- # ── Key-Value Mapping Generation (JSON / YAML / TOML) ───────────
414
- # lutaml-model's key_value DSL generates mappings shared by all
415
- # key-value formats (JSON, YAML, TOML). of_json / as_json / etc.
416
- # continue to work because they delegate to the same mappings.
417
-
418
- def build_field_json(klass, field_def)
419
- flag_defs = field_def.define_flag || []
420
- flag_refs = field_def.flag || []
421
- has_flags = flag_defs.any? || flag_refs.any?
422
- json_vk = field_def.json_value_key
423
- json_vk_flag = field_def.json_value_key_flag&.flag_ref
424
-
425
- if json_vk_flag
426
- build_field_json_value_key_flag(klass, field_def, json_vk_flag)
427
- return
428
- end
429
-
430
- value_key = json_vk || "STRVALUE"
431
-
432
- flag_attr_maps = flag_defs.filter_map do |f|
433
- [f.name, safe_attr(f.name)] if f.name
434
- end
435
- flag_ref_maps = flag_refs.filter_map do |f|
436
- [f.ref, safe_attr(f.ref)] if f.ref
437
- end
438
-
439
- klass.class_eval do
440
- key_value do
441
- root field_def.name
442
-
443
- if has_flags || json_vk
444
- map value_key, to: :content
445
- else
446
- map "content", to: :content
447
- end
448
-
449
- flag_attr_maps.each do |xml_name, attr_name|
450
- map xml_name, to: attr_name
451
- end
452
-
453
- flag_ref_maps.each do |xml_name, attr_name|
454
- map xml_name, to: attr_name
455
- end
456
- end
457
- end
458
- end
459
-
460
- # json-value-key-flag: the flag value becomes the JSON key for content.
461
- # E.g. {"prop1": "value1", "id": "id1"} where "prop1" is the name flag value.
462
- # We store metadata on the field class and handle serialization via
463
- # custom with: callbacks at the assembly level.
464
- def build_field_json_value_key_flag(klass, field_def, key_flag_ref)
465
- key_attr = safe_attr(key_flag_ref)
466
- flag_defs = field_def.define_flag || []
467
- flag_refs = field_def.flag || []
468
-
469
- other_flag_maps = flag_defs.reject { |f| f.name == key_flag_ref }
470
- .filter_map do |f|
471
- if f.name
472
- [f.name,
473
- safe_attr(f.name)]
474
- end
475
- end +
476
- flag_refs.reject { |f| f.ref == key_flag_ref }
477
- .filter_map do |f|
478
- if f.ref
479
- [f.ref,
480
- safe_attr(f.ref)]
481
- end
482
- end
483
-
484
- # Store metadata: pairs of [json_key, attr_name] for other flags
485
- klass.instance_variable_set(:@json_vk_flag_key_attr, key_attr)
486
- klass.instance_variable_set(:@json_vk_flag_other_flag_maps,
487
- other_flag_maps)
488
-
489
- klass.class_eval do
490
- key_value do
491
- root field_def.name
492
- other_flag_maps.each do |json_name, attr_name|
493
- map json_name, to: attr_name
494
- end
495
- end
496
- end
497
- end
498
-
499
- # Build custom with: callbacks for a field that uses json-value-key-flag.
500
- # Called from build_assembly_json when the referenced field has this pattern.
501
- def build_vk_flag_field_callbacks(parent_klass, field_klass, json_name,
502
- attr_sym)
503
- key_attr = field_klass.instance_variable_get(:@json_vk_flag_key_attr)
504
- other_flag_maps = field_klass.instance_variable_get(:@json_vk_flag_other_flag_maps)
505
- known_json_keys = other_flag_maps.map(&:first)
506
-
507
- from_method = :"json_from_vkf_#{attr_sym}_#{json_name.gsub('-', '_')}"
508
- to_method = :"json_to_vkf_#{attr_sym}_#{json_name.gsub('-', '_')}"
509
-
510
- parent_klass.define_method(from_method) do |instance, value|
511
- items = case value
512
- when Array then value
513
- when Hash then [value]
514
- when nil then []
515
- else [value]
516
- end
517
- parsed = items.map do |item|
518
- item = item.dup
519
- key_val = nil
520
- content_val = nil
521
- item.each do |k, v|
522
- unless known_json_keys.include?(k)
523
- key_val = k
524
- content_val = v
525
- end
526
- end
527
- obj = field_klass.allocate
528
- obj.instance_variable_set(:@using_default, {})
529
- obj.instance_variable_set(:@lutaml_register, :default)
530
- obj.instance_variable_set("@#{key_attr}", key_val)
531
- obj.instance_variable_set(:@content, content_val)
532
- other_flag_maps.each do |json_key, attr_name|
533
- if item.key?(json_key)
534
- obj.instance_variable_set("@#{attr_name}",
535
- item[json_key])
536
- end
537
- end
538
- obj
539
- end
540
- instance.instance_variable_set("@#{attr_sym}", parsed)
541
- end
542
-
543
- parent_klass.define_method(to_method) do |instance, doc|
544
- current = instance.instance_variable_get("@#{attr_sym}")
545
- if current.is_a?(Array)
546
- doc[json_name] = current.map do |item|
547
- key_val = item.instance_variable_get("@#{key_attr}")
548
- content_val = item.instance_variable_get(:@content)
549
- result = { key_val => content_val }
550
- other_flag_maps.each do |json_key, attr_name|
551
- val = item.instance_variable_get("@#{attr_name}")
552
- result[json_key] = val if val
553
- end
554
- result
555
- end
556
- end
557
- end
558
-
559
- { from_method: from_method, to_method: to_method }
560
- end
561
-
562
- # Build custom with: callbacks for BY_KEY group-as.
563
- # JSON format: {"key1": "val1", "key2": "val2"} — a map keyed by json-key flag.
564
- # Internal format: array of field instances, each with the key flag set.
565
- def build_by_key_field_callbacks(parent_klass, field_klass, json_name,
566
- attr_sym, json_key_flag)
567
- key_attr = safe_attr(json_key_flag)
568
- field_klass && field_klass.instance_variable_get(:@json_vk_flag_key_attr).nil? &&
569
- field_klass.attributes.any? do |k, _|
570
- k != :content && k.to_s != key_attr.to_s
571
- end
572
-
573
- from_method = :"json_from_bykey_#{attr_sym}_#{json_name.gsub('-', '_')}"
574
- to_method = :"json_to_bykey_#{attr_sym}_#{json_name.gsub('-', '_')}"
575
-
576
- parent_klass.define_method(from_method) do |instance, value|
577
- return unless value.is_a?(Hash)
578
-
579
- parsed = value.map do |k, v|
580
- obj = if field_klass
581
- field_klass.allocate.tap do |o|
582
- o.instance_variable_set(:@using_default, {})
583
- o.instance_variable_set(:@lutaml_register, :default)
584
- o.instance_variable_set("@#{key_attr}", k)
585
- if v.is_a?(Hash)
586
- # Field with flags — deserialize from hash
587
- v.each do |vk, vv|
588
- attr_sym_local = vk.gsub("-", "_").to_sym
589
- begin
590
- o.instance_variable_set("@#{attr_sym_local}", vv)
591
- rescue StandardError
592
- # skip unknown attributes
593
- end
594
- end
595
- else
596
- o.instance_variable_set(:@content, v)
597
- end
598
- end
599
- else
600
- k
601
- end
602
- obj
603
- end
604
- instance.instance_variable_set("@#{attr_sym}", parsed)
605
- end
606
-
607
- parent_klass.define_method(to_method) do |instance, doc|
608
- current = instance.instance_variable_get("@#{attr_sym}")
609
- if current.is_a?(Array)
610
- result = {}
611
- current.each do |item|
612
- if field_klass
613
- key_val = item.instance_variable_get("@#{key_attr}")
614
- content_val = item.instance_variable_get(:@content)
615
- if field_klass.attributes.keys.any? do |k|
616
- k != :content && k.to_s != key_attr.to_s && item.instance_variable_get("@#{k}")
617
- end
618
- # Has other flags — serialize as object
619
- obj = {}
620
- field_klass.attributes.each_key do |attr_k|
621
- next if attr_k.to_s == key_attr.to_s
622
-
623
- v = item.instance_variable_get("@#{attr_k}")
624
- obj[attr_k.to_s] = v if v
625
- end
626
- result[key_val] = obj
627
- else
628
- result[key_val] = content_val
629
- end
630
- end
631
- end
632
- doc[json_name] = result
633
- end
634
- end
635
-
636
- { from_method: from_method, to_method: to_method }
637
- end
638
-
639
- # Handles BY_KEY group-as for assembly references.
640
- # In JSON, assemblies are keyed by their json-key flag value:
641
- # {"en": {...}, "de": {...}}
642
- # On parse (from): deserialize each value into the assembly class,
643
- # setting the key flag attribute on each instance.
644
- # On serialize (to): extract the key flag value and build a Hash.
645
- def build_by_key_assembly_callbacks(parent_klass, asm_klass, json_name,
646
- attr_sym, json_key_flag, grouped: false, child_attr: nil)
647
- key_attr = safe_attr(json_key_flag)
648
-
649
- from_method = :"json_from_bykey_asm_#{attr_sym}_#{json_name.gsub('-',
650
- '_')}"
651
- to_method = :"json_to_bykey_asm_#{attr_sym}_#{json_name.gsub('-', '_')}"
652
-
653
- parent_klass.define_method(from_method) do |instance, value|
654
- return unless value.is_a?(Hash)
655
-
656
- parsed = value.map do |k, v|
657
- if asm_klass
658
- obj = if v.is_a?(Hash)
659
- asm_klass.of_json(v)
660
- else
661
- asm_klass.new
662
- end
663
- obj.instance_variable_set("@#{key_attr}", k)
664
- obj
665
- else
666
- k
667
- end
668
- end
669
-
670
- if grouped && child_attr
671
- # GROUPED wrapper: create wrapper instance containing the array
672
- wrapper = instance.instance_variable_get("@#{attr_sym}")
673
- unless wrapper
674
- attr_type = instance.class.attributes[attr_sym]
675
- wrapper = attr_type.type.new
676
- end
677
- wrapper.instance_variable_set("@#{child_attr}", parsed)
678
- instance.instance_variable_set("@#{attr_sym}", wrapper)
679
- else
680
- instance.instance_variable_set("@#{attr_sym}", parsed)
681
- end
682
- end
683
-
684
- parent_klass.define_method(to_method) do |instance, doc|
685
- current = instance.instance_variable_get("@#{attr_sym}")
686
- items = if grouped && current && child_attr
687
- current.send(child_attr)
688
- else
689
- current
690
- end
691
-
692
- if items.is_a?(Array)
693
- result = {}
694
- items.each do |item|
695
- next unless asm_klass
696
-
697
- key_val = item.instance_variable_get("@#{key_attr}")
698
- if item.is_a?(Lutaml::Model::Serializable)
699
- sub = asm_klass.as_json(item)
700
- # Remove the key flag from the sub-hash (it's the outer key)
701
- key_json_name = asm_klass.mappings_for(:json).instance_variable_get(:@mappings)
702
- .find do |_map_key, rule|
703
- rule.to.to_s == key_attr.to_s
704
- end&.first
705
- sub.delete(key_json_name) if key_json_name
706
- result[key_val] = sub.empty? ? {} : sub
707
- else
708
- result[key_val] = {}
709
- end
710
- end
711
- doc[json_name] = result
712
- end
713
- end
714
-
715
- { from_method: from_method, to_method: to_method }
716
- end
717
-
718
- def build_assembly_json(klass, root_name, assembly_def)
719
- flag_defs = assembly_def.define_flag || []
720
- flag_refs = assembly_def.flag || []
721
-
722
- flag_attr_maps = flag_defs.filter_map do |f|
723
- [f.name, safe_attr(f.name)] if f.name
724
- end
725
- flag_ref_maps = flag_refs.filter_map do |f|
726
- [f.ref, safe_attr(f.ref)] if f.ref
727
- end
728
-
729
- json_field_mappings = collect_json_field_mappings(assembly_def)
730
- json_assembly_mappings = collect_json_assembly_mappings(assembly_def)
731
-
732
- # Separate vk_flag, by_key, and singleton_or_array mappings for custom handling
733
- vk_flag_mappings = json_field_mappings.select { |m| m[:vk_flag] }
734
- by_key_mappings = json_field_mappings.select { |m| m[:by_key] }
735
- soa_mappings = json_field_mappings.select { |m| m[:singleton_or_array] }
736
- regular_field_mappings = json_field_mappings.reject do |m|
737
- m[:vk_flag] || m[:by_key] || m[:singleton_or_array]
738
- end
739
-
740
- # Separate assembly SOA from regular assembly mappings
741
- assembly_by_key_mappings = json_assembly_mappings.select do |m|
742
- m[:by_key]
743
- end
744
- assembly_soa_mappings = json_assembly_mappings.select do |m|
745
- m[:singleton_or_array]
746
- end
747
- regular_assembly_mappings = json_assembly_mappings.reject do |m|
748
- m[:by_key] || m[:singleton_or_array]
749
- end
750
-
751
- klass.class_eval do
752
- key_value do
753
- root root_name
754
-
755
- flag_attr_maps.each do |xml_name, attr_name|
756
- map xml_name, to: attr_name
757
- end
758
-
759
- flag_ref_maps.each do |xml_name, attr_name|
760
- map xml_name, to: attr_name
761
- end
762
-
763
- regular_field_mappings.each do |mapping|
764
- if mapping[:scalar]
765
- map mapping[:json_name], to: mapping[:attr_name],
766
- with: { to: mapping[:to_method], from: mapping[:from_method] }
767
- else
768
- map mapping[:json_name], to: mapping[:attr_name],
769
- render_empty: true
770
- end
771
- end
772
-
773
- regular_assembly_mappings.each do |mapping|
774
- map mapping[:json_name], to: mapping[:attr_name], render_empty: true
775
- end
776
- end
777
- end
778
-
779
- # Define with: callback methods for scalar field mappings
780
- regular_field_mappings.each do |mapping|
781
- next unless mapping[:scalar]
782
-
783
- field_klass = mapping[:field_klass]
784
- attr_sym = mapping[:attr_name]
785
-
786
- has_flags = mapping[:has_flags]
787
-
788
- klass.define_method(mapping[:from_method]) do |instance, value|
789
- if value.is_a?(Array)
790
- parsed = value.map do |v|
791
- has_flags ? field_klass.of_json(v) : field_klass.new(content: v)
792
- end
793
- instance.instance_variable_set("@#{attr_sym}", parsed)
794
- elsif value.is_a?(Hash)
795
- if value.empty?
796
- inst = field_klass.new(content: "")
797
- inst.instance_variable_set(:@_was_empty_hash, true)
798
- instance.instance_variable_set("@#{attr_sym}", inst)
799
- else
800
- instance.instance_variable_set("@#{attr_sym}",
801
- field_klass.of_json(value))
802
- end
803
- elsif value
804
- instance.instance_variable_set("@#{attr_sym}",
805
- has_flags ? field_klass.of_json(value) : field_klass.new(content: value))
806
- end
807
- end
808
-
809
- klass.define_method(mapping[:to_method]) do |instance, doc|
810
- current = instance.instance_variable_get("@#{attr_sym}")
811
- if current.is_a?(Array)
812
- result = current.map do |item|
813
- if has_flags && item.is_a?(Lutaml::Model::Serializable)
814
- field_klass.as_json(item)
815
- else
816
- item.respond_to?(:content) ? item.content : item
817
- end
818
- end
819
- doc[mapping[:json_name]] = result
820
- elsif current
821
- if current.instance_variable_get(:@_was_empty_hash)
822
- doc[mapping[:json_name]] = {}
823
- elsif has_flags && current.is_a?(Lutaml::Model::Serializable)
824
- doc[mapping[:json_name]] = field_klass.as_json(current)
825
- else
826
- val = current.respond_to?(:content) ? current.content : current
827
- doc[mapping[:json_name]] = val
828
- end
829
- end
830
- end
831
- end
832
-
833
- # Handle SINGLETON_OR_ARRAY non-scalar field mappings with custom with: callbacks
834
- soa_mappings.each do |mapping|
835
- attr_sym = mapping[:attr_name]
836
- json_name = mapping[:json_name]
837
- from_m = mapping[:from_method]
838
- to_m = mapping[:to_method]
839
- field_klass = mapping[:field_klass]
840
-
841
- klass.define_method(from_m) do |instance, value|
842
- items = case value
843
- when Hash then [value]
844
- when Array then value
845
- when String then [value]
846
- else return
847
- end
848
- parsed = items.map do |item|
849
- case item
850
- when Hash then field_klass.of_json(item)
851
- when String then field_klass.of_json(item)
852
- else item
853
- end
854
- end
855
- instance.instance_variable_set("@#{attr_sym}", parsed)
856
- end
857
-
858
- klass.define_method(to_m) do |instance, doc|
859
- current = instance.instance_variable_get("@#{attr_sym}")
860
- if current.is_a?(Array)
861
- result = current.map do |item|
862
- if item.is_a?(Lutaml::Model::Serializable)
863
- field_klass.as_json(item)
864
- else
865
- item
866
- end
867
- end
868
- doc[json_name] = result.length == 1 ? result.first : result
869
- end
870
- end
871
-
872
- klass.class_eval do
873
- key_value do
874
- map json_name, to: attr_sym,
875
- with: { to: to_m, from: from_m }
876
- end
877
- end
878
-
879
- # Add alias mapping for ref name if it differs from group-as name
880
- if mapping[:alt_json_name]
881
- klass.class_eval do
882
- key_value do
883
- map mapping[:alt_json_name], to: attr_sym,
884
- with: { to: to_m, from: from_m }
885
- end
886
- end
887
- end
888
- end
889
-
890
- # Handle json-value-key-flag fields with custom with: callbacks
891
- vk_flag_mappings.each do |mapping|
892
- callbacks = build_vk_flag_field_callbacks(
893
- klass, mapping[:field_klass], mapping[:json_name], mapping[:attr_name]
894
- )
895
- # Re-open json block to add the mapping with custom with:
896
- klass.class_eval do
897
- key_value do
898
- map mapping[:json_name], to: mapping[:attr_name],
899
- with: { to: callbacks[:to_method], from: callbacks[:from_method] }
900
- end
901
- end
902
- end
903
-
904
- # Handle BY_KEY group-as with custom with: callbacks
905
- by_key_mappings.each do |mapping|
906
- # Ensure the mapping target attribute exists (GROUPED wrappers may not
907
- # register the child attr name as a top-level attribute)
908
- unless klass.attributes.key?(mapping[:attr_name])
909
- klass.attribute mapping[:attr_name], mapping[:field_klass],
910
- collection: true
911
- end
912
- callbacks = build_by_key_field_callbacks(
913
- klass, mapping[:field_klass], mapping[:json_name],
914
- mapping[:attr_name], mapping[:json_key_flag]
915
- )
916
- klass.class_eval do
917
- key_value do
918
- map mapping[:json_name], to: mapping[:attr_name],
919
- with: { to: callbacks[:to_method], from: callbacks[:from_method] }
920
- end
921
- end
922
- end
923
-
924
- # Handle BY_KEY assembly mappings with custom with: callbacks
925
- assembly_by_key_mappings.each do |mapping|
926
- unless klass.attributes.key?(mapping[:attr_name])
927
- asm_type = mapping[:asm_klass] || Lutaml::Model::Serializable
928
- klass.attribute mapping[:attr_name], asm_type, collection: true
929
- end
930
- callbacks = build_by_key_assembly_callbacks(
931
- klass, mapping[:asm_klass], mapping[:json_name],
932
- mapping[:attr_name], mapping[:json_key_flag],
933
- grouped: mapping[:grouped] || false,
934
- child_attr: mapping[:child_attr]
935
- )
936
- klass.class_eval do
937
- key_value do
938
- map mapping[:json_name], to: mapping[:attr_name],
939
- with: { to: callbacks[:to_method], from: callbacks[:from_method] }
940
- end
941
- end
942
- end
943
-
944
- # Handle SINGLETON_OR_ARRAY assembly mappings with custom with: callbacks
945
- assembly_soa_mappings.each do |mapping|
946
- attr_sym = mapping[:attr_name]
947
- json_name = mapping[:json_name]
948
- from_m = mapping[:from_method]
949
- to_m = mapping[:to_method]
950
- asm_klass = mapping[:asm_klass]
951
-
952
- # Typed instances for all SOA (both explicit and implicit)
953
- klass.define_method(from_m) do |instance, value|
954
- items = case value
955
- when Hash then [value]
956
- when Array then value
957
- else return
958
- end
959
- parsed = if asm_klass
960
- items.map do |item|
961
- asm_klass.of_json(item.is_a?(Hash) ? item : {})
962
- end
963
- else
964
- items
965
- end
966
- # For singleton attributes (collection: false), unwrap single-item arrays
967
- attr_def = klass.attributes[attr_sym]
968
- if parsed.length == 1 && attr_def && !attr_def.collection
969
- instance.instance_variable_set("@#{attr_sym}", parsed.first)
970
- else
971
- instance.instance_variable_set("@#{attr_sym}", parsed)
972
- end
973
- end
974
-
975
- klass.define_method(to_m) do |instance, doc|
976
- current = instance.instance_variable_get("@#{attr_sym}")
977
- if current.is_a?(Array)
978
- result = current.map do |item|
979
- if asm_klass && item.is_a?(Lutaml::Model::Serializable)
980
- asm_klass.as_json(item)
981
- else
982
- item
983
- end
984
- end
985
- doc[json_name] = result.length == 1 ? result.first : result
986
- elsif current
987
- doc[json_name] = if asm_klass && current.is_a?(Lutaml::Model::Serializable)
988
- asm_klass.as_json(current)
989
- else
990
- current
991
- end
992
- end
993
- end
994
-
995
- klass.class_eval do
996
- key_value do
997
- map json_name, to: attr_sym, render_empty: true,
998
- with: { to: to_m, from: from_m }
999
- end
1000
- end
1001
- end
1002
-
1003
- # Collapsible BY_KEY: when an assembly has no flags and only one BY_KEY
1004
- # child, the NIST toolchain outputs the BY_KEY map directly without the
1005
- # group-as name wrapper (e.g. author-index JSON is {"archimedes": {...}}
1006
- # not {"authors": {"archimedes": {...}}}).
1007
- if flag_defs.empty? && flag_refs.empty? &&
1008
- json_assembly_mappings.length == 1 &&
1009
- json_assembly_mappings.first[:by_key]
1010
-
1011
- by_key_json_name = json_assembly_mappings.first[:json_name]
1012
-
1013
- orig_of_json = klass.method(:of_json)
1014
- klass.define_singleton_method(:of_json) do |data, options = {}|
1015
- if data.is_a?(Hash) && !data.key?(by_key_json_name)
1016
- orig_of_json.call({ by_key_json_name => data }, options)
1017
- else
1018
- orig_of_json.call(data, options)
1019
- end
1020
- end
1021
-
1022
- orig_as_json = klass.method(:as_json)
1023
- klass.define_singleton_method(:as_json) do |instance, options = {}|
1024
- result = orig_as_json.call(instance, options)
1025
- if result.is_a?(Hash) && result.key?(by_key_json_name)
1026
- result[by_key_json_name]
1027
- else
1028
- result
1029
- end
1030
- end
1031
- end
1032
- end
1033
-
1034
- def collect_json_field_mappings(assembly_def)
1035
- mappings = []
1036
- model = assembly_def.model
1037
- return mappings unless model
1038
-
1039
- mappings.concat(collect_model_json_field_mappings(model))
1040
- mappings
1041
- end
1042
-
1043
- def collect_model_json_field_mappings(model)
1044
- mappings = []
1045
-
1046
- model.field&.each { |fr| mappings << build_field_json_mapping(fr) }
1047
- model.define_field&.each do |fd|
1048
- mappings << build_inline_field_json_mapping(fd) if fd.name
1049
- end
1050
- model.choice&.each do |c|
1051
- c.field&.each { |fr| mappings << build_field_json_mapping(fr) }
1052
- c.define_field&.each do |fd|
1053
- mappings << build_inline_field_json_mapping(fd) if fd.name
1054
- end
1055
- end
1056
- model.choice_group&.each do |cg|
1057
- cg.field&.each do |fr|
1058
- mappings << build_field_json_mapping(fr, cg.group_as)
1059
- end
1060
- cg.define_field&.each do |fd|
1061
- mappings << build_inline_field_json_mapping(fd) if fd.name
1062
- end
1063
- end
1064
-
1065
- mappings
1066
- end
1067
-
1068
- def build_field_json_mapping(field_ref, override_group_as = nil)
1069
- ref_name = field_ref.ref
1070
- return nil unless ref_name
1071
-
1072
- group_as = override_group_as || field_ref.group_as
1073
- field_def = @field_defs[ref_name]
1074
- field_klass = @classes["Field_#{ref_name.gsub('-', '_')}"]
1075
- has_flags = field_has_flags?(field_def)
1076
-
1077
- json_name = if group_as
1078
- group_as.name
1079
- else
1080
- field_ref.use_name&.content || ref_name
1081
- end
1082
- attr_name = safe_attr(ref_name)
1083
-
1084
- # Check for BY_KEY group-as
1085
- if group_as&.in_json == "BY_KEY"
1086
- json_key_flag = field_def&.json_key&.flag_ref
1087
- return {
1088
- json_name: json_name, attr_name: attr_name,
1089
- by_key: true, field_klass: field_klass,
1090
- json_key_flag: json_key_flag
1091
- }
1092
- end
1093
-
1094
- # Check for json-value-key-flag pattern
1095
- if field_klass&.instance_variable_get(:@json_vk_flag_key_attr)
1096
- return {
1097
- json_name: json_name, attr_name: attr_name,
1098
- vk_flag: true, field_klass: field_klass
1099
- }
1100
- end
1101
-
1102
- if has_flags
1103
- is_soa = group_as && ["SINGLETON_OR_ARRAY",
1104
- "ARRAY"].include?(group_as.in_json)
1105
- method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
1106
- if is_soa
1107
- result = {
1108
- json_name: json_name, attr_name: attr_name, scalar: false,
1109
- singleton_or_array: true, field_klass: field_klass,
1110
- to_method: :"json_soa_to_#{method_suffix}",
1111
- from_method: :"json_soa_from_#{method_suffix}"
1112
- }
1113
- # Include ref_name for SOA fields with group-as, so we can also
1114
- # accept the ref name as a JSON key during deserialization (some
1115
- # NIST worked examples use ref name instead of group-as name).
1116
- if group_as && ref_name != json_name
1117
- result[:alt_json_name] =
1118
- ref_name
1119
- end
1120
- result
1121
- else
1122
- # Singleton field with flags: typed instance, no array wrapping
1123
- {
1124
- json_name: json_name, attr_name: attr_name, scalar: true,
1125
- has_flags: true, field_klass: field_klass,
1126
- to_method: :"json_to_#{method_suffix}",
1127
- from_method: :"json_from_#{method_suffix}"
1128
- }
1129
- end
1130
- else
1131
- method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
1132
- {
1133
- json_name: json_name, attr_name: attr_name, scalar: true,
1134
- field_klass: field_klass,
1135
- to_method: :"json_to_#{method_suffix}",
1136
- from_method: :"json_from_#{method_suffix}"
1137
- }
1138
- end
1139
- end
1140
-
1141
- def build_inline_field_json_mapping(field_def)
1142
- json_name = field_def.name
1143
- attr_name = safe_attr(field_def.name)
1144
- has_flags = field_has_flags?(field_def)
1145
-
1146
- if has_flags
1147
- field_klass = @classes[scoped_field_name(field_def.name)]
1148
- method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
1149
- {
1150
- json_name: json_name, attr_name: attr_name, scalar: false,
1151
- singleton_or_array: true, field_klass: field_klass,
1152
- to_method: :"json_soa_to_#{method_suffix}",
1153
- from_method: :"json_soa_from_#{method_suffix}"
1154
- }
1155
- else
1156
- { json_name: json_name, attr_name: attr_name, scalar: false }
1157
- end
1158
- end
1159
-
1160
- def field_has_flags?(field_def, _field_ref = nil)
1161
- return false unless field_def
1162
-
1163
- field_def.define_flag&.any? || field_def.flag&.any? || field_def.json_value_key || field_def.json_value_key_flag
1164
- end
1165
-
1166
- def collect_json_assembly_mappings(assembly_def)
1167
- mappings = []
1168
- model = assembly_def.model
1169
- return mappings unless model
1170
-
1171
- mappings.concat(collect_model_json_assembly_mappings(model))
1172
- mappings
1173
- end
1174
-
1175
- def collect_model_json_assembly_mappings(model)
1176
- mappings = []
1177
-
1178
- model.assembly&.each do |ar|
1179
- ref_name = ar.ref
1180
- next unless ref_name
1181
-
1182
- group_as = ar.group_as
1183
- json_name = group_as&.name || ar.use_name&.content || ref_name
1184
- # When GROUPED in XML, the attribute is the group-as name (wrapper).
1185
- # Otherwise it's the ref name (direct collection).
1186
- attr_name = group_as&.in_xml == "GROUPED" ? safe_attr(group_as.name) : safe_attr(ref_name)
1187
- mapping = { json_name: json_name, attr_name: attr_name }
1188
- if group_as&.in_json == "BY_KEY"
1189
- asm_def = @assembly_defs[ref_name]
1190
- json_key_flag = asm_def&.json_key&.flag_ref
1191
- asm_klass = @classes["Assembly_#{ref_name.gsub('-', '_')}"]
1192
- mapping[:by_key] = true
1193
- mapping[:asm_klass] = asm_klass
1194
- mapping[:json_key_flag] = json_key_flag
1195
- mapping[:grouped] = true if group_as&.in_xml == "GROUPED"
1196
- if group_as&.in_xml == "GROUPED"
1197
- mapping[:child_attr] =
1198
- safe_attr(ref_name)
1199
- end
1200
- else
1201
- check_assembly_soa(mapping, group_as, attr_name, json_name)
1202
- end
1203
- mappings << mapping
1204
- end
1205
-
1206
- model.define_assembly&.each do |ad|
1207
- next unless ad.name
1208
-
1209
- group_as = ad.group_as
1210
- json_name = group_as&.name || ad.name
1211
- attr_name = safe_attr(ad.name)
1212
- mapping = { json_name: json_name, attr_name: attr_name }
1213
- if group_as&.in_json == "BY_KEY"
1214
- json_key_flag = ad.json_key&.flag_ref
1215
- mapping[:by_key] = true
1216
- mapping[:json_key_flag] = json_key_flag
1217
- else
1218
- check_assembly_soa(mapping, group_as, attr_name, json_name)
1219
- end
1220
- mappings << mapping
1221
- end
1222
-
1223
- model.choice&.each do |c|
1224
- c.assembly&.each do |ar|
1225
- ref_name = ar.ref
1226
- next unless ref_name
1227
-
1228
- group_as = ar.group_as
1229
- json_name = group_as&.name || ar.use_name&.content || ref_name
1230
- attr_name = safe_attr(ref_name)
1231
- mapping = { json_name: json_name, attr_name: attr_name }
1232
- check_assembly_soa(mapping, group_as, attr_name, json_name)
1233
- mappings << mapping
1234
- end
1235
- c.define_assembly&.each do |ad|
1236
- next unless ad.name
1237
-
1238
- group_as = ad.group_as
1239
- json_name = group_as&.name || ad.name
1240
- attr_name = safe_attr(ad.name)
1241
- mapping = { json_name: json_name, attr_name: attr_name }
1242
- check_assembly_soa(mapping, group_as, attr_name, json_name)
1243
- mappings << mapping
1244
- end
1245
- end
1246
-
1247
- model.choice_group&.each do |cg|
1248
- group_as = cg.group_as
1249
- json_name = group_as&.name
1250
- cg.assembly&.each do |ar|
1251
- ref_name = ar.ref
1252
- next unless ref_name
1253
-
1254
- name = json_name || ar.use_name&.content || ref_name
1255
- attr_name = safe_attr(ref_name)
1256
- mapping = { json_name: name, attr_name: attr_name }
1257
- check_assembly_soa(mapping, group_as, attr_name, name)
1258
- mappings << mapping
1259
- end
1260
- cg.define_assembly&.each do |ad|
1261
- next unless ad.name
1262
-
1263
- name = json_name || ad.name
1264
- attr_name = safe_attr(ad.name)
1265
- mapping = { json_name: name, attr_name: attr_name }
1266
- check_assembly_soa(mapping, group_as, attr_name, name)
1267
- mappings << mapping
1268
- end
1269
- end
1270
-
1271
- mappings
1272
- end
1273
-
1274
- def check_assembly_soa(mapping, group_as, attr_name, json_name)
1275
- is_soa = group_as&.in_json == "SINGLETON_OR_ARRAY" || group_as.nil?
1276
- return unless is_soa
1277
-
1278
- method_suffix = "#{attr_name}_#{json_name.gsub('-', '_')}"
1279
- mapping[:singleton_or_array] = true
1280
- mapping[:to_method] = :"json_assembly_soa_to_#{method_suffix}"
1281
- mapping[:from_method] = :"json_assembly_soa_from_#{method_suffix}"
1282
- # Attach the assembly class for casting in from: callback
1283
- asm_klass = @classes["Assembly_#{attr_name.to_s.gsub('-', '_')}"]
1284
- mapping[:asm_klass] = asm_klass if asm_klass
1285
- end
1286
-
1287
- # ── Assembly Class Generation ─────────────────────────────────────
1288
-
1289
- def create_assembly_placeholder(assembly_def)
1290
- return unless assembly_def.name
1291
-
1292
- klass_name = "Assembly_#{assembly_def.name.gsub('-', '_')}"
1293
- @classes[klass_name] ||= Class.new(Lutaml::Model::Serializable)
1294
- end
1295
-
1296
- def populate_assembly_class(assembly_def)
1297
- return unless assembly_def.name
1298
-
1299
- klass_name = "Assembly_#{assembly_def.name.gsub('-', '_')}"
1300
- klass = @classes[klass_name]
1301
- return unless klass
1302
-
1303
- @current_assembly_name = assembly_def.name.gsub("-", "_")
1304
-
1305
- assembly_def.define_flag&.each { |f| add_inline_flag(klass, f) }
1306
- assembly_def.flag&.each { |f| add_flag_reference(klass, f) }
1307
-
1308
- process_model(klass, assembly_def.model) if assembly_def.model
1309
-
1310
- root_name = assembly_def.root_name&.content || assembly_def.name
1311
- build_assembly_xml(klass, root_name, assembly_def)
1312
- build_assembly_json(klass, root_name, assembly_def)
1313
-
1314
- if assembly_def.root_name&.content
1315
- add_json_root_handling(klass,
1316
- root_name)
1317
- end
1318
-
1319
- apply_constraint_validation(klass, assembly_def.constraint)
1320
- klass.instance_variable_set(:@populated, true)
1321
- ensure
1322
- @current_assembly_name = nil
1323
- end
1324
-
1325
- def build_assembly_xml(klass, root_name, assembly_def)
1326
- flag_defs = assembly_def.define_flag || []
1327
- flag_refs = assembly_def.flag || []
1328
- child_mappings = collect_child_mappings(assembly_def)
1329
-
1330
- # Precompute safe attribute names
1331
- flag_attr_maps = flag_defs.filter_map do |f|
1332
- [f.name, safe_attr(f.name)] if f.name
1333
- end
1334
- flag_ref_maps = flag_refs.filter_map do |f|
1335
- [f.ref, safe_attr(f.ref)] if f.ref
1336
- end
1337
-
1338
- klass.class_eval do
1339
- xml do
1340
- element root_name
1341
- ordered
1342
-
1343
- flag_attr_maps.each do |xml_name, attr_name|
1344
- map_attribute xml_name, to: attr_name
1345
- end
1346
-
1347
- flag_ref_maps.each do |xml_name, attr_name|
1348
- map_attribute xml_name, to: attr_name
1349
- end
1350
-
1351
- child_mappings.each do |mapping|
1352
- map_element mapping[:xml_name], to: mapping[:attr_name]
1353
- end
1354
- end
1355
- end
1356
- end
1357
-
1358
- def collect_child_mappings(assembly_def)
1359
- mappings = []
1360
- model = assembly_def.model
1361
- return mappings unless model
1362
-
1363
- mappings.concat(collect_model_child_mappings(model))
1364
- mappings
1365
- end
1366
-
1367
- def collect_model_child_mappings(model)
1368
- mappings = []
1369
-
1370
- model.field&.each do |field_ref|
1371
- ref_name = field_ref.ref
1372
- next unless ref_name
1373
-
1374
- xml_name = field_ref.use_name&.content || ref_name
1375
- group_as = field_ref.group_as
1376
- grouped = group_as&.in_xml == "GROUPED"
1377
-
1378
- mappings << build_child_mapping(xml_name, group_as, grouped, ref_name)
1379
- end
1380
-
1381
- model.assembly&.each do |assembly_ref|
1382
- ref_name = assembly_ref.ref
1383
- next unless ref_name
1384
-
1385
- xml_name = assembly_xml_element_name(assembly_ref)
1386
- group_as = assembly_ref.group_as
1387
- grouped = group_as&.in_xml == "GROUPED"
1388
-
1389
- attr_name = grouped ? safe_attr(group_as.name) : safe_attr(ref_name)
1390
- mappings << { xml_name: grouped ? group_as.name : xml_name,
1391
- attr_name: attr_name, grouped: grouped }
1392
- end
1393
-
1394
- model.define_field&.each do |inline_def|
1395
- next unless inline_def.name
1396
-
1397
- mappings << { xml_name: inline_def.name,
1398
- attr_name: safe_attr(inline_def.name), grouped: false }
1399
- end
1400
-
1401
- model.define_assembly&.each do |inline_def|
1402
- next unless inline_def.name
1403
-
1404
- mappings << { xml_name: inline_def.name,
1405
- attr_name: safe_attr(inline_def.name), grouped: false }
1406
- end
1407
-
1408
- model.choice&.each do |c|
1409
- mappings.concat(collect_choice_child_mappings(c))
1410
- end
1411
- model.choice_group&.each do |cg|
1412
- mappings.concat(collect_choice_group_child_mappings(cg))
1413
- end
1414
-
1415
- mappings
1416
- end
1417
-
1418
- def collect_choice_child_mappings(choice)
1419
- mappings = []
1420
-
1421
- choice.field&.each do |field_ref|
1422
- ref_name = field_ref.ref
1423
- next unless ref_name
1424
-
1425
- xml_name = field_ref.use_name&.content || ref_name
1426
- group_as = field_ref.group_as
1427
- grouped = group_as&.in_xml == "GROUPED"
1428
-
1429
- mappings << build_child_mapping(xml_name, group_as, grouped, ref_name)
1430
- end
1431
-
1432
- choice.assembly&.each do |assembly_ref|
1433
- ref_name = assembly_ref.ref
1434
- next unless ref_name
1435
-
1436
- xml_name = assembly_xml_element_name(assembly_ref)
1437
- group_as = assembly_ref.group_as
1438
- grouped = group_as&.in_xml == "GROUPED"
1439
-
1440
- attr_name = grouped ? safe_attr(group_as.name) : safe_attr(ref_name)
1441
- mappings << { xml_name: grouped ? group_as.name : xml_name,
1442
- attr_name: attr_name, grouped: grouped }
1443
- end
1444
-
1445
- choice.define_field&.each do |inline_def|
1446
- next unless inline_def.name
1447
-
1448
- mappings << { xml_name: inline_def.name,
1449
- attr_name: safe_attr(inline_def.name), grouped: false }
1450
- end
1451
-
1452
- choice.define_assembly&.each do |inline_def|
1453
- next unless inline_def.name
1454
-
1455
- mappings << { xml_name: inline_def.name,
1456
- attr_name: safe_attr(inline_def.name), grouped: false }
1457
- end
1458
-
1459
- mappings
1460
- end
1461
-
1462
- def collect_choice_group_child_mappings(choice_group)
1463
- mappings = []
1464
-
1465
- choice_group.field&.each do |field_ref|
1466
- ref_name = field_ref.ref
1467
- next unless ref_name
1468
-
1469
- xml_name = field_ref.use_name&.content || ref_name
1470
- group_as = choice_group.group_as
1471
- grouped = group_as&.in_xml == "GROUPED"
1472
- mappings << build_child_mapping(xml_name, group_as, grouped, ref_name)
1473
- end
1474
-
1475
- choice_group.assembly&.each do |assembly_ref|
1476
- ref_name = assembly_ref.ref
1477
- next unless ref_name
1478
-
1479
- xml_name = assembly_xml_element_name(assembly_ref)
1480
- group_as = choice_group.group_as
1481
- grouped = group_as&.in_xml == "GROUPED"
1482
- attr_name = grouped ? safe_attr(group_as.name) : safe_attr(ref_name)
1483
- mappings << { xml_name: grouped ? group_as.name : xml_name,
1484
- attr_name: attr_name, grouped: grouped }
1485
- end
1486
-
1487
- choice_group.define_field&.each do |inline_def|
1488
- next unless inline_def.name
1489
-
1490
- mappings << { xml_name: inline_def.name,
1491
- attr_name: safe_attr(inline_def.name), grouped: false }
1492
- end
1493
-
1494
- choice_group.define_assembly&.each do |inline_def|
1495
- next unless inline_def.name
1496
-
1497
- mappings << { xml_name: inline_def.name,
1498
- attr_name: safe_attr(inline_def.name), grouped: false }
1499
- end
1500
-
1501
- mappings
1502
- end
1503
-
1504
- def build_child_mapping(xml_name, group_as, grouped, ref_name = nil)
1505
- if grouped
1506
- { xml_name: group_as.name, attr_name: safe_attr(group_as.name),
1507
- grouped: true }
1508
- else
1509
- attr_name = safe_attr(ref_name || xml_name)
1510
- { xml_name: xml_name, attr_name: attr_name, grouped: false }
1511
- end
1512
- end
1513
-
1514
- # ── Model Processing ──────────────────────────────────────────────
1515
-
1516
- def process_model(klass, model)
1517
- # Initialize occurrence constraints registry
1518
- unless klass.instance_variable_defined?(:@occurrence_constraints)
1519
- klass.instance_variable_set(:@occurrence_constraints,
1520
- {})
1521
- end
1522
- occ = klass.instance_variable_get(:@occurrence_constraints)
1523
-
1524
- model.field&.each do |fr|
1525
- add_field_reference(klass, fr)
1526
- record_occurrence_constraint(occ, fr)
1527
- end
1528
- model.assembly&.each do |ar|
1529
- add_assembly_reference(klass, ar)
1530
- record_occurrence_constraint(occ, ar)
1531
- end
1532
- model.define_field&.each { |fd| add_inline_field(klass, fd) }
1533
- model.define_assembly&.each { |ad| add_inline_assembly(klass, ad) }
1534
- model.choice&.each { |c| process_choice(klass, c) }
1535
- model.choice_group&.each { |cg| process_choice_group(klass, cg) }
1536
- add_any_content(klass) if model.any
1537
-
1538
- # Add validate_occurrences method if not already defined
1539
- unless klass.method_defined?(:validate_occurrences)
1540
- occ_ref = klass.instance_variable_get(:@occurrence_constraints)
1541
- klass.define_method(:validate_occurrences) do
1542
- Metaschema::ConstraintValidator.validate_occurrences(self, occ_ref)
1543
- end
1544
- end
1545
- end
1546
-
1547
- def record_occurrence_constraint(occ, ref)
1548
- ref_name = ref.ref
1549
- return unless ref_name
1550
-
1551
- attr_name = safe_attr(ref_name)
1552
- min = ref.min_occurs.to_i
1553
- max_raw = ref.max_occurs
1554
- max = max_raw == "unbounded" ? nil : max_raw&.to_i
1555
-
1556
- occ[attr_name] = { min: min, max: max } if min.positive? || max
1557
- end
1558
-
1559
- def add_field_reference(klass, field_ref)
1560
- ref_name = field_ref.ref
1561
- return unless ref_name
1562
-
1563
- field_klass = @classes["Field_#{ref_name.gsub('-', '_')}"]
1564
- return unless field_klass
1565
-
1566
- collection = unbounded?(field_ref.max_occurs)
1567
- group_as = field_ref.group_as
1568
-
1569
- if group_as&.in_xml == "GROUPED"
1570
- group_attr = safe_attr(group_as.name)
1571
- wrapper_klass = Class.new(Lutaml::Model::Serializable)
1572
- child_attr = safe_attr(ref_name)
1573
- wrapper_klass.attribute child_attr, field_klass, collection: true
1574
- wrapper_klass.class_eval do
1575
- xml do
1576
- element group_as.name
1577
- map_element ref_name, to: child_attr
1578
- end
1579
- end
1580
- klass.attribute group_attr, wrapper_klass
1581
- else
1582
- attr_name = safe_attr(ref_name)
1583
- klass.attribute attr_name, field_klass, collection: collection
1584
- end
1585
- end
1586
-
1587
- def add_assembly_reference(klass, assembly_ref)
1588
- ref_name = assembly_ref.ref
1589
- return unless ref_name
1590
-
1591
- assembly_klass = @classes["Assembly_#{ref_name.gsub('-', '_')}"] ||
1592
- create_placeholder_assembly(ref_name)
1593
-
1594
- collection = unbounded?(assembly_ref.max_occurs)
1595
- group_as = assembly_ref.group_as
1596
- xml_name = assembly_xml_element_name(assembly_ref)
1597
-
1598
- if group_as&.in_xml == "GROUPED"
1599
- group_attr = safe_attr(group_as.name)
1600
- child_attr = safe_attr(ref_name)
1601
- wrapper_klass = Class.new(Lutaml::Model::Serializable)
1602
- wrapper_klass.attribute child_attr, assembly_klass, collection: true
1603
- wrapper_klass.class_eval do
1604
- xml do
1605
- element group_as.name
1606
- map_element xml_name, to: child_attr
1607
- end
1608
- end
1609
- klass.attribute group_attr, wrapper_klass
1610
- else
1611
- attr_name = safe_attr(ref_name)
1612
- klass.attribute attr_name, assembly_klass, collection: collection
1613
- end
1614
- end
1615
-
1616
- def add_inline_field(klass, field_def)
1617
- return unless field_def.name
1618
-
1619
- attr_name = safe_attr(field_def.name)
1620
- is_markup = TypeMapper.markup?(field_def.as_type)
1621
- is_multiline = TypeMapper.multiline?(field_def.as_type)
1622
- content_type = TypeMapper.map(field_def.as_type)
1623
- collection = unbounded?(field_def.max_occurs)
1624
- has_flags = field_def.define_flag&.any? || field_def.flag&.any?
1625
-
1626
- if is_markup || is_multiline
1627
- inline_klass = Class.new(Lutaml::Model::Serializable)
1628
- if is_multiline
1629
- apply_markup_multiline_attributes(inline_klass)
1630
- else
1631
- apply_markup_attributes(inline_klass)
1632
- end
1633
-
1634
- field_def.define_flag&.each { |f| add_inline_flag(inline_klass, f) }
1635
- field_def.flag&.each { |f| add_flag_reference(inline_klass, f) }
1636
-
1637
- inline_name = field_def.name
1638
- inline_flag_defs = field_def.define_flag || []
1639
- inline_flag_refs = field_def.flag || []
1640
- inline_flag_attr_maps = inline_flag_defs.filter_map do |f|
1641
- [f.name, safe_attr(f.name)] if f.name
1642
- end
1643
- inline_flag_ref_maps = inline_flag_refs.filter_map do |f|
1644
- [f.ref, safe_attr(f.ref)] if f.ref
1645
- end
1646
-
1647
- inline_klass.class_eval do
1648
- xml do
1649
- element inline_name
1650
- mixed_content
1651
- ordered
1652
- map_content to: :content
1653
- map_element "a", to: :a
1654
- map_element "insert", to: :insert
1655
- map_element "br", to: :br
1656
- map_element "code", to: :code
1657
- map_element "em", to: :em
1658
- map_element "i", to: :i
1659
- map_element "b", to: :b
1660
- map_element "strong", to: :strong
1661
- map_element "sub", to: :sub
1662
- map_element "sup", to: :sup
1663
- map_element "q", to: :q
1664
- map_element "img", to: :img
1665
-
1666
- if is_multiline
1667
- map_element "p", to: :p
1668
- map_element "h1", to: :h1
1669
- map_element "h2", to: :h2
1670
- map_element "h3", to: :h3
1671
- map_element "h4", to: :h4
1672
- map_element "h5", to: :h5
1673
- map_element "h6", to: :h6
1674
- map_element "ul", to: :ul
1675
- map_element "ol", to: :ol
1676
- map_element "pre", to: :pre
1677
- map_element "hr", to: :hr
1678
- map_element "blockquote", to: :blockquote
1679
- map_element "table", to: :table
1680
- end
1681
-
1682
- inline_flag_attr_maps.each do |xml_name, attr_name|
1683
- map_attribute xml_name, to: attr_name
1684
- end
1685
-
1686
- inline_flag_ref_maps.each do |xml_name, attr_name|
1687
- map_attribute xml_name, to: attr_name
1688
- end
1689
- end
1690
- end
1691
-
1692
- klass.attribute attr_name, inline_klass, collection: collection
1693
- elsif has_flags
1694
- # Non-markup field with flags needs its own class for flag attributes
1695
- inline_klass = Class.new(Lutaml::Model::Serializable)
1696
- inline_klass.attribute :content, content_type
1697
- field_def.define_flag&.each { |f| add_inline_flag(inline_klass, f) }
1698
- field_def.flag&.each { |f| add_flag_reference(inline_klass, f) }
1699
-
1700
- flag_attr_maps = field_def.define_flag&.filter_map do |f|
1701
- [f.name, safe_attr(f.name)] if f.name
1702
- end || []
1703
- flag_ref_maps = field_def.flag&.filter_map do |f|
1704
- [f.ref, safe_attr(f.ref)] if f.ref
1705
- end || []
1706
-
1707
- inline_name = field_def.name
1708
- inline_klass.class_eval do
1709
- xml do
1710
- element inline_name
1711
- map_content to: :content
1712
- flag_attr_maps.each do |xml_name, attr_sym|
1713
- map_attribute xml_name, to: attr_sym
1714
- end
1715
- flag_ref_maps.each do |xml_name, attr_sym|
1716
- map_attribute xml_name, to: attr_sym
1717
- end
1718
- end
1719
- key_value do
1720
- root inline_name
1721
- map "STRVALUE", to: :content
1722
- flag_attr_maps.each do |xml_name, attr_sym|
1723
- map xml_name, to: attr_sym
1724
- end
1725
- flag_ref_maps.each do |xml_name, attr_sym|
1726
- map xml_name, to: attr_sym
1727
- end
1728
- end
1729
- end
1730
-
1731
- # Register inline field class for JSON mapping lookups (scoped to parent)
1732
- klass_name = scoped_field_name(field_def.name)
1733
- @classes[klass_name] = inline_klass
1734
-
1735
- klass.attribute attr_name, inline_klass, collection: collection
1736
- else
1737
- klass.attribute attr_name, content_type, collection: collection
1738
- end
1739
- end
1740
-
1741
- def add_inline_assembly(klass, assembly_def)
1742
- return unless assembly_def.name
1743
-
1744
- attr_name = safe_attr(assembly_def.name)
1745
- collection = unbounded?(assembly_def.max_occurs)
1746
-
1747
- inline_klass = Class.new(Lutaml::Model::Serializable)
1748
-
1749
- assembly_def.define_flag&.each { |f| add_inline_flag(inline_klass, f) }
1750
- assembly_def.flag&.each { |f| add_flag_reference(inline_klass, f) }
1751
-
1752
- process_model(inline_klass, assembly_def.model) if assembly_def.model
1753
-
1754
- inline_name = assembly_def.name
1755
- inline_flag_defs = assembly_def.define_flag || []
1756
- inline_flag_refs = assembly_def.flag || []
1757
- inline_child_mappings = assembly_def.model ? collect_inline_child_mappings(assembly_def) : []
1758
- inline_flag_attr_maps = inline_flag_defs.filter_map do |f|
1759
- [f.name, safe_attr(f.name)] if f.name
1760
- end
1761
- inline_flag_ref_maps = inline_flag_refs.filter_map do |f|
1762
- [f.ref, safe_attr(f.ref)] if f.ref
1763
- end
1764
-
1765
- inline_klass.class_eval do
1766
- xml do
1767
- element inline_name
1768
- ordered
1769
-
1770
- inline_flag_attr_maps.each do |xml_name, attr_name|
1771
- map_attribute xml_name, to: attr_name
1772
- end
1773
-
1774
- inline_flag_ref_maps.each do |xml_name, attr_name|
1775
- map_attribute xml_name, to: attr_name
1776
- end
1777
-
1778
- inline_child_mappings.each do |mapping|
1779
- map_element mapping[:xml_name], to: mapping[:attr_name]
1780
- end
1781
- end
1782
- end
1783
-
1784
- klass.attribute attr_name, inline_klass, collection: collection
1785
-
1786
- # Add JSON mappings for the inline assembly
1787
- build_inline_assembly_json(klass, inline_klass, inline_name, assembly_def)
1788
- end
1789
-
1790
- def build_inline_assembly_json(_parent_klass, inline_klass, inline_name,
1791
- assembly_def)
1792
- flag_defs = assembly_def.define_flag || []
1793
- flag_refs = assembly_def.flag || []
1794
-
1795
- inline_flag_attr_maps = flag_defs.filter_map do |f|
1796
- [f.name, safe_attr(f.name)] if f.name
1797
- end
1798
- inline_flag_ref_maps = flag_refs.filter_map do |f|
1799
- [f.ref, safe_attr(f.ref)] if f.ref
1800
- end
1801
-
1802
- json_field_mappings = collect_json_field_mappings(assembly_def)
1803
- json_assembly_mappings = collect_json_assembly_mappings(assembly_def)
1804
-
1805
- # Check if this inline assembly has any nested assembly children
1806
- # that might be empty objects (choice assemblies). If so, we need
1807
- # custom JSON handling because lutaml-model skips empty nested models.
1808
- has_nested_asm = json_assembly_mappings.any?
1809
-
1810
- if has_nested_asm
1811
- # Use custom of_json/to_json that handles empty nested assemblies
1812
- build_inline_assembly_json_custom(
1813
- inline_klass, inline_name, inline_flag_attr_maps, inline_flag_ref_maps,
1814
- json_field_mappings, json_assembly_mappings
1815
- )
1816
- else
1817
- # Standard lutaml-model mapping approach
1818
- build_inline_assembly_json_standard(
1819
- inline_klass, inline_name, inline_flag_attr_maps, inline_flag_ref_maps,
1820
- json_field_mappings
1821
- )
1822
- end
1823
- end
1824
-
1825
- def build_inline_assembly_json_standard(inline_klass, inline_name,
1826
- inline_flag_attr_maps, inline_flag_ref_maps,
1827
- json_field_mappings)
1828
- regular_field_mappings = json_field_mappings.reject do |m|
1829
- m[:vk_flag] || m[:by_key]
1830
- end
1831
- vk_flag_mappings = json_field_mappings.select { |m| m[:vk_flag] }
1832
- by_key_mappings = json_field_mappings.select { |m| m[:by_key] }
1833
-
1834
- inline_klass.class_eval do
1835
- key_value do
1836
- root inline_name
1837
-
1838
- inline_flag_attr_maps.each do |xml_name, attr_name|
1839
- map xml_name, to: attr_name
1840
- end
1841
-
1842
- inline_flag_ref_maps.each do |xml_name, attr_name|
1843
- map xml_name, to: attr_name
1844
- end
1845
-
1846
- regular_field_mappings.each do |mapping|
1847
- if mapping[:scalar]
1848
- map mapping[:json_name], to: mapping[:attr_name],
1849
- with: { to: mapping[:to_method], from: mapping[:from_method] }
1850
- else
1851
- map mapping[:json_name], to: mapping[:attr_name],
1852
- render_empty: true
1853
- end
1854
- end
1855
- end
1856
- end
1857
-
1858
- define_scalar_field_callbacks(inline_klass, regular_field_mappings)
1859
-
1860
- vk_flag_mappings.each do |mapping|
1861
- callbacks = build_vk_flag_field_callbacks(
1862
- inline_klass, mapping[:field_klass], mapping[:json_name], mapping[:attr_name]
1863
- )
1864
- inline_klass.class_eval do
1865
- key_value do
1866
- map mapping[:json_name], to: mapping[:attr_name],
1867
- with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1868
- end
1869
- end
1870
- end
1871
-
1872
- by_key_mappings.each do |mapping|
1873
- callbacks = build_by_key_field_callbacks(
1874
- inline_klass, mapping[:field_klass], mapping[:json_name],
1875
- mapping[:attr_name], mapping[:json_key_flag]
1876
- )
1877
- inline_klass.class_eval do
1878
- key_value do
1879
- map mapping[:json_name], to: mapping[:attr_name],
1880
- with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1881
- end
1882
- end
1883
- end
1884
- end
1885
-
1886
- def build_inline_assembly_json_custom(inline_klass, inline_name,
1887
- inline_flag_attr_maps, inline_flag_ref_maps,
1888
- json_field_mappings, json_assembly_mappings)
1889
- # Build full JSON mappings — include assembly mappings so lutaml-model's
1890
- # Transformation path can parse them when this class is nested in a parent.
1891
- regular_field_mappings = json_field_mappings.reject do |m|
1892
- m[:vk_flag] || m[:by_key]
1893
- end
1894
- vk_flag_mappings = json_field_mappings.select { |m| m[:vk_flag] }
1895
- by_key_mappings = json_field_mappings.select { |m| m[:by_key] }
1896
-
1897
- # Pre-generate method names for assembly mappings (only to: for serialization)
1898
- json_assembly_mappings.each do |mapping|
1899
- json_name = mapping[:json_name]
1900
- attr_sym = mapping[:attr_name]
1901
- mapping[:to_method] =
1902
- :"json_to_asm_#{attr_sym}_#{json_name.gsub('-', '_')}"
1903
- end
1904
-
1905
- inline_klass.class_eval do
1906
- key_value do
1907
- root inline_name
1908
-
1909
- inline_flag_attr_maps.each do |xml_name, attr_name|
1910
- map xml_name, to: attr_name
1911
- end
1912
-
1913
- inline_flag_ref_maps.each do |xml_name, attr_name|
1914
- map xml_name, to: attr_name
1915
- end
1916
-
1917
- regular_field_mappings.each do |mapping|
1918
- if mapping[:scalar]
1919
- map mapping[:json_name], to: mapping[:attr_name],
1920
- with: { to: mapping[:to_method], from: mapping[:from_method] }
1921
- else
1922
- map mapping[:json_name], to: mapping[:attr_name],
1923
- render_empty: true
1924
- end
1925
- end
1926
-
1927
- # Assembly mappings use to: override for serialization.
1928
- # Default from: handles casting via lutaml-model's built-in mechanism.
1929
- json_assembly_mappings.each do |mapping|
1930
- map mapping[:json_name], to: mapping[:attr_name],
1931
- with: { to: mapping[:to_method] }
1932
- end
1933
- end
1934
- end
1935
-
1936
- # Define with: callback methods for scalar field mappings
1937
- define_scalar_field_callbacks(inline_klass, regular_field_mappings)
1938
-
1939
- vk_flag_mappings.each do |mapping|
1940
- callbacks = build_vk_flag_field_callbacks(
1941
- inline_klass, mapping[:field_klass], mapping[:json_name], mapping[:attr_name]
1942
- )
1943
- inline_klass.class_eval do
1944
- key_value do
1945
- map mapping[:json_name], to: mapping[:attr_name],
1946
- with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1947
- end
1948
- end
1949
- end
1950
-
1951
- by_key_mappings.each do |mapping|
1952
- callbacks = build_by_key_field_callbacks(
1953
- inline_klass, mapping[:field_klass], mapping[:json_name],
1954
- mapping[:attr_name], mapping[:json_key_flag]
1955
- )
1956
- inline_klass.class_eval do
1957
- key_value do
1958
- map mapping[:json_name], to: mapping[:attr_name],
1959
- with: { to: callbacks[:to_method], from: callbacks[:from_method] }
1960
- end
1961
- end
1962
- end
1963
-
1964
- # Define to: callback methods for assembly mappings.
1965
- json_assembly_mappings.each do |mapping|
1966
- attr_sym = mapping[:attr_name]
1967
- to_method = mapping[:to_method]
1968
- json_name = mapping[:json_name]
1969
-
1970
- inline_klass.define_method(to_method) do |instance, doc|
1971
- current = instance.instance_variable_get("@#{attr_sym}")
1972
- if current
1973
- if current.is_a?(Lutaml::Model::Serializable)
1974
- # Serialize the nested assembly's attributes into the doc
1975
- sub = {}
1976
- current.class.mappings_for(:json).instance_variable_get(:@mappings).each do |key, rule|
1977
- val = current.send(rule.to)
1978
- next if val.nil?
1979
-
1980
- sub[key] = val.respond_to?(:content) ? val.content : val
1981
- end
1982
- doc[json_name] = sub.empty? ? {} : sub
1983
- else
1984
- doc[json_name] = current
1985
- end
1986
- end
1987
- end
1988
- end
1989
- end
1990
-
1991
- def define_scalar_field_callbacks(klass, field_mappings)
1992
- field_mappings.each do |mapping|
1993
- next unless mapping[:scalar]
1994
-
1995
- field_klass = mapping[:field_klass]
1996
- attr_sym = mapping[:attr_name]
1997
-
1998
- klass.define_method(mapping[:from_method]) do |instance, value|
1999
- if value.is_a?(Array)
2000
- instance.instance_variable_set("@#{attr_sym}", value.map do |v|
2001
- field_klass.new(content: v)
2002
- end)
2003
- elsif value
2004
- instance.instance_variable_set("@#{attr_sym}",
2005
- field_klass.new(content: value))
2006
- end
2007
- end
2008
-
2009
- klass.define_method(mapping[:to_method]) do |instance, doc|
2010
- current = instance.instance_variable_get("@#{attr_sym}")
2011
- if current.is_a?(Array)
2012
- doc[mapping[:json_name]] = current.map do |item|
2013
- item.respond_to?(:content) ? item.content : item
2014
- end
2015
- elsif current
2016
- doc[mapping[:json_name]] =
2017
- current.respond_to?(:content) ? current.content : current
2018
- end
2019
- end
2020
- end
2021
- end
2022
-
2023
- def collect_inline_child_mappings(assembly_def)
2024
- model = assembly_def.model
2025
- return [] unless model
2026
-
2027
- collect_model_child_mappings(model)
2028
- end
2029
-
2030
- # ── Flag Handling ─────────────────────────────────────────────────
2031
-
2032
- def add_inline_flag(klass, flag_def)
2033
- return unless flag_def.name
2034
-
2035
- attr_name = safe_attr(flag_def.name)
2036
- type = TypeMapper.map(flag_def.as_type)
2037
- klass.attribute attr_name, type
2038
- end
2039
-
2040
- def add_flag_reference(klass, flag_ref)
2041
- return unless flag_ref.ref
2042
-
2043
- flag_name = flag_ref.ref
2044
- flag_def = @flag_defs[flag_name]
2045
- attr_name = safe_attr(flag_name)
2046
- type = flag_def ? TypeMapper.map(flag_def.as_type) : :string
2047
- klass.attribute attr_name, type
2048
- end
2049
-
2050
- # ── Choice Handling ───────────────────────────────────────────────
2051
-
2052
- def process_choice(klass, choice)
2053
- choice.assembly&.each { |ar| add_assembly_reference(klass, ar) }
2054
- choice.field&.each { |fr| add_field_reference(klass, fr) }
2055
- choice.define_assembly&.each { |ad| add_inline_assembly(klass, ad) }
2056
- choice.define_field&.each { |fd| add_inline_field(klass, fd) }
2057
- end
2058
-
2059
- def process_choice_group(klass, choice_group)
2060
- choice_group.assembly&.each do |ar|
2061
- add_grouped_assembly_reference(klass, ar)
2062
- end
2063
- choice_group.field&.each { |fr| add_grouped_field_reference(klass, fr) }
2064
- choice_group.define_assembly&.each { |ad| add_inline_assembly(klass, ad) }
2065
- choice_group.define_field&.each { |fd| add_inline_field(klass, fd) }
2066
- end
2067
-
2068
- def add_grouped_assembly_reference(klass, grouped_ref)
2069
- ref_name = grouped_ref.ref
2070
- return unless ref_name
2071
-
2072
- assembly_klass = @classes["Assembly_#{ref_name.gsub('-', '_')}"] ||
2073
- create_placeholder_assembly(ref_name)
2074
-
2075
- attr_name = safe_attr(ref_name)
2076
- klass.attribute attr_name, assembly_klass
2077
- end
2078
-
2079
- def add_grouped_field_reference(klass, grouped_ref)
2080
- ref_name = grouped_ref.ref
2081
- return unless ref_name
2082
-
2083
- field_klass = @classes["Field_#{ref_name.gsub('-', '_')}"]
2084
- return unless field_klass
2085
-
2086
- attr_name = safe_attr(ref_name)
2087
- klass.attribute attr_name, field_klass
2088
- end
2089
-
2090
- # ── Helpers ───────────────────────────────────────────────────────
2091
-
2092
- def scoped_field_name(field_name)
2093
- base = "Field_#{field_name.gsub('-', '_')}"
2094
- @current_assembly_name ? "#{base}_in_#{@current_assembly_name}" : base
2095
- end
2096
-
2097
- def unbounded?(max_occurs)
2098
- max_occurs == "unbounded" || (max_occurs.to_i > 1 && max_occurs != "1")
2099
- end
2100
-
2101
- def create_placeholder_assembly(name)
2102
- key = "Assembly_#{name.gsub('-', '_')}"
2103
- @classes[key] ||= Class.new(Lutaml::Model::Serializable)
2104
- end
2105
-
2106
- def add_any_content(klass)
2107
- klass.attribute :any_content, :string
2108
- end
2109
-
2110
- def add_json_root_handling(klass, json_root)
2111
- klass.instance_variable_set(:@json_root_name, json_root)
2112
- class << klass
2113
- attr_reader :json_root_name
2114
- end
2115
-
2116
- original_of_json = klass.method(:of_json)
2117
- klass.define_singleton_method(:of_json) do |doc, options = {}|
2118
- if doc.is_a?(Hash) && doc.key?(json_root_name)
2119
- original_of_json.call(doc[json_root_name], options)
2120
- else
2121
- original_of_json.call(doc, options)
2122
- end
2123
- end
2124
-
2125
- original_to_json = klass.method(:to_json)
2126
- klass.define_singleton_method(:to_json) do |instance, options = {}|
2127
- json_str = original_to_json.call(instance, options)
2128
- { json_root_name => JSON.parse(json_str) }.to_json
2129
- end
2130
-
2131
- klass.send(:define_method, :to_json) do |options = {}|
2132
- self.class.to_json(self, options)
2133
- end
2134
-
2135
- # YAML root wrapping — mirrors JSON root handling
2136
- original_of_yaml = klass.method(:of_yaml)
2137
- klass.define_singleton_method(:of_yaml) do |doc, options = {}|
2138
- if doc.is_a?(Hash) && doc.key?(json_root_name)
2139
- original_of_yaml.call(doc[json_root_name], options)
2140
- else
2141
- original_of_yaml.call(doc, options)
2142
- end
2143
- end
2144
-
2145
- original_to_yaml = klass.method(:to_yaml)
2146
- klass.define_singleton_method(:to_yaml) do |instance, options = {}|
2147
- yaml_str = original_to_yaml.call(instance, options)
2148
- data = YAML.safe_load(yaml_str,
2149
- permitted_classes: [Date, DateTime, Time, Symbol])
2150
- { json_root_name => data }.to_yaml
2151
- end
2152
-
2153
- klass.send(:define_method, :to_yaml) do |options = {}|
2154
- self.class.to_yaml(self, options)
2155
- end
2156
- end
2157
-
2158
- # ── Constraint Validation Integration ──────────────────────────────
2159
-
2160
- def apply_constraint_validation(klass, constraint_def)
2161
- return unless constraint_def
2162
-
2163
- # Store the constraint definition on the class for later access
2164
- klass.instance_variable_set(:@metaschema_constraints, constraint_def)
2165
- klass.define_singleton_method(:metaschema_constraints) do
2166
- @metaschema_constraints
2167
- end
2168
-
2169
- klass.define_method(:validate_constraints) do
2170
- validator = ConstraintValidator.new
2171
- validator.validate(self, self.class.metaschema_constraints)
2172
- end
2173
- end
2174
279
  end
2175
280
  end