lutaml-lml 0.1.8 → 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.
@@ -38,7 +38,7 @@ grammar Lml version "1.0.0" {
38
38
  boolean = ( "true" / "false" ) as boolean
39
39
  number = ( 1*%x30-39 "." 1*%x30-39 ) as float / ( 1*%x30-39 ) as number
40
40
  variable = quoted_string / 1*word
41
- reference = "reference:(" ( variable *( "." variable ) ) as reference ")"
41
+ reference = ( "reference:(" / "ref:(" ) ( variable *( "." variable ) ) as reference ")"
42
42
  range = ( variable as start ".." variable as end ) as range
43
43
  namespaced_identifier = variable *( "::" variable )
44
44
  comment_definition = [ ws ] ( "**" / "#" ) ( *( !newline any_char ) ) as comments
@@ -49,8 +49,8 @@ grammar Lml version "1.0.0" {
49
49
  cardinality_body_definition = cardchar as min [ ".." [ cardchar as max ] ]
50
50
  cardinality = "[" cardinality_body_definition as cardinality "]"
51
51
 
52
- value = boolean / reference / range / number / quoted_string
53
- key_value_pair = variable as key ws [ "=" ] [ ws ] value as value
52
+ data_value = boolean / reference / range / number / quoted_string
53
+ key_value_pair = variable as key ws [ "=" ] [ ws ] data_value as value
54
54
  key_value_map = "{" [ whitespace ] ( *( key_value_pair whitespace ) ) as key_value_map "}"
55
55
  kw_visibility_modifier = "+" / "-" / "#" / "~"
56
56
 
@@ -69,8 +69,11 @@ grammar Lml version "1.0.0" {
69
69
 
70
70
  keyword_type_argument = "type" [ ws ] ( 1*nonspace ) as type [ whitespace ]
71
71
  keyword_cardinality_argument = "cardinality" [ ws ] cardinality_body_definition as cardinality [ whitespace ]
72
- keyword_any_argument = [ ws ] ( 1*nonspace ) as name ws [ "=" ] [ ws ] attribute_value as value [ whitespace ]
73
- keyword_attribute_options = *( keyword_type_argument / keyword_cardinality_argument / ( keyword_any_argument ) as properties )
72
+ keyword_any_argument = [ ws ] ( 1*nonspace ) as name ws [ "=" / ":" ] [ ws ] attribute_value as value [ whitespace ]
73
+ keyword_values_argument = "values" ws ( ( value_set_body ) as value_set / ( variable ) as value_set_ref ) [ whitespace ]
74
+ keyword_pattern_argument = ( "pattern" ws "/" ( *( ( "\\" any_char ) / ( !"/" any_char ) ) ) as pattern "/" [ whitespace ] )
75
+ value_set_body = "{" [ whitespace ] ( *( ( ( data_value / variable ) as item ) [ "," ] [ whitespace ] ) ) as items "}"
76
+ keyword_attribute_options = *( keyword_type_argument / keyword_cardinality_argument / keyword_values_argument / keyword_pattern_argument / ( keyword_any_argument ) as properties )
74
77
  keyword_attribute_body = "{" [ whitespace ] keyword_attribute_options [ whitespace ] "}"
75
78
  keyword_attribute_definition = ( "attribute" ws attribute_name [ ws ] keyword_attribute_body ) as attributes
76
79
 
@@ -89,18 +92,43 @@ grammar Lml version "1.0.0" {
89
92
 
90
93
  # ---- Definitions ----
91
94
  class_modifier = [ ( [ whitespace ] ( "abstract" / "interface" ) ) as modifier ws ]
95
+ # ---- Namespaces (RS 3010 extension: lutaml-model XmlNamespace) ----
96
+ namespace_uri_line = ( [ ws ] "uri" ws ( quoted_string / ( 1*word ) ) as uri [ whitespace ] )
97
+ namespace_prefix_line = ( [ ws ] "prefix" ws ( quoted_string / ( 1*word ) ) as prefix [ whitespace ] )
98
+ namespace_body = "{" [ whitespace ] ( *( namespace_uri_line / namespace_prefix_line ) ) as members "}"
99
+ namespace_inline = ( ws ( quoted_string ) as uri [ ws "prefix" ws ( quoted_string / ( 1*word ) ) as prefix ] )
100
+ namespace_definition = ( [ whitespace ] "namespace" ws ( 1*word ) as name [ ws ] ( namespace_body / namespace_inline ) [ whitespace ] ) as namespaces
101
+
102
+ # ---- Serialization mappings (RS 3010 extension: sibling-linked) ----
103
+ serialization_body = "{" [ whitespace ] ( *( serialization_line ) ) as rules "}"
104
+ serialization_line = ( mapping_element_name_line / mapping_element_line / mapping_attribute_line / mapping_content_line / mapping_namespace_line / mapping_mixed_line / legacy_map_line / mapping_map_line / mapping_map_key_line / mapping_map_value_line ) [ whitespace ]
105
+ legacy_map_line = ( [ ws ] "map" ws dq_string as wire "," ws "to:" ws ( quoted_string / ( 1*word ) ) as to [ whitespace ] ) as legacy_map
106
+ mapping_element_name_line = ( [ ws ] "element" ws dq_string as element_name [ whitespace ] ) as element_name
107
+ mapping_element_line = ( [ ws ] "map_element" ws ( element_args ) ) as map_element
108
+ element_args = ( dq_string as wire [ ws ( 1*word ) as field ] / ( 1*word ) as field )
109
+ mapping_attribute_line = ( [ ws ] "map_attribute" ws ( attribute_args ) ) as map_attribute
110
+ attribute_args = ( dq_string as wire [ ws ( 1*word ) as field ] / ( 1*word ) as field )
111
+ mapping_content_line = ( [ ws ] "map_content" ws ( 1*word ) as field ) as map_content
112
+ mapping_namespace_line = ( [ ws ] "namespace" ws ( 1*word ) as namespace_ref ) as map_namespace
113
+ mapping_mixed_line = ( [ ws ] "mixed_content" ) as mixed_content
114
+ mapping_map_line = ( [ ws ] "map" ws map_args ) as map
115
+ map_args = ( dq_string as wire ws ( 1*word ) as field / dq_string as wire / ( 1*word ) as field )
116
+ mapping_map_key_line = ( [ ws ] "map_key" ws "to_instance" ws ( 1*word ) as field ) as map_key
117
+ mapping_map_value_line = ( [ ws ] "map_value" ws ( "to_instance" / "as_attribute" ) as mode ws ( 1*word ) as field ) as map_value
118
+ mapping_definition = ( [ whitespace ] "mapping" ws ( 1*word ) as format ws ( 1*word ) as target [ ws ] serialization_body [ whitespace ] ) as serialization_mappings
119
+
92
120
  # ---- Mapping blocks (lutaml-model wire names) ----
93
121
  map_name = ( quoted_string / 1*( word / "-" / "." ) )
94
122
  map_line = [ ws ] "map" ws ( map_name ) as wire "," ws "to:" ws ( map_name ) as to [ whitespace ]
95
123
  mapping_body = [ ws ] "{" [ whitespace ] ( *( map_line ) ) as maps "}"
96
- mapping_section = [ whitespace ] "mapping" ws ( variable ) as format mapping_body
124
+ class_mapping_section = ( [ whitespace ] "mapping" ws ( 1*word ) as format [ ws ] serialization_body [ whitespace ] ) as serialization_mappings
97
125
 
98
126
  derive_definition = ( [ whitespace ] "derive" ws attribute_name [ ws ] ":" [ ws ] [ quotes ] ( 1*typechar ) as type [ quotes ] ) as derived_field [ whitespace ]
99
127
  string_format_item = ( [ ws ] ( "artifact" / "root" ) as key ws ( quoted_string / 1*nonspace ) as value [ whitespace ] )
100
128
  string_format_body = "{" [ whitespace ] ( *( string_format_item ) ) as items [ whitespace ] "}"
101
129
  string_format_definition = ( [ whitespace ] "string_format" ws ( variable ) as format [ ws ] string_format_body ) as string_format
102
130
 
103
- class_inner_definitions = definition_body / mapping_section / derive_definition / string_format_definition / ( !( "attribute" ws ) attribute_definition ) / keyword_attribute_definition / comment_definition / comment_multiline_definition
131
+ class_inner_definitions = definition_body / definition_line / class_mapping_section / derive_definition / string_format_definition / attribute_comma_definition / ( !( "attribute" ws ) attribute_definition ) / keyword_attribute_definition / comment_definition / comment_multiline_definition
104
132
  class_inner_definition = class_inner_definitions [ whitespace ]
105
133
  class_body = [ ws ] "{" [ whitespace ] ( *( class_inner_definition ) ) as members "}"
106
134
  parent_class = [ ws ] "<" [ ws ] ( class_name_chars ) as parent_class
@@ -109,12 +137,26 @@ grammar Lml version "1.0.0" {
109
137
  # Balanced-brace definition text: nested "{...}" and "\"-escapes allowed
110
138
  definition_content = *( ( "\\" any_char ) / ( "{" definition_content "}" ) / ( !"}" any_char ) )
111
139
  definition_body = [ ws ] "definition" [ whitespace ] "{" ( definition_content ) as definition "}"
140
+ definition_line = ( [ ws ] "definition" ws ( dq_string / sq_string ) ) as definition [ whitespace ]
112
141
 
113
142
  member_field_line = [ ws ] attribute_name [ ws ] ( 1*nonspace ) as member_field_type [ whitespace ]
114
143
  member_fields_definition = ( [ whitespace ] "member_fields" [ whitespace ] "{" [ whitespace ] ( *( member_field_line ) ) as member_fields [ whitespace ] "}" ) as member_fields_decl
144
+ attribute_comma_definition = ( [ whitespace ] "attribute" ws attribute_name "," [ ws ] ( 1*typechar ) as type [ ws ] [ "{" [ whitespace ] keyword_attribute_options [ whitespace ] "}" ] [ whitespace ] ) as attributes
115
145
  enum_member_with_payload = ( [ whitespace ] attribute_name [ ws ] ( class_body / keyword_attribute_body ) ) as attributes
116
146
 
117
- enum_inner_definitions = definition_body / member_fields_definition / enum_member_with_payload / attribute_definition / comment_definition / comment_multiline_definition
147
+ # ---- Canonical value construct (RS 3001 par. Value / par. Enums) ----
148
+ # `value <name> { block }` is the canonical enum member; the bare
149
+ # identifier member is its documented shortcut. from_table expands to
150
+ # value members at compile time.
151
+ value_name = quoted_string / map_name
152
+ value_inner_definition = ( definition_body / definition_line / instance / attribute_line / comment_definition / comment_multiline_definition ) [ whitespace ]
153
+ value_body = "{" [ whitespace ] ( *( value_inner_definition ) ) as members "}"
154
+ value_definition = ( [ whitespace ] "value" ws ( value_name ) as value_name [ ws ] [ value_body ] [ whitespace ] ) as value_decl
155
+
156
+ from_table_definition = ( [ whitespace ] "from_table" [ ws ] ( map_name ) as from_table_name [ ws ] [ "{" [ whitespace ] *( "artifact" [ ws ] ( map_name ) as artifact_table [ whitespace ] ) "}" ] [ whitespace ] ) as from_table_decl
157
+
158
+ enum_values_shorthand = ( [ whitespace ] "values" ws value_set_body [ whitespace ] ) as value_set_decl
159
+ enum_inner_definitions = definition_body / definition_line / member_fields_definition / from_table_definition / enum_values_shorthand / value_definition / enum_member_with_payload / attribute_comma_definition / attribute_definition / comment_definition / comment_multiline_definition
118
160
  enum_inner_definition = enum_inner_definitions [ whitespace ]
119
161
  enum_body = [ ws ] "{" [ whitespace ] ( *( enum_inner_definition ) ) as members "}"
120
162
  enum_definition = [ whitespace ] "enum" ws [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ enum_body ]
@@ -122,12 +164,14 @@ grammar Lml version "1.0.0" {
122
164
  data_type_definition = [ whitespace ] "data_type" ws [ quotes ] class_name as name [ quotes ] [ attribute_stereotype ] [ data_type_body ]
123
165
  primitive_definition = [ whitespace ] "primitive" ws [ quotes ] class_name as name [ quotes ]
124
166
 
125
- diagram_inner_definitions = title_definition / caption_definition / fontname_definition / ( class_definition ) as classes / ( enum_definition ) as enums / ( primitive_definition ) as primitives / ( data_type_definition ) as data_types / ( association_definition ) as associations / comment_definition / comment_multiline_definition
167
+ package_body = [ ws ] "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
168
+ package_definition = ( [ whitespace ] "package" ws class_name as name [ ws ] package_body [ whitespace ] ) as packages
169
+ diagram_inner_definitions = namespace_definition / mapping_definition / package_definition / title_definition / caption_definition / fontname_definition / ( class_definition ) as classes / ( enum_definition ) as enums / ( primitive_definition ) as primitives / ( data_type_definition ) as data_types / ( association_definition ) as associations / comment_definition / comment_multiline_definition
126
170
  diagram_inner_definition = diagram_inner_definitions [ whitespace ]
127
171
  diagram_body = [ ws ] "{" [ whitespace ] ( *( diagram_inner_definition ) ) as members "}"
128
172
  diagram_definition = [ whitespace ] "diagram" [ ws ] class_name as name diagram_body [ whitespace ]
129
173
  diagram_definitions = diagram_definition [ whitespace ]
130
- model_definitions = ( *( diagram_inner_definition ) ) as members
174
+ model_definitions = ( 1*( diagram_inner_definition ) ) as members
131
175
 
132
176
  view_only_definitions = ( view_import ) as view_imports / show_directive / hide_directive
133
177
  view_inner_definitions = view_only_definitions / diagram_inner_definitions
@@ -156,7 +200,8 @@ grammar Lml version "1.0.0" {
156
200
  require_block = ( *( require_stmt [ whitespace ] ) ) as requires
157
201
 
158
202
  # ---- Instances ----
159
- instance = keyword_instance / class_instance
203
+ instance = instance_named / keyword_instance / class_instance
204
+ instance_named = ( [ whitespace ] "instance" ws quoted_string as name [ ws ] [ ( namespaced_identifier ) as instance_type ] [ ws ] "{" [ whitespace ] ( ( [ ws ] instance ) / attributes ) "}" [ whitespace ] ) as instance
160
205
  isa_definition = [ whitespace ] "isa" ws ( namespaced_identifier ) as isa [ whitespace ]
161
206
  keyword_instance = ( [ whitespace ] "instance" [ ws ( namespaced_identifier ) as instance_type ] [ ws ] "{" [ whitespace ] [ isa_definition ] ( ( [ ws ] instance ) / attributes ) "}" ) as instance [ whitespace ]
162
207
  class_instance = ( variable as instance_type [ whitespace ] quoted_string as name [ whitespace ] [ [ whitespace ] "extends" [ whitespace ] quoted_string as parent [ whitespace ] ] "{" [ whitespace ] [ lml_instance_body ] "}" ) as instance [ whitespace ]
@@ -164,11 +209,11 @@ grammar Lml version "1.0.0" {
164
209
  instance_template = [ whitespace ] "template" [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}"
165
210
  lml_instance_members = ( instance_template ) as template / attributes
166
211
  models = [ whitespace ] "models" [ whitespace ] variable as name [ whitespace ] "{" ( *( ( ( string_format_definition ) as default_string_formats ) / model_body ) ) as members "}" [ whitespace ]
167
- model_body = ( ( class_definition ) as classes / ( enum_definition ) as enums ) [ whitespace ]
212
+ model_body = diagram_inner_definition
168
213
 
169
214
  # ---- Data structures ----
170
- attribute_value = instance / list / key_value_map / value / 1*( !%x0A any_char )
171
- list_item = instance / value
215
+ attribute_value = instance / list / key_value_map / data_value / 1*( !%x0A any_char )
216
+ list_item = instance / data_value
172
217
  list = "[" [ whitespace ] ( *( list_item [ ws ] [ "," ] [ whitespace ] ) ) as list [ whitespace ] "]"
173
218
  collection = [ whitespace ] "collection" ws quoted_string as name [ ws ] "{" [ whitespace ] [ includes ] [ whitespace ] [ validation ] [ whitespace ] "}" [ whitespace ]
174
219
  includes = [ whitespace ] "includes" [ ws ] ( list ) as includes
@@ -176,14 +221,16 @@ grammar Lml version "1.0.0" {
176
221
  condition = [ whitespace ] "condition" ws quoted_string as condition [ whitespace ]
177
222
  import = [ whitespace ] "import" [ ws ] "{" [ whitespace ] ( *( import_definition ) ) as imports "}" [ whitespace ]
178
223
  import_definition = ( 1*nonspace ) as format_type [ ws ] quoted_string as file [ whitespace ] "{" [ whitespace ] attributes [ whitespace ] "}" [ whitespace ]
179
- instances = [ whitespace ] "instances" [ whitespace ] "{" [ whitespace ] [ instances_body ] "}" [ whitespace ]
224
+ instances = [ whitespace ] "instances" [ ws ] [ ( quoted_string / variable ) as name ] [ whitespace ] "{" [ whitespace ] [ instances_body ] "}" [ whitespace ]
180
225
  instances_body = ( *( instances_member [ whitespace ] ) ) as instances
181
226
  instances_member = import / ( collection ) as collections / export / instance
182
227
  export = [ whitespace ] "export" [ whitespace ] "{" [ whitespace ] ( *( export_format [ whitespace ] ) ) as exports "}" [ whitespace ]
183
228
  export_format = [ whitespace ] "format" ws variable as format_type [ whitespace ] "{" [ whitespace ] attributes "}" [ whitespace ]
184
229
 
185
- # ---- Root (grammar/instances.rb: Full) ----
186
- diagram = [ require_block ] ( models / diagram_definitions / view_definitions / instances / instance / model_definitions )
230
+ # ---- Root (RS 3001: a document interleaves declarations) ----
231
+ diagram = [ require_block ] ( *( top_level_definition ) ) as members
232
+
233
+ top_level_definition = ( models / diagram_definitions / view_definitions / instances / instance / model_definitions ) [ whitespace ]
187
234
  }
188
235
 
189
236
  entry diagram: diagram
@@ -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)
@@ -183,14 +193,30 @@ module Lutaml
183
193
  end
184
194
 
185
195
  def compile_document(doc)
196
+ @declared_namespaces = Array(doc.namespaces)
197
+ @serialization_mappings = Array(doc.serialization_mappings)
186
198
  doc.classes.each { |c| compile_class(c) }
199
+ hoist_class_serialization_mappings(doc)
187
200
  doc.enums.each { |e| compile_enum(e) }
188
201
  doc.data_types.each { |dt| compile_data_type(dt) }
189
202
  resolve_forward_references
190
203
  apply_xml_mappings
204
+ apply_key_value_mappings
191
205
  register_string_formats(doc)
192
206
  end
193
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
218
+ end
219
+
194
220
  def resolve_forward_references
195
221
  @forward_refs.each do |class_name, deferred_attrs|
196
222
  klass = @compiled[class_name]
@@ -232,6 +258,7 @@ module Lutaml
232
258
 
233
259
  def compile_enum(enum_def)
234
260
  name = enum_def.name.to_s
261
+ expand_from_table(enum_def)
235
262
  values = extract_enum_values(enum_def)
236
263
 
237
264
  payload_fields = Array(enum_def.member_fields).map(&:name).map(&:to_s)
@@ -246,7 +273,8 @@ module Lutaml
246
273
  end
247
274
 
248
275
  values.each do |value|
249
- compiled_klass.define_singleton_method(value.name) do
276
+ accessor = value.name.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
277
+ compiled_klass.define_singleton_method(accessor) do
250
278
  new({ value: value.name, description: value.description }.merge(value.payload || {}))
251
279
  end
252
280
  end
@@ -297,6 +325,9 @@ module Lutaml
297
325
  @compiled.each do |name, klass|
298
326
  next if @enum_names.include?(name)
299
327
 
328
+ custom = serialization_mapping_for(name, 'xml')
329
+ next apply_custom_xml_mapping(klass, custom) if custom
330
+
300
331
  apply_xml_mapping(klass, name)
301
332
  end
302
333
  end
@@ -313,16 +344,126 @@ module Lutaml
313
344
  end
314
345
  end
315
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
+
316
440
  def build_attributes(klass_def)
317
441
  Array(klass_def.attributes).map do |attr|
318
442
  attr_name = attr.name.to_sym
319
- raw_type = attr.type.to_s
443
+ raw_type = resolve_values_type(klass_def, attr)
320
444
  type = resolve_type(raw_type)
321
445
  options = build_options(attr)
322
446
  [attr_name, raw_type, type, options]
323
447
  end
324
448
  end
325
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
+
326
467
  def resolve_type(type_name)
327
468
  if TYPE_MAP.key?(type_name)
328
469
  TYPE_MAP[type_name]
@@ -391,20 +532,76 @@ module Lutaml
391
532
  raise ValidationError, "Unrecognized cardinality token: #{val.inspect}"
392
533
  end
393
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
+
394
582
  def extract_enum_values(enum_def)
395
583
  member_fields = Array(enum_def.member_fields).map do |f|
396
584
  [f.name.to_s, f.type.to_s]
397
585
  end
398
586
  enum_def.attributes.map do |attr|
399
- description = Array(attr.attributes).find do |nested|
400
- nested.name == 'description'
401
- end
587
+ description = member_description(attr)
402
588
  payload = extract_member_payload(attr)
403
589
  validate_member_payload(enum_def, attr, payload, member_fields)
404
- EnumValue.new(attr.name.to_s, description&.type, payload)
590
+ EnumValue.new(attr.name.to_s, description, payload)
405
591
  end
406
592
  end
407
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)
600
+ end
601
+ text = nested&.type || nested&.definition || nested&.value
602
+ text.is_a?(Hash) && text.key?(:string) ? text[:string] : text
603
+ end
604
+
408
605
  # L7a: per-member payload lines ride the keyword-attribute
409
606
  # properties (`abbreviation = "WD"`)
410
607
  def extract_member_payload(attr)
@@ -457,18 +654,12 @@ module Lutaml
457
654
  def register_string_formats(doc)
458
655
  defaults = doc.packages.flat_map(&:default_string_formats)
459
656
  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
466
-
467
- defaults.each do |fmt|
468
- next if Array(class_def.string_formats).map(&:format).include?(fmt.format)
657
+ declared = Array(class_def.string_formats)
658
+ declared.each { |fmt| register_string_format(class_def.name.to_s, fmt) }
469
659
 
470
- register_string_format(class_def.name.to_s, fmt)
471
- end
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) }
472
663
  end
473
664
  end
474
665
 
@@ -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,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: -> { [] }
@@ -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
@@ -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
@@ -19,6 +19,8 @@ module Lutaml
19
19
  attribute :definition, Lutaml::Lml::Types::TextType
20
20
  attribute :association, :string
21
21
  attribute :default, :string
22
+ attribute :value_set, :string, collection: true, default: -> { [] }
23
+ attribute :pattern, :string
22
24
 
23
25
  # LML-specific attributes
24
26
  attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
@@ -53,6 +55,8 @@ module Lutaml
53
55
  map "definition", to: :definition
54
56
  map "association", to: :association
55
57
  map "default", to: :default
58
+ map "value_set", to: :value_set
59
+ map "pattern", to: :pattern
56
60
  map "properties", to: :properties
57
61
  map "value", to: :value, with: { to: :value_to, from: :value_from }
58
62
  map "attributes", to: :attributes
@@ -26,6 +26,7 @@ module Lutaml
26
26
  attribute :wire_mappings, "Lutaml::Lml::Mapping", collection: true, default: -> { [] }
27
27
  attribute :string_formats, "Lutaml::Lml::StringFormat", collection: true, default: -> { [] }
28
28
  attribute :derived_fields, "Lutaml::Lml::DerivedField", collection: true, default: -> { [] }
29
+ attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
29
30
 
30
31
  # LML-specific
31
32
  attribute :parent_class, :string