lutaml-lml 0.1.8 → 0.2.1

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/lutaml/lml/cli.rb +20 -4
  3. data/lib/lutaml/lml/data_processor/attribute_processing.rb +49 -9
  4. data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +62 -0
  5. data/lib/lutaml/lml/data_processor.rb +3 -0
  6. data/lib/lutaml/lml/document_builder.rb +230 -6
  7. data/lib/lutaml/lml/format/adapter/standard_adapter.rb +17 -4
  8. data/lib/lutaml/lml/formatter/elk.rb +42 -0
  9. data/lib/lutaml/lml/formatter/graphviz/node_formatter.rb +2 -1
  10. data/lib/lutaml/lml/formatter.rb +1 -0
  11. data/lib/lutaml/lml/grammar/lml.artifact.json +8659 -3193
  12. data/lib/lutaml/lml/grammar/lml.parg +236 -90
  13. data/lib/lutaml/lml/layout/elk_engine.rb +15 -0
  14. data/lib/lutaml/lml/layout/elk_graph_builder.rb +129 -0
  15. data/lib/lutaml/lml/layout/elk_svg_markers.rb +45 -0
  16. data/lib/lutaml/lml/layout/elk_svg_renderer.rb +119 -0
  17. data/lib/lutaml/lml/layout/graph_viz_engine.rb +1 -1
  18. data/lib/lutaml/lml/layout.rb +4 -0
  19. data/lib/lutaml/lml/model_compiler.rb +231 -19
  20. data/lib/lutaml/lml/models/data_type.rb +1 -0
  21. data/lib/lutaml/lml/models/document.rb +31 -0
  22. data/lib/lutaml/lml/models/enum.rb +2 -0
  23. data/lib/lutaml/lml/models/instance.rb +1 -0
  24. data/lib/lutaml/lml/models/instance_collection.rb +1 -0
  25. data/lib/lutaml/lml/models/mapping_rule.rb +16 -0
  26. data/lib/lutaml/lml/models/namespace.rb +14 -0
  27. data/lib/lutaml/lml/models/operation_parameter.rb +1 -0
  28. data/lib/lutaml/lml/models/reference.rb +23 -0
  29. data/lib/lutaml/lml/models/serialization_mapping.rb +17 -0
  30. data/lib/lutaml/lml/models/top_element_attribute.rb +16 -0
  31. data/lib/lutaml/lml/models/uml_class.rb +2 -1
  32. data/lib/lutaml/lml/preprocessor.rb +49 -8
  33. data/lib/lutaml/lml/transform.rb +1 -1
  34. data/lib/lutaml/lml/validator.rb +344 -0
  35. data/lib/lutaml/lml/version.rb +1 -1
  36. data/lib/lutaml/lml.rb +5 -0
  37. metadata +15 -4
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Layout
5
+ # Renders a laid-out Elkrb::Graph (nodes positioned by the layout) as
6
+ # standalone SVG: rounded-rect nodes with text-line labels and
7
+ # polyline edges with per-type arrow markers.
8
+ class ElkSvgRenderer
9
+ include ElkSvgMarkers
10
+
11
+ EDGE_STROKE = ElkSvgMarkers::EDGE_STROKE
12
+
13
+ HEADER = <<~XML
14
+ <?xml version="1.0" encoding="UTF-8"?>
15
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%<width>s" height="%<height>s" viewBox="0 0 %<width>s %<height>s">
16
+ XML
17
+ NODE_FILL = '#f8f8f8'
18
+ NODE_STROKE = '#333333'
19
+
20
+ def render(graph)
21
+ @graph = graph
22
+ @out = +''
23
+ @out << format(HEADER, width: bounds_width, height: bounds_height)
24
+ @out << "<defs>\n#{defs}\n</defs>\n"
25
+ nodes
26
+ edges
27
+ @out << "</svg>\n"
28
+ @out
29
+ end
30
+
31
+ private
32
+
33
+ def nodes
34
+ @graph.children.each { |node| render_node(node) }
35
+ end
36
+
37
+ def render_node(node)
38
+ @out << %(<g transform="translate(#{node.x.to_i},#{node.y.to_i})">)
39
+ @out << node_rect(node)
40
+ node.labels.each_with_index { |label, i| @out << node_label(label, i) }
41
+ @out << '</g>'
42
+ end
43
+
44
+ def node_rect(node)
45
+ %(<rect width="#{node.width.to_i}" height="#{node.height.to_i}" rx="6" ) +
46
+ %(fill="#{NODE_FILL}" stroke="#{NODE_STROKE}"/>)
47
+ end
48
+
49
+ def node_label(label, index)
50
+ y = ((index + 0.7) * ElkGraphBuilder::LINE_HEIGHT).round(1)
51
+ weight = index.zero? ? ' font-weight="bold"' : ''
52
+ %(<text x="7" y="#{y}" font-family="Helvetica" font-size="12"#{weight}>#{escape(label.text)}</text>)
53
+ end
54
+
55
+ def edges
56
+ @graph.edges.each { |edge| render_edge(edge) }
57
+ end
58
+
59
+ def render_edge(edge)
60
+ points = edge_points(edge)
61
+ return if points.empty?
62
+
63
+ @out << edge_polyline(edge, points)
64
+ edge_label(edge, points)
65
+ end
66
+
67
+ def edge_polyline(edge, points)
68
+ pts = points.map { |p| "#{p.x.round(1)},#{p.y.round(1)}" }.join(' ')
69
+ %(<polyline fill="none" stroke="#{EDGE_STROKE}" marker-end="url(##{arrow_marker(edge)})" points="#{pts}"/>)
70
+ end
71
+
72
+ def edge_label(edge, points)
73
+ label = Array(edge.labels).first
74
+ return unless label&.text
75
+
76
+ @out << edge_label_text(label, edge_anchor(points))
77
+ end
78
+
79
+ def edge_anchor(points)
80
+ mid = points[points.size / 2]
81
+ { x: (mid.x + 4).round(1), y: (mid.y - 4).round(1) }
82
+ end
83
+
84
+ def edge_label_text(label, anchor)
85
+ %(<text x="#{anchor[:x]}" y="#{anchor[:y]}" font-family="Helvetica" ) +
86
+ %(font-size="10" fill="#{EDGE_STROKE}">#{escape(label.text)}</text>)
87
+ end
88
+
89
+ def edge_points(edge)
90
+ sections = Array(edge.sections)
91
+ return [] if sections.empty?
92
+
93
+ points = [sections.first.start_point]
94
+ sections.each do |section|
95
+ points.concat(Array(section.bend_points))
96
+ points << section.end_point
97
+ end
98
+ points.compact
99
+ end
100
+
101
+ def bounds_width
102
+ extent(:x, :width) + 20
103
+ end
104
+
105
+ def bounds_height
106
+ extent(:y, :height) + 20
107
+ end
108
+
109
+ def extent(pos, size)
110
+ values = @graph.children.map { |n| n.public_send(pos).to_f + n.public_send(size).to_f }
111
+ [values.max || 0, 0].max.round
112
+ end
113
+
114
+ def escape(text)
115
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
116
+ end
117
+ end
118
+ end
119
+ end
@@ -7,7 +7,7 @@ module Lutaml
7
7
  module Layout
8
8
  class GraphVizEngine < Engine
9
9
  def render(type)
10
- Open3.popen3("dot -T#{type}") do |stdin, stdout, _stderr, _wait|
10
+ Open3.popen3("dot", "-T#{type}") do |stdin, stdout, _stderr, _wait|
11
11
  stdout.binmode
12
12
  stdin.puts(input)
13
13
  stdin.close
@@ -4,5 +4,9 @@ module Lutaml
4
4
  module Layout
5
5
  autoload :Engine, "lutaml/lml/layout/engine"
6
6
  autoload :GraphVizEngine, "lutaml/lml/layout/graph_viz_engine"
7
+ autoload :ElkEngine, "lutaml/lml/layout/elk_engine"
8
+ autoload :ElkGraphBuilder, "lutaml/lml/layout/elk_graph_builder"
9
+ autoload :ElkSvgRenderer, "lutaml/lml/layout/elk_svg_renderer"
10
+ autoload :ElkSvgMarkers, "lutaml/lml/layout/elk_svg_markers"
7
11
  end
8
12
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Lutaml
4
6
  module Lml
5
7
  class ModelCompiler
@@ -7,9 +9,13 @@ module Lutaml
7
9
 
8
10
  TYPE_MAP = {
9
11
  'String' => :string,
12
+ 'string' => :string,
10
13
  'Integer' => :integer,
14
+ 'integer' => :integer,
11
15
  'Boolean' => :boolean,
16
+ 'boolean' => :boolean,
12
17
  'Float' => :float,
18
+ 'float' => :float,
13
19
  'Date' => :date,
14
20
  'date_time' => :date_time,
15
21
  'DateTime' => :date_time,
@@ -21,13 +27,17 @@ module Lutaml
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, artifact_paths: {})
30
+ def initialize(namespace: nil, artifact_paths: {}, default_artifact: nil)
25
31
  @namespace = namespace
26
32
  @artifact_paths = artifact_paths
33
+ @default_artifact = default_artifact
27
34
  @compiled = {}
28
35
  @forward_refs = {}
29
36
  @enum_names = Set.new
30
37
  @enum_classes = {}
38
+ @declared_namespaces = []
39
+ @serialization_mappings = []
40
+ @namespace_classes = {}
31
41
  end
32
42
 
33
43
  def compile(input)
@@ -135,6 +145,20 @@ module Lutaml
135
145
  klass.new(**attrs)
136
146
  end
137
147
 
148
+ # An inline map's cargo is {key_value_map: [{key:, value:}, ...]} with
149
+ # literal-shaped keys/values ({string: ..}, {number: ..}, {boolean: ..});
150
+ # materialize it as a plain Hash (issue #17: maps hydrate as data).
151
+ def hydrate_map_cargo(cargo)
152
+ cargo[:key_value_map].to_h do |pair|
153
+ [hydrate_scalar(pair[:key]).to_s, hydrate_scalar(pair[:value])]
154
+ end
155
+ end
156
+
157
+ def hydrate_scalar(literal)
158
+ literal = literal.value if literal.respond_to?(:value)
159
+ literal.is_a?(Hash) ? literal.values.first : literal
160
+ end
161
+
138
162
  def resolve_instance_type(instance)
139
163
  # L6: the explicit isa keyword is the type override; a `type`
140
164
  # attribute is ordinary data (pubid's identifier-kind field)
@@ -168,7 +192,14 @@ module Lutaml
168
192
  end
169
193
 
170
194
  def resolve_instance_value(value, nested, attr_def = nil)
195
+ value = value.value if value.respond_to?(:value)
196
+ return hydrate_map_cargo(value) if value.is_a?(Hash) && value.key?(:key_value_map)
197
+
171
198
  if nested.any?
199
+ # A scalar attribute with a single nested instance hydrates to
200
+ # the object itself; the declaration is the truth (issue #17).
201
+ return hydrate_instance(nested.first) if !attr_def&.collection && nested.one?
202
+
172
203
  nested.map { |i| hydrate_instance(i) }
173
204
  elsif value.is_a?(Array)
174
205
  value
@@ -183,14 +214,30 @@ module Lutaml
183
214
  end
184
215
 
185
216
  def compile_document(doc)
217
+ @declared_namespaces = Array(doc.namespaces)
218
+ @serialization_mappings = Array(doc.serialization_mappings)
186
219
  doc.classes.each { |c| compile_class(c) }
220
+ hoist_class_serialization_mappings(doc)
187
221
  doc.enums.each { |e| compile_enum(e) }
188
222
  doc.data_types.each { |dt| compile_data_type(dt) }
189
223
  resolve_forward_references
190
224
  apply_xml_mappings
225
+ apply_key_value_mappings
191
226
  register_string_formats(doc)
192
227
  end
193
228
 
229
+ # In-class `mapping <format> { … }` is the shortcut for a sibling
230
+ # mapping targeting the enclosing class; hoist them so emission is
231
+ # uniform.
232
+ def hoist_class_serialization_mappings(doc)
233
+ (Array(doc.classes) + Array(doc.data_types)).each do |klass_def|
234
+ Array(klass_def.serialization_mappings).each do |mapping|
235
+ mapping.target = klass_def.name.to_s
236
+ @serialization_mappings << mapping
237
+ end
238
+ end
239
+ end
240
+
194
241
  def resolve_forward_references
195
242
  @forward_refs.each do |class_name, deferred_attrs|
196
243
  klass = @compiled[class_name]
@@ -232,6 +279,7 @@ module Lutaml
232
279
 
233
280
  def compile_enum(enum_def)
234
281
  name = enum_def.name.to_s
282
+ expand_from_table(enum_def)
235
283
  values = extract_enum_values(enum_def)
236
284
 
237
285
  payload_fields = Array(enum_def.member_fields).map(&:name).map(&:to_s)
@@ -246,7 +294,8 @@ module Lutaml
246
294
  end
247
295
 
248
296
  values.each do |value|
249
- compiled_klass.define_singleton_method(value.name) do
297
+ accessor = value.name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
298
+ compiled_klass.define_singleton_method(accessor) do
250
299
  new({ value: value.name, description: value.description }.merge(value.payload || {}))
251
300
  end
252
301
  end
@@ -297,6 +346,9 @@ module Lutaml
297
346
  @compiled.each do |name, klass|
298
347
  next if @enum_names.include?(name)
299
348
 
349
+ custom = serialization_mapping_for(name, 'xml')
350
+ next apply_custom_xml_mapping(klass, custom) if custom
351
+
300
352
  apply_xml_mapping(klass, name)
301
353
  end
302
354
  end
@@ -306,23 +358,133 @@ module Lutaml
306
358
  return if attr_names.empty?
307
359
 
308
360
  klass.xml do |mapping|
309
- mapping.root(root_name)
361
+ mapping.element(root_name)
310
362
  attr_names.each do |attr_name|
311
363
  mapping.map_element(attr_name.to_s, to: attr_name)
312
364
  end
313
365
  end
314
366
  end
315
367
 
368
+ # RS 3010 extension: key-value mapping blocks for the declared
369
+ # formats (yaml/json/toml share lutaml-model's key_value model).
370
+ def apply_key_value_mappings
371
+ @compiled.each do |name, klass|
372
+ next if @enum_names.include?(name)
373
+
374
+ custom = serialization_mapping_for(name, 'key_value')
375
+ apply_custom_key_value_mapping(klass, custom) if custom
376
+ end
377
+ end
378
+
379
+ def serialization_mapping_for(target, format)
380
+ @serialization_mappings.find do |m|
381
+ m.target.to_s == target && serialization_format(m.format) == format
382
+ end
383
+ end
384
+
385
+ def serialization_format(format)
386
+ %w[yaml json toml].include?(format.to_s) ? 'key_value' : format.to_s
387
+ end
388
+
389
+ def apply_custom_xml_mapping(klass, mapping)
390
+ check_namespace_ambiguity(mapping)
391
+ ns_class = namespace_class_for(mapping.namespace_ref) if mapping.namespace_ref
392
+ rule_ns = mapping.rules.select { |r| r.kind == 'namespace' }
393
+ .to_h { |r| [r.field, namespace_class_for(r.field)] }
394
+ klass.xml do |m|
395
+ m.element(mapping.element_name) if mapping.element_name
396
+ m.namespace(ns_class) if ns_class
397
+ m.mixed_content if mapping.mixed_content
398
+ mapping.rules.each do |rule|
399
+ case rule.kind
400
+ when 'element' then m.map_element(rule.wire, to: rule.field.to_sym)
401
+ when 'attribute' then m.map_attribute(rule.wire, to: rule.field.to_sym)
402
+ when 'content' then m.map_content(to: rule.field.to_sym)
403
+ when 'namespace' then m.namespace(rule_ns[rule.field])
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ def apply_custom_key_value_mapping(klass, mapping)
410
+ formats = mapping.format.to_s == 'key_value' ? %w[yaml json toml] : [mapping.format.to_s]
411
+ klass.instance_eval do
412
+ formats.each do |format|
413
+ public_send(format) do |m|
414
+ mapping.rules.each do |rule|
415
+ case rule.kind
416
+ when 'map' then m.map(rule.wire, to: rule.field.to_sym)
417
+ when 'key' then m.map_key(to_instance: rule.field.to_sym)
418
+ when 'value'
419
+ if rule.mode == 'as_attribute'
420
+ m.map_value(as_attribute: rule.field.to_sym)
421
+ else
422
+ m.map_value(to_instance: rule.field.to_sym)
423
+ end
424
+ end
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end
430
+
431
+ def check_namespace_ambiguity(mapping)
432
+ return unless mapping.format.to_s == 'xml'
433
+ return if mapping.namespace_ref || @declared_namespaces.length < 2
434
+
435
+ raise ArgumentError,
436
+ "mapping #{mapping.format} #{mapping.target}: ambiguous default " \
437
+ "namespace — #{@declared_namespaces.length} namespaces in scope " \
438
+ "(#{@declared_namespaces.map(&:name).join(', ')}); declare one with " \
439
+ "'namespace <name>'"
440
+ end
441
+
442
+ def namespace_class_for(ref)
443
+ return nil if ref.nil?
444
+
445
+ require 'lutaml/xml/namespace' unless defined?(Lutaml::Xml::Namespace)
446
+
447
+ ns_class = @namespace_classes[ref]
448
+ return ns_class if ns_class
449
+
450
+ declared = @declared_namespaces.find { |ns| ns.name.to_s == ref }
451
+ raise ArgumentError, "unknown namespace '#{ref}'" unless declared
452
+
453
+ ns_class = Class.new(Lutaml::Xml::Namespace)
454
+ uri = declared.uri
455
+ prefix = declared.prefix.to_s
456
+ ns_class.define_singleton_method(:uri) { uri }
457
+ ns_class.define_singleton_method(:prefix_default) { prefix }
458
+ @namespace_classes[ref] = ns_class
459
+ end
460
+
316
461
  def build_attributes(klass_def)
317
462
  Array(klass_def.attributes).map do |attr|
318
463
  attr_name = attr.name.to_sym
319
- raw_type = attr.type.to_s
464
+ raw_type = resolve_values_type(klass_def, attr)
320
465
  type = resolve_type(raw_type)
321
466
  options = build_options(attr)
322
467
  [attr_name, raw_type, type, options]
323
468
  end
324
469
  end
325
470
 
471
+ # RS 3001 §Attribute: `values { v1, v2 }` expands into an anonymous
472
+ # enum baked from the inline set; the attribute is typed with it.
473
+ def resolve_values_type(klass_def, attr)
474
+ return attr.type.to_s unless attr.value_set&.any?
475
+
476
+ enum_name = anonymous_enum_name(klass_def.name, attr.name)
477
+ members = attr.value_set.map { |v| ::Lutaml::Lml::TopElementAttribute.new(name: v.to_s) }
478
+ compile_enum(::Lutaml::Lml::Enum.new(name: enum_name, attributes: members))
479
+ enum_name
480
+ end
481
+
482
+ def anonymous_enum_name(class_name, attr_name)
483
+ base = class_name.to_s.split('::').last
484
+ attr_part = attr_name.to_s.split('_').map(&:capitalize).join
485
+ "#{base}#{attr_part}Values"
486
+ end
487
+
326
488
  def resolve_type(type_name)
327
489
  if TYPE_MAP.key?(type_name)
328
490
  TYPE_MAP[type_name]
@@ -391,18 +553,74 @@ module Lutaml
391
553
  raise ValidationError, "Unrecognized cardinality token: #{val.inspect}"
392
554
  end
393
555
 
556
+ # L7a point 3: from_table members are baked at compile time — the
557
+ # artifact table's rows become the values, validated against the
558
+ # declared member_fields (unknown field = error; missing = nil).
559
+ # The member name comes from the row's "name" column.
560
+ def expand_from_table(enum_def)
561
+ return unless enum_def.from_table_name
562
+
563
+ rows = artifact_table_rows(enum_def)
564
+ known = Array(enum_def.member_fields).map { |f| f.name.to_s }
565
+ rows.each do |row|
566
+ row = row.transform_keys(&:to_s)
567
+ member_name = row.fetch('name')
568
+ unknown = row.keys - known - ['name']
569
+ if unknown.any?
570
+ raise ArgumentError,
571
+ "enum #{enum_def.name}: table row #{member_name.inspect} has " \
572
+ "undeclared fields #{unknown.inspect} (declared: #{known.inspect})"
573
+ end
574
+ payload = row.slice(*known).compact
575
+ enum_def.attributes << TopElementAttribute.new(
576
+ name: member_name,
577
+ properties: payload.map do |field, value|
578
+ TopElementAttribute.new(name: field, value: value)
579
+ end
580
+ )
581
+ end
582
+ end
583
+
584
+ def artifact_table_rows(enum_def)
585
+ reference = enum_def.from_table_artifact || @default_artifact
586
+ path = artifact_path_for(reference.to_s)
587
+ unless path
588
+ raise ArgumentError,
589
+ "enum #{enum_def.name}: from_table artifact #{reference.inspect} not found"
590
+ end
591
+
592
+ envelope = JSON.parse(File.read(path))
593
+ table = envelope['tables'].to_h[enum_def.from_table_name]
594
+ unless table
595
+ raise ArgumentError,
596
+ "enum #{enum_def.name}: table #{enum_def.from_table_name.inspect} " \
597
+ "not found in artifact #{reference.inspect}"
598
+ end
599
+
600
+ table['rows'].to_a
601
+ end
602
+
394
603
  def extract_enum_values(enum_def)
395
604
  member_fields = Array(enum_def.member_fields).map do |f|
396
605
  [f.name.to_s, f.type.to_s]
397
606
  end
398
607
  enum_def.attributes.map do |attr|
399
- description = Array(attr.attributes).find do |nested|
400
- nested.name == 'description'
401
- end
608
+ description = member_description(attr)
402
609
  payload = extract_member_payload(attr)
403
610
  validate_member_payload(enum_def, attr, payload, member_fields)
404
- EnumValue.new(attr.name.to_s, description&.type, payload)
611
+ EnumValue.new(attr.name.to_s, description, payload)
612
+ end
613
+ end
614
+
615
+ # The member description rides a nested attribute named `description`
616
+ # (bare-member form) or `definition` (RS 3001 `value` blocks); both
617
+ # spellings are accepted.
618
+ def member_description(attr)
619
+ nested = Array(attr.attributes).find do |n|
620
+ %w[description definition].include?(n.name.to_s)
405
621
  end
622
+ text = nested&.type || nested&.definition || nested&.value
623
+ text.is_a?(Hash) && text.key?(:string) ? text[:string] : text
406
624
  end
407
625
 
408
626
  # L7a: per-member payload lines ride the keyword-attribute
@@ -457,18 +675,12 @@ module Lutaml
457
675
  def register_string_formats(doc)
458
676
  defaults = doc.packages.flat_map(&:default_string_formats)
459
677
  doc.classes.each do |class_def|
460
- Array(class_def.string_formats).each do |fmt|
461
- register_string_format(class_def.name.to_s, fmt)
462
- end
463
- next if Array(class_def.string_formats).any? do |f|
464
- f.format == defaults.first&.format
465
- end
678
+ declared = Array(class_def.string_formats)
679
+ declared.each { |fmt| register_string_format(class_def.name.to_s, fmt) }
466
680
 
467
- defaults.each do |fmt|
468
- next if Array(class_def.string_formats).map(&:format).include?(fmt.format)
469
-
470
- register_string_format(class_def.name.to_s, fmt)
471
- end
681
+ declared_formats = declared.map(&:format)
682
+ defaults.reject { |fmt| declared_formats.include?(fmt.format) }
683
+ .each { |fmt| register_string_format(class_def.name.to_s, fmt) }
472
684
  end
473
685
  end
474
686
 
@@ -7,6 +7,7 @@ module Lutaml
7
7
  attribute :name, :string
8
8
  attribute :wire_mappings, "Lutaml::Lml::Mapping", collection: true, default: -> { [] }
9
9
  attribute :derived_fields, "Lutaml::Lml::DerivedField", collection: true, default: -> { [] }
10
+ attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
10
11
  attribute :definition, Lutaml::Lml::Types::TextType
11
12
  attribute :keyword, :string, default: "dataType"
12
13
  attribute :stereotype, :string, collection: true, default: -> { [] }
@@ -16,6 +16,11 @@ 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
+ # `models <Name> version "x.y.z"` headers: model name -> declared
21
+ # version (issue #16, the co-release identity line).
22
+ attribute :model_versions, :hash, default: -> { {} }
23
+ attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
19
24
  attribute :primitives, "Lutaml::Lml::PrimitiveType", collection: true, default: -> { [] }
20
25
  attribute :associations, "Lutaml::Lml::Association", collection: true, default: -> { [] }
21
26
  attribute :diagrams, "Lutaml::Lml::Diagram", collection: true, default: -> { [] }
@@ -25,6 +30,32 @@ module Lutaml
25
30
  attribute :instance, "Lutaml::Lml::Instance"
26
31
  attribute :requires, :string, collection: true, default: -> { [] }
27
32
  attribute :instances, "Lutaml::Lml::InstanceCollection"
33
+
34
+ # Repeated `instances` blocks accumulate (issue #17): instances,
35
+ # imports, exports and collections concat; a duplicate collection
36
+ # name is an error (same name is a mistake, distinct names intent).
37
+ def instances=(collection)
38
+ collection = Lutaml::Lml::InstanceCollection.new(**collection) if collection.is_a?(Hash)
39
+ if @instances.nil? || (@instances.instances.empty? && @instances.imports.empty? &&
40
+ @instances.exports.empty? && @instances.collections.empty?)
41
+ @instances = collection
42
+ else
43
+ merge_instances(collection)
44
+ end
45
+ end
46
+
47
+ def merge_instances(incoming)
48
+ names = @instances.collections.map(&:name)
49
+ incoming.collections.each do |collection|
50
+ if names.include?(collection.name)
51
+ raise Lutaml::Lml::Error, "duplicate collection name '#{collection.name}'"
52
+ end
53
+ end
54
+ @instances.instances.concat(incoming.instances)
55
+ @instances.imports.concat(incoming.imports)
56
+ @instances.exports.concat(incoming.exports)
57
+ @instances.collections.concat(incoming.collections)
58
+ end
28
59
  attribute :view_imports, "Lutaml::Lml::ViewImport", collection: true, default: -> { [] }
29
60
  attribute :show_filter, "Lutaml::Lml::ViewFilter"
30
61
  attribute :hide_filter, "Lutaml::Lml::ViewFilter"
@@ -18,6 +18,8 @@ module Lutaml
18
18
  default: -> { [] }
19
19
  attribute :modifier, :string
20
20
  attribute :member_fields, "Lutaml::Lml::MemberField", collection: true, default: -> { [] }
21
+ attribute :from_table_name, :string
22
+ attribute :from_table_artifact, :string
21
23
  attribute :operations, "Lutaml::Lml::Operation", collection: true, default: -> { [] }
22
24
  attribute :values, "Lutaml::Lml::Value", collection: true, default: -> { [] }
23
25
 
@@ -3,6 +3,7 @@
3
3
  module Lutaml
4
4
  module Lml
5
5
  class Instance < Lutaml::Model::Serializable
6
+ attribute :name, :string
6
7
  attribute :type, :string
7
8
  attribute :isa, :string
8
9
  attribute :attributes, "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: []
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ # One line of a serialization mapping: what kind of mapping (element,
6
+ # attribute, content, map, key, value), the serialization-side wire
7
+ # name, the model attribute it binds, and the qualifier mode for
8
+ # key/value collections (to_instance, as_attribute).
9
+ class MappingRule < Lutaml::Model::Serializable
10
+ attribute :kind, :string
11
+ attribute :wire, :string
12
+ attribute :field, :string
13
+ attribute :mode, :string
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ # RS 3010 extension: a declared XML namespace (uri + optional prefix).
6
+ # The compiler generates the corresponding Lutaml::Model::XmlNamespace
7
+ # subclass for the compiled model namespace.
8
+ class Namespace < Lutaml::Model::Serializable
9
+ attribute :name, :string
10
+ attribute :uri, :string
11
+ attribute :prefix, :string
12
+ end
13
+ end
14
+ end
@@ -6,6 +6,7 @@ module Lutaml
6
6
  attribute :name, :string
7
7
  attribute :type, :string
8
8
  attribute :direction, :string, default: "in"
9
+ attribute :default, :string
9
10
  end
10
11
  end
11
12
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ # RS 3001 par. Value type: a reference to another instance
6
+ # (`reference:(path)` / `ref:(path)`). The attribute's value slot keeps
7
+ # its primitive wire shape (a `reference` hash); this is the typed
8
+ # domain view of it, built at one canonical point
9
+ # (TopElementAttribute#reference) so validation and serialization
10
+ # consume structured references instead of re-sniffing hashes.
11
+ class Reference < Lutaml::Model::Serializable
12
+ attribute :path, :string
13
+
14
+ def segments
15
+ @segments ||= path.to_s.split('.')
16
+ end
17
+
18
+ def to_s
19
+ path.to_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ # RS 3010 extension: a serialization mapping declared separately from
6
+ # its class and linked by target name —
7
+ # mapping xml Ceramic { element "ceramic" … }
8
+ class SerializationMapping < Lutaml::Model::Serializable
9
+ attribute :format, :string
10
+ attribute :target, :string
11
+ attribute :element_name, :string
12
+ attribute :namespace_ref, :string
13
+ attribute :mixed_content, :boolean, default: false
14
+ attribute :rules, 'Lutaml::Lml::MappingRule', collection: true, default: -> { [] }
15
+ end
16
+ end
17
+ end