lutaml-lml 0.1.7 → 0.2.0

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,32 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Lutaml
4
6
  module Lml
5
7
  class ModelCompiler
6
8
  class ValidationError < Error; end
7
9
 
8
10
  TYPE_MAP = {
9
- "String" => :string,
10
- "Integer" => :integer,
11
- "Boolean" => :boolean,
12
- "Float" => :float,
13
- "Date" => :date,
14
- "date_time" => :date_time,
15
- "DateTime" => :date_time,
16
- "Time" => :time,
17
- "Uri" => :string,
11
+ 'String' => :string,
12
+ 'string' => :string,
13
+ 'Integer' => :integer,
14
+ 'integer' => :integer,
15
+ 'Boolean' => :boolean,
16
+ 'boolean' => :boolean,
17
+ 'Float' => :float,
18
+ 'float' => :float,
19
+ 'Date' => :date,
20
+ 'date_time' => :date_time,
21
+ 'DateTime' => :date_time,
22
+ 'Time' => :time,
23
+ 'Uri' => :string
18
24
  }.freeze
19
25
 
20
26
  # Tokens that denote unbounded cardinality in LML attribute
21
27
  # declarations. Anything else must parse as an Integer.
22
28
  UNBOUNDED_TOKENS = %w[* n N unbounded].freeze
23
29
 
24
- def initialize(namespace: nil)
30
+ def initialize(namespace: nil, artifact_paths: {}, default_artifact: nil)
25
31
  @namespace = namespace
32
+ @artifact_paths = artifact_paths
33
+ @default_artifact = default_artifact
26
34
  @compiled = {}
27
35
  @forward_refs = {}
28
36
  @enum_names = Set.new
29
37
  @enum_classes = {}
38
+ @declared_namespaces = []
39
+ @serialization_mappings = []
40
+ @namespace_classes = {}
30
41
  end
31
42
 
32
43
  def compile(input)
@@ -70,14 +81,14 @@ module Lutaml
70
81
 
71
82
  private
72
83
 
73
- def validate_instance(instance, errors, path = "root")
84
+ def validate_instance(instance, errors, path = 'root')
74
85
  if instance.instance && Array(instance.attributes).empty?
75
86
  validate_instance(instance.instance, errors, "#{path}.instance")
76
87
  return
77
88
  end
78
89
 
79
90
  attrs = Array(instance.attributes)
80
- type_attr = attrs.find { |a| a.name == "type" }
91
+ type_attr = attrs.find { |a| a.name == 'type' }
81
92
  type_name = type_attr ? type_attr.value.to_s : instance.type
82
93
  klass = @compiled[demodulize(type_name)]
83
94
  unless klass
@@ -106,19 +117,20 @@ module Lutaml
106
117
 
107
118
  attrs.each do |attr|
108
119
  next unless schema_attrs.include?(attr.name)
120
+
109
121
  attr_def = klass.attributes[attr.name.to_sym]
110
122
  next unless attr_def
111
123
 
112
- if attr_def.collection? && attr.instances.any?
113
- attr.instances.each_with_index do |nested, i|
114
- validate_instance(nested, errors, "#{path}.#{attr.name}[#{i}]")
115
- end
124
+ next unless attr_def.collection? && attr.instances.any?
125
+
126
+ attr.instances.each_with_index do |nested, i|
127
+ validate_instance(nested, errors, "#{path}.#{attr.name}[#{i}]")
116
128
  end
117
129
  end
118
130
 
119
- if instance.instance
120
- validate_instance(instance.instance, errors, "#{path}.instance")
121
- end
131
+ return unless instance.instance
132
+
133
+ validate_instance(instance.instance, errors, "#{path}.instance")
122
134
  end
123
135
 
124
136
  def hydrate_instance(instance)
@@ -134,13 +146,15 @@ module Lutaml
134
146
  end
135
147
 
136
148
  def resolve_instance_type(instance)
137
- type_attr = Array(instance.attributes).find { |a| a.name == "type" }
138
- raw = type_attr ? type_attr.value.to_s : instance.type.to_s
139
- demodulize(raw)
149
+ # L6: the explicit isa keyword is the type override; a `type`
150
+ # attribute is ordinary data (pubid's identifier-kind field)
151
+ return demodulize(instance.isa.to_s) if instance.isa
152
+
153
+ demodulize(instance.type.to_s)
140
154
  end
