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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +49 -420
- data/README.adoc +54 -4
- data/lib/metaschema/markup_multiline_datatype.rb +41 -0
- data/lib/metaschema/model_generator/assembly_factory.rb +1583 -0
- data/lib/metaschema/model_generator/field_factory.rb +275 -0
- data/lib/metaschema/model_generator/services/collapsibles_collapser.rb +82 -0
- data/lib/metaschema/model_generator/services/field_deserializer.rb +92 -0
- data/lib/metaschema/model_generator/services/field_serializer.rb +111 -0
- data/lib/metaschema/model_generator/utils.rb +64 -0
- data/lib/metaschema/model_generator.rb +98 -1993
- data/lib/metaschema/ruby_source_emitter.rb +9 -3
- data/lib/metaschema/type_mapper.rb +21 -1
- data/lib/metaschema/version.rb +1 -1
- data/lib/metaschema.rb +1 -0
- metadata +9 -2
|
@@ -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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
# ──
|
|
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
|