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.
- checksums.yaml +4 -4
- data/lib/lutaml/lml/cli.rb +12 -3
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +49 -9
- data/lib/lutaml/lml/data_processor/instance_processing.rb +5 -0
- data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +62 -0
- data/lib/lutaml/lml/data_processor.rb +3 -0
- data/lib/lutaml/lml/document_builder.rb +235 -9
- data/lib/lutaml/lml/grammar/lml.artifact.json +6473 -2425
- data/lib/lutaml/lml/grammar/lml.parg +82 -19
- data/lib/lutaml/lml/model_compiler.rb +392 -39
- data/lib/lutaml/lml/models/data_type.rb +3 -0
- data/lib/lutaml/lml/models/derived_field.rb +14 -0
- data/lib/lutaml/lml/models/document.rb +2 -0
- data/lib/lutaml/lml/models/enum.rb +3 -0
- data/lib/lutaml/lml/models/instance.rb +2 -0
- data/lib/lutaml/lml/models/instance_collection.rb +1 -0
- data/lib/lutaml/lml/models/mapping.rb +15 -0
- data/lib/lutaml/lml/models/mapping_rule.rb +16 -0
- data/lib/lutaml/lml/models/member_field.rb +12 -0
- data/lib/lutaml/lml/models/namespace.rb +14 -0
- data/lib/lutaml/lml/models/package.rb +1 -0
- data/lib/lutaml/lml/models/serialization_mapping.rb +17 -0
- data/lib/lutaml/lml/models/string_format.rb +16 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +4 -0
- data/lib/lutaml/lml/models/uml_class.rb +4 -0
- data/lib/lutaml/lml/preprocessor.rb +82 -6
- data/lib/lutaml/lml/validator.rb +276 -0
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +8 -0
- metadata +11 -2
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 =
|
|
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 ==
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
demodulize(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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?(
|
|
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
|
|
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?(
|
|
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] =
|
|
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
|
|
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 =
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
EnumValue.new(attr.name.to_s, description
|
|
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(
|
|
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: []
|