141
155
 
142
156
  def demodulize(name)
143
- name.split("::").last.to_s
157
+ name.split('::').last.to_s
144
158
  end
145
159
 
146
160
  def hydrate_as_untyped(instance, type_name)
@@ -168,10 +182,10 @@ module Lutaml
168
182
  nested.map { |i| hydrate_instance(i) }
169
183
  elsif value.is_a?(Array)
170
184
  value
171
- elsif enum_class = attr_def && enum_class_for(attr_def.type)
185
+ elsif (enum_class = attr_def && enum_class_for(attr_def.type))
172
186
  value_name = enum_value_name(value.to_s)
173
187
  description = enum_class.values
174
- .find { |v| v.name == value_name }&.description
188
+ .find { |v| v.name == value_name }&.description
175
189
  enum_class.new(value: value_name, description: description)
176
190
  elsif !value.nil?
177
191
  value
@@ -179,11 +193,28 @@ module Lutaml
179
193
  end
180
194
 
181
195
  def compile_document(doc)
196
+ @declared_namespaces = Array(doc.namespaces)
197
+ @serialization_mappings = Array(doc.serialization_mappings)
182
198
  doc.classes.each { |c| compile_class(c) }
199
+ hoist_class_serialization_mappings(doc)
183
200
  doc.enums.each { |e| compile_enum(e) }
184
201
  doc.data_types.each { |dt| compile_data_type(dt) }
185
202
  resolve_forward_references
186
203
  apply_xml_mappings
204
+ apply_key_value_mappings
205
+ register_string_formats(doc)
206
+ end
207
+
208
+ # In-class `mapping <format> { … }` is the shortcut for a sibling
209
+ # mapping targeting the enclosing class; hoist them so emission is
210
+ # uniform.
211
+ def hoist_class_serialization_mappings(doc)
212
+ (Array(doc.classes) + Array(doc.data_types)).each do |klass_def|
213
+ Array(klass_def.serialization_mappings).each do |mapping|
214
+ mapping.target = klass_def.name.to_s
215
+ @serialization_mappings << mapping
216
+ end
217
+ end
187
218
  end
188
219
 
189
220
  def resolve_forward_references
@@ -201,27 +232,50 @@ module Lutaml
201
232
  def compile_class(klass_def)
202
233
  name = klass_def.name.to_s
203
234
  compiled_klass = build_compiled_class(klass_def)
235
+ apply_declared_mappings(compiled_klass, klass_def)
236
+ declare_derived_fields(compiled_klass, klass_def)
204
237
  register(name, compiled_klass)
205
238
  end
206
239
 
207
- alias_method :compile_data_type, :compile_class
240
+ # Emit the lutaml-model mapping blocks declared in the LML source:
241
+ # mapping yaml { map "full-name", to: "name" }
242
+ # becomes klass.yml do |m| m.map "full-name", to: :name end
243
+ def apply_declared_mappings(compiled_klass, def_obj)
244
+ return unless def_obj.is_a?(UmlClass) || def_obj.is_a?(DataType)
245
+
246
+ def_obj.wire_mappings.to_a.group_by(&:format).each do |format, entries|
247
+ next unless format && Lutaml::Model::FormatRegistry.registered?(format.to_sym)
248
+
249
+ compiled_klass.public_send(format) do |mapping|
250
+ entries.each { |entry| mapping.map(entry.wire, to: entry.to.to_sym) }
251
+ end
252
+ end
253
+ end
254
+
255
+ alias compile_data_type compile_class
208
256
 
209
- EnumValue = Struct.new(:name, :description)
257
+ EnumValue = Struct.new(:name, :description, :payload)
210
258
 
211
259
  def compile_enum(enum_def)
212
260
  name = enum_def.name.to_s
261
+ expand_from_table(enum_def)
213
262
  values = extract_enum_values(enum_def)
214
263
 
264
+ payload_fields = Array(enum_def.member_fields).map(&:name).map(&:to_s)
215
265
  compiled_klass = Class.new(Lutaml::Model::Serializable) do
216
266
  attribute :value, :string, default: values.first.name
217
267
  attribute :description, :string
268
+ payload_fields.each do |field|
269
+ attribute field.to_sym, :string
270
+ end
218
271
 
219
272
  define_method(:to_s) { value }
220
273
  end
221
274
 
222
275
  values.each do |value|
223
- compiled_klass.define_singleton_method(value.name) do
224
- new(value: value.name, description: value.description)
276
+ accessor = value.name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
277
+ compiled_klass.define_singleton_method(accessor) do
278
+ new({ value: value.name, description: value.description }.merge(value.payload || {}))
225
279
  end
226
280
  end
227
281
  compiled_klass.define_singleton_method(:values) { values }
@@ -237,7 +291,7 @@ module Lutaml
237
291
  def build_compiled_class(def_obj)
238
292
  all_attrs = build_attributes(def_obj)
239
293
  immediate, deferred = all_attrs.partition do |(_, raw_type, type, _)|
240
- type != :string || TYPE_MAP.key?(raw_type) || raw_type.start_with?("reference:(")
294
+ type != :string || TYPE_MAP.key?(raw_type) || raw_type.start_with?('reference:(')
241
295
  end
242
296
 
243
297
  compiled_klass = Class.new(Lutaml::Model::Serializable) do
@@ -245,6 +299,19 @@ module Lutaml
245
299
  attribute attr_name, type, options
246
300
  end
247
301
  end
302
+ render_nil_map = Array(def_obj.attributes).to_h do |a|
303
+ [a.name.to_sym, declared_render_nil(a)]
304
+ end.compact
305
+ unless render_nil_map.empty?
306
+ compiled_klass.class_eval do
307
+ @lml_render_nil = render_nil_map
308
+
309
+ class << self
310
+ attr_reader :lml_render_nil
311
+ end
312
+ end
313
+ apply_render_nil_mappings(compiled_klass, render_nil_map)
314
+ end
248
315
 
249
316
  name = def_obj.name.to_s
250
317
  @forward_refs[name] = deferred unless deferred.empty?
@@ -258,6 +325,9 @@ module Lutaml
258
325
  @compiled.each do |name, klass|
259
326
  next if @enum_names.include?(name)
260
327
 
328
+ custom = serialization_mapping_for(name, 'xml')
329
+ next apply_custom_xml_mapping(klass, custom) if custom
330
+
261
331
  apply_xml_mapping(klass, name)
262
332
  end
263
333
  end
@@ -274,20 +344,130 @@ module Lutaml
274
344
  end
275
345
  end
276
346
 
347
+ # RS 3010 extension: key-value mapping blocks for the declared
348
+ # formats (yaml/json/toml share lutaml-model's key_value model).
349
+ def apply_key_value_mappings
350
+ @compiled.each do |name, klass|
351
+ next if @enum_names.include?(name)
352
+
353
+ custom = serialization_mapping_for(name, 'key_value')
354
+ apply_custom_key_value_mapping(klass, custom) if custom
355
+ end
356
+ end
357
+
358
+ def serialization_mapping_for(target, format)
359
+ @serialization_mappings.find do |m|
360
+ m.target.to_s == target && serialization_format(m.format) == format
361
+ end
362
+ end
363
+
364
+ def serialization_format(format)
365
+ %w[yaml json toml].include?(format.to_s) ? 'key_value' : format.to_s
366
+ end
367
+
368
+ def apply_custom_xml_mapping(klass, mapping)
369
+ check_namespace_ambiguity(mapping)
370
+ ns_class = namespace_class_for(mapping.namespace_ref) if mapping.namespace_ref
371
+ rule_ns = mapping.rules.select { |r| r.kind == 'namespace' }
372
+ .to_h { |r| [r.field, namespace_class_for(r.field)] }
373
+ klass.xml do |m|
374
+ m.root(mapping.element_name) if mapping.element_name
375
+ m.namespace(ns_class) if ns_class
376
+ m.mixed_content if mapping.mixed_content
377
+ mapping.rules.each do |rule|
378
+ case rule.kind
379
+ when 'element' then m.map_element(rule.wire, to: rule.field.to_sym)
380
+ when 'attribute' then m.map_attribute(rule.wire, to: rule.field.to_sym)
381
+ when 'content' then m.map_content(to: rule.field.to_sym)
382
+ when 'namespace' then m.namespace(rule_ns[rule.field])
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ def apply_custom_key_value_mapping(klass, mapping)
389
+ formats = mapping.format.to_s == 'key_value' ? %w[yaml json toml] : [mapping.format.to_s]
390
+ klass.instance_eval do
391
+ formats.each do |format|
392
+ public_send(format) do |m|
393
+ mapping.rules.each do |rule|
394
+ case rule.kind
395
+ when 'map' then m.map(rule.wire, to: rule.field.to_sym)
396
+ when 'key' then m.map_key(to_instance: rule.field.to_sym)
397
+ when 'value'
398
+ if rule.mode == 'as_attribute'
399
+ m.map_value(as_attribute: rule.field.to_sym)
400
+ else
401
+ m.map_value(to_instance: rule.field.to_sym)
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end
407
+ end
408
+ end
409
+
410
+ def check_namespace_ambiguity(mapping)
411
+ return unless mapping.format.to_s == 'xml'
412
+ return if mapping.namespace_ref || @declared_namespaces.length < 2
413
+
414
+ raise ArgumentError,
415
+ "mapping #{mapping.format} #{mapping.target}: ambiguous default " \
416
+ "namespace — #{@declared_namespaces.length} namespaces in scope " \
417
+ "(#{@declared_namespaces.map(&:name).join(', ')}); declare one with " \
418
+ "'namespace <name>'"
419
+ end
420
+
421
+ def namespace_class_for(ref)
422
+ return nil if ref.nil?
423
+
424
+ require "lutaml/xml/namespace" unless defined?(Lutaml::Xml::Namespace)
425
+
426
+ ns_class = @namespace_classes[ref]
427
+ return ns_class if ns_class
428
+
429
+ declared = @declared_namespaces.find { |ns| ns.name.to_s == ref }
430
+ raise ArgumentError, "unknown namespace '#{ref}'" unless declared
431
+
432
+ ns_class = Class.new(Lutaml::Xml::Namespace)
433
+ uri = declared.uri
434
+ prefix = declared.prefix.to_s
435
+ ns_class.define_singleton_method(:uri) { uri }
436
+ ns_class.define_singleton_method(:prefix_default) { prefix }
437
+ @namespace_classes[ref] = ns_class
438
+ end
439
+
277
440
  def build_attributes(klass_def)
278
441
  Array(klass_def.attributes).map do |attr|
279
442
  attr_name = attr.name.to_sym
280
- raw_type = attr.type.to_s
443
+ raw_type = resolve_values_type(klass_def, attr)
281
444
  type = resolve_type(raw_type)
282
445
  options = build_options(attr)
283
446
  [attr_name, raw_type, type, options]
284
447
  end
285
448
  end
286
449
 
450
+ # RS 3001 §Attribute: `values { v1, v2 }` expands into an anonymous
451
+ # enum baked from the inline set; the attribute is typed with it.
452
+ def resolve_values_type(klass_def, attr)
453
+ return attr.type.to_s unless attr.value_set&.any?
454
+
455
+ enum_name = anonymous_enum_name(klass_def.name, attr.name)
456
+ members = attr.value_set.map { |v| ::Lutaml::Lml::TopElementAttribute.new(name: v.to_s) }
457
+ compile_enum(::Lutaml::Lml::Enum.new(name: enum_name, attributes: members))
458
+ enum_name
459
+ end
460
+
461
+ def anonymous_enum_name(class_name, attr_name)
462
+ base = class_name.to_s.split('::').last
463
+ attr_part = attr_name.to_s.split('_').map(&:capitalize).join
464
+ "#{base}#{attr_part}Values"
465
+ end
466
+
287
467
  def resolve_type(type_name)
288
468
  if TYPE_MAP.key?(type_name)
289
469
  TYPE_MAP[type_name]
290
- elsif type_name.start_with?("reference:(")
470
+ elsif type_name.start_with?('reference:(')
291
471
  :string
292
472
  elsif @compiled.key?(type_name)
293
473
  @compiled[type_name]
@@ -297,11 +477,12 @@ module Lutaml
297
477
  end
298
478
 
299
479
  def build_options(attr)
480
+ declared = declared_attribute_options(attr)
300
481
  options = {}
301
482
  card = attr.cardinality
302
483
  unless card
303
484
  # LML attributes without an explicit cardinality are optional (0..1)
304
- options[:default] = nil
485
+ options[:default] = declared[:default]
305
486
  return options
306
487
  end
307
488
 
@@ -312,12 +493,37 @@ module Lutaml
312
493
  options[:collection] = true
313
494
  elsif max.nil? && min && min > 1
314
495
  options[:collection] = true
315
- elsif min == 0
496
+ elsif min.zero?
316
497
  options[:default] = nil
317
498
  end
499
+ # a declared default literal wins over the absent-marker
500
+ options[:default] = declared[:default] if declared.key?(:default)
501
+ options
502
+ end
503
+
504
+ # L7b: default <literal> and render_nil true|false ride the
505
+ # keyword-attribute properties (bare name-value lines). Defaults
506
+ # fire only on ABSENT input; render_nil mirrors lutaml-model.
507
+ def declared_attribute_options(attr)
508
+ options = {}
509
+ Array(attr.properties).each do |prop|
510
+ next unless prop.name.to_s == 'default'
511
+
512
+ value = prop.value
513
+ value = value[:string] if value.is_a?(Hash) && value.key?(:string)
514
+ options[:default] = value&.to_s
515
+ end
318
516
  options
319
517
  end
320
518
 
519
+ # render_nil is a lutaml-model MAPPING option, not an attribute
520
+ # option: collect the declaration for the mapping passes
521
+ def declared_render_nil(attr)
522
+ Array(attr.properties).find do |prop|
523
+ prop.name.to_s == 'render_nil'
524
+ end&.then { |prop| prop.value.to_s == 'true' }
525
+ end
526
+
321
527
  def parse_cardinality_value(val)
322
528
  return nil if val.nil?
323
529
  return Float::INFINITY if UNBOUNDED_TOKENS.include?(val.to_s)
@@ -326,13 +532,90 @@ module Lutaml
326
532
  raise ValidationError, "Unrecognized cardinality token: #{val.inspect}"
327
533
  end
328
534
 
535
+ # L7a point 3: from_table members are baked at compile time — the
536
+ # artifact table's rows become the values, validated against the
537
+ # declared member_fields (unknown field = error; missing = nil).
538
+ # The member name comes from the row's "name" column.
539
+ def expand_from_table(enum_def)
540
+ return unless enum_def.from_table_name
541
+
542
+ rows = artifact_table_rows(enum_def)
543
+ known = Array(enum_def.member_fields).map { |f| f.name.to_s }
544
+ rows.each do |row|
545
+ row = row.transform_keys(&:to_s)
546
+ member_name = row.fetch('name')
547
+ unknown = row.keys - known - ['name']
548
+ if unknown.any?
549
+ raise ArgumentError,
550
+ "enum #{enum_def.name}: table row #{member_name.inspect} has " \
551
+ "undeclared fields #{unknown.inspect} (declared: #{known.inspect})"
552
+ end
553
+ payload = row.slice(*known).compact
554
+ enum_def.attributes << TopElementAttribute.new(
555
+ name: member_name,
556
+ properties: payload.map do |field, value|
557
+ TopElementAttribute.new(name: field, value: value)
558
+ end
559
+ )
560
+ end
561
+ end
562
+
563
+ def artifact_table_rows(enum_def)
564
+ reference = enum_def.from_table_artifact || @default_artifact
565
+ path = artifact_path_for(reference.to_s)
566
+ unless path
567
+ raise ArgumentError,
568
+ "enum #{enum_def.name}: from_table artifact #{reference.inspect} not found"
569
+ end
570
+
571
+ envelope = JSON.parse(File.read(path))
572
+ table = envelope['tables'].to_h[enum_def.from_table_name]
573
+ unless table
574
+ raise ArgumentError,
575
+ "enum #{enum_def.name}: table #{enum_def.from_table_name.inspect} " \
576
+ "not found in artifact #{reference.inspect}"
577
+ end
578
+
579
+ table['rows'].to_a
580
+ end
581
+
329
582
  def extract_enum_values(enum_def)
583
+ member_fields = Array(enum_def.member_fields).map do |f|
584
+ [f.name.to_s, f.type.to_s]
585
+ end
330
586
  enum_def.attributes.map do |attr|
331
- description = Array(attr.attributes).find do |nested|
332
- nested.name == "description"
333
- end
334
- EnumValue.new(attr.name.to_s, description&.type)
587
+ description = member_description(attr)
588
+ payload = extract_member_payload(attr)
589
+ validate_member_payload(enum_def, attr, payload, member_fields)
590
+ EnumValue.new(attr.name.to_s, description, payload)
591
+ end
592
+ end
593
+
594
+ # The member description rides a nested attribute named `description`
595
+ # (bare-member form) or `definition` (RS 3001 `value` blocks); both
596
+ # spellings are accepted.
597
+ def member_description(attr)
598
+ nested = Array(attr.attributes).find do |n|
599
+ %w[description definition].include?(n.name.to_s)
335
600
  end
601
+ text = nested&.type || nested&.definition || nested&.value
602
+ text.is_a?(Hash) && text.key?(:string) ? text[:string] : text
603
+ end
604
+
605
+ # L7a: per-member payload lines ride the keyword-attribute
606
+ # properties (`abbreviation = "WD"`)
607
+ def extract_member_payload(attr)
608
+ Array(attr.properties).to_h { |p| [p.name.to_s, p.value&.to_s] }
609
+ end
610
+
611
+ def validate_member_payload(enum_def, attr, payload, member_fields)
612
+ known = member_fields.map(&:first)
613
+ unknown = payload.keys - known
614
+ return unless unknown.any?
615
+
616
+ raise ArgumentError,
617
+ "enum #{enum_def.name}: member #{attr.name} has undeclared " \
618
+ "payload fields #{unknown.inspect} (declared: #{known.inspect})"
336
619
  end
337
620
 
338
621
  def enum_class_for(type)
@@ -340,7 +623,77 @@ module Lutaml
340
623
  end
341
624
 
342
625
  def enum_value_name(raw)
343
- raw.split("::").last
626
+ raw.split('::').last
627
+ end
628
+
629
+ # L7b: derived fields declare existence + type; computation is
630
+ # bound by the binder from artifact derive specs. The attribute
631
+ # participates in mappings like any other (relaton consumes URNs
632
+ # from model JSON).
633
+ def declare_derived_fields(compiled_klass, def_obj)
634
+ return unless def_obj.is_a?(UmlClass) || def_obj.is_a?(DataType)
635
+
636
+ derived = Array(def_obj.derived_fields)
637
+ return if derived.empty?
638
+
639
+ compiled_klass.class_eval do
640
+ @lml_derived_fields = derived.map(&:name).map(&:to_sym)
641
+ def self.derived_fields
642
+ @lml_derived_fields
643
+ end
644
+ end
645
+ derived.each do |field|
646
+ compiled_klass.attribute field.name.to_sym, resolve_type(field.type.to_s)
647
+ end
648
+ end
649
+
650
+ # L5: register grammar-backed string formats. Class-level
651
+ # declarations win; a models-block default applies to classes
652
+ # without their own. Artifact resolution: the compiler's
653
+ # artifacts map (name => path), else the string as a path.
654
+ def register_string_formats(doc)
655
+ defaults = doc.packages.flat_map(&:default_string_formats)
656
+ doc.classes.each do |class_def|
657
+ declared = Array(class_def.string_formats)
658
+ declared.each { |fmt| register_string_format(class_def.name.to_s, fmt) }
659
+
660
+ declared_formats = declared.map(&:format)
661
+ defaults.reject { |fmt| declared_formats.include?(fmt.format) }
662
+ .each { |fmt| register_string_format(class_def.name.to_s, fmt) }
663
+ end
664
+ end
665
+
666
+ def register_string_format(class_name, fmt)
667
+ artifact_path = artifact_path_for(fmt.artifact.to_s)
668
+ return unless artifact_path
669
+
670
+ compiled = @compiled[class_name]
671
+ return unless compiled
672
+
673
+ Parsanol::PARG::Lutaml.register(compiled,
674
+ format_name: fmt.format.to_sym,
675
+ artifact: artifact_path,
676
+ entry: fmt.root.to_s)
677
+ end
678
+
679
+ def artifact_path_for(reference)
680
+ mapped = @artifact_paths && @artifact_paths[reference]
681
+ return mapped if mapped && File.exist?(mapped.to_s)
682
+ return reference if File.exist?(reference)
683
+
684
+ nil
685
+ end
686
+
687
+ # Emit KV mappings carrying render_nil for the declared
688
+ # attributes (yaml/json round-trips honor them)
689
+ def apply_render_nil_mappings(compiled_klass, render_nil_map)
690
+ %i[yaml json].each do |fmt|
691
+ compiled_klass.public_send(fmt) do |mapping|
692
+ render_nil_map.each do |name, flag|
693
+ mapping.map name.to_s, to: name, render_nil: flag
694
+ end
695
+ end
696
+ end
344
697
  end
345
698
 
346
699
  def register(name, klass)
@@ -5,6 +5,9 @@ module Lutaml
5
5
  class DataType < Lutaml::Model::Serializable
6
6
  # From TopElement
7
7
  attribute :name, :string
8
+ attribute :wire_mappings, "Lutaml::Lml::Mapping", collection: true, default: -> { [] }
9
+ attribute :derived_fields, "Lutaml::Lml::DerivedField", collection: true, default: -> { [] }
10
+ attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
8
11
  attribute :definition, Lutaml::Lml::Types::TextType
9
12
  attribute :keyword, :string, default: "dataType"
10
13
  attribute :stereotype, :string, collection: true, default: -> { [] }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ # A derived-field declaration: existence + type only. The
6
+ # computation is bound by the binder from the grammar artifact's
7
+ # derive specs — LML never defines computation.
8
+ # derive urn: String
9
+ class DerivedField < Lutaml::Model::Serializable
10
+ attribute :name, :string
11
+ attribute :type, :string
12
+ end
13
+ end
14
+ end
@@ -16,6 +16,8 @@ module Lutaml
16
16
  attribute :data_types, "Lutaml::Lml::DataType", collection: true, default: -> { [] }
17
17
  attribute :enums, "Lutaml::Lml::Enum", collection: true, default: -> { [] }
18
18
  attribute :packages, "Lutaml::Lml::Package", collection: true, default: -> { [] }
19
+ attribute :namespaces, "Lutaml::Lml::Namespace", collection: true, default: -> { [] }
20
+ attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
19
21
  attribute :primitives, "Lutaml::Lml::PrimitiveType", collection: true, default: -> { [] }
20
22
  attribute :associations, "Lutaml::Lml::Association", collection: true, default: -> { [] }
21
23
  attribute :diagrams, "Lutaml::Lml::Diagram", collection: true, default: -> { [] }
@@ -17,6 +17,9 @@ module Lutaml
17
17
  attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true,
18
18
  default: -> { [] }
19
19
  attribute :modifier, :string
20
+ attribute :member_fields, "Lutaml::Lml::MemberField", collection: true, default: -> { [] }
21
+ attribute :from_table_name, :string
22
+ attribute :from_table_artifact, :string
20
23
  attribute :operations, "Lutaml::Lml::Operation", collection: true, default: -> { [] }
21
24
  attribute :values, "Lutaml::Lml::Value", collection: true, default: -> { [] }
22
25
 
@@ -3,7 +3,9 @@
3
3
  module Lutaml
4
4
  module Lml
5
5
  class Instance < Lutaml::Model::Serializable
6
+ attribute :name, :string
6
7
  attribute :type, :string
8
+ attribute :isa, :string
7
9
  attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true
8
10
  attribute :instance, "Lutaml::Lml::Instance"
9
11
  attribute :template, "Lutaml::Lml::TopElementAttribute", collection: true
@@ -3,6 +3,7 @@
3
3
  module Lutaml
4
4
  module Lml
5
5
  class InstanceCollection < Lutaml::Model::Serializable
6
+ attribute :name, :string
6
7
  attribute :instances, "Lutaml::Lml::Instance", collection: true, default: []
7
8
  attribute :imports, "Lutaml::Lml::InstancesImport", collection: true, default: []
8
9
  attribute :exports, "Lutaml::Lml::InstancesExport", collection: true, default: []