lutaml-lml 0.2.0 → 0.3.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 +84 -18
- data/lib/lutaml/lml/data_processor/value_processing.rb +6 -1
- data/lib/lutaml/lml/document_builder.rb +72 -3
- data/lib/lutaml/lml/format/adapter/mapping.rb +12 -0
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +41 -12
- data/lib/lutaml/lml/formatter/elk.rb +45 -0
- data/lib/lutaml/lml/formatter/graphviz/node_formatter.rb +2 -1
- data/lib/lutaml/lml/formatter/svg_pdf/path_parser.rb +134 -0
- data/lib/lutaml/lml/formatter/svg_pdf.rb +281 -0
- data/lib/lutaml/lml/formatter.rb +2 -0
- data/lib/lutaml/lml/grammar/lml.artifact.json +7265 -4550
- data/lib/lutaml/lml/grammar/lml.parg +230 -115
- data/lib/lutaml/lml/layout/elk_engine.rb +15 -0
- data/lib/lutaml/lml/layout/elk_graph_builder.rb +129 -0
- data/lib/lutaml/lml/layout/elk_svg_markers.rb +45 -0
- data/lib/lutaml/lml/layout/elk_svg_renderer.rb +119 -0
- data/lib/lutaml/lml/layout/graph_viz_engine.rb +1 -1
- data/lib/lutaml/lml/layout.rb +4 -0
- data/lib/lutaml/lml/model_compiler/enums.rb +172 -0
- data/lib/lutaml/lml/model_compiler/hydration.rb +123 -0
- data/lib/lutaml/lml/model_compiler/serialization_mappings.rb +162 -0
- data/lib/lutaml/lml/model_compiler/string_formats.rb +42 -0
- data/lib/lutaml/lml/model_compiler/validation.rb +65 -0
- data/lib/lutaml/lml/model_compiler.rb +46 -453
- data/lib/lutaml/lml/models/document.rb +29 -0
- data/lib/lutaml/lml/models/operation_parameter.rb +1 -0
- data/lib/lutaml/lml/models/reference.rb +23 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +12 -0
- data/lib/lutaml/lml/models/uml_class.rb +1 -1
- data/lib/lutaml/lml/preprocessor.rb +44 -79
- data/lib/lutaml/lml/transform.rb +54 -6
- data/lib/lutaml/lml/validator.rb +88 -19
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +19 -3
- metadata +17 -4
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
class ModelCompiler
|
|
6
|
+
# Evaluates hydrated instances against their compiled
|
|
7
|
+
# models (unknown attributes, enum membership, nesting).
|
|
8
|
+
module Validation
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def validate_instance(instance, errors, path = 'root')
|
|
12
|
+
if instance.instance && Array(instance.attributes).empty?
|
|
13
|
+
validate_instance(instance.instance, errors, "#{path}.instance")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attrs = Array(instance.attributes)
|
|
18
|
+
type_attr = attrs.find { |a| a.name == 'type' }
|
|
19
|
+
type_name = type_attr ? type_attr.value.to_s : instance.type
|
|
20
|
+
klass = @compiled[demodulize(type_name)]
|
|
21
|
+
unless klass
|
|
22
|
+
errors << "#{path}: unknown type '#{type_name}'"
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
schema_attrs = klass.attributes.keys.map(&:to_s)
|
|
27
|
+
present_attrs = attrs.map(&:name)
|
|
28
|
+
schema_set = schema_attrs.to_set
|
|
29
|
+
present_set = present_attrs.to_set
|
|
30
|
+
|
|
31
|
+
unknown = present_set - schema_set - Set.new(%w[type])
|
|
32
|
+
unknown.each do |name|
|
|
33
|
+
errors << "#{path}.#{name}: attribute not defined on #{type_name}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
required = klass.attributes.values
|
|
37
|
+
.reject { |v| v.collection? || v.options.key?(:default) }
|
|
38
|
+
.to_set { |v| v.name.to_s }
|
|
39
|
+
|
|
40
|
+
missing = required - present_set
|
|
41
|
+
missing.each do |name|
|
|
42
|
+
errors << "#{path}.#{name}: required attribute missing (cardinality min >= 1)"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attrs.each do |attr|
|
|
46
|
+
next unless schema_attrs.include?(attr.name)
|
|
47
|
+
|
|
48
|
+
attr_def = klass.attributes[attr.name.to_sym]
|
|
49
|
+
next unless attr_def
|
|
50
|
+
|
|
51
|
+
next unless attr_def.collection? && attr.instances.any?
|
|
52
|
+
|
|
53
|
+
attr.instances.each_with_index do |nested, i|
|
|
54
|
+
validate_instance(nested, errors, "#{path}.#{attr.name}[#{i}]")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return unless instance.instance
|
|
59
|
+
|
|
60
|
+
validate_instance(instance.instance, errors, "#{path}.instance")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -4,9 +4,24 @@ require 'json'
|
|
|
4
4
|
|
|
5
5
|
module Lutaml
|
|
6
6
|
module Lml
|
|
7
|
+
# Compiles LML model definitions into anonymous
|
|
8
|
+
# Lutaml::Model::Serializable subclasses, hydrates instance data
|
|
9
|
+
# against them, and applies declared serialization mappings. The
|
|
10
|
+
# concern modules (Hydration, Validation, Enums,
|
|
11
|
+
# SerializationMappings, StringFormats) share this class's state.
|
|
7
12
|
class ModelCompiler
|
|
13
|
+
autoload :Hydration, "lutaml/lml/model_compiler/hydration"
|
|
14
|
+
autoload :Validation, "lutaml/lml/model_compiler/validation"
|
|
15
|
+
autoload :Enums, "lutaml/lml/model_compiler/enums"
|
|
16
|
+
autoload :SerializationMappings, "lutaml/lml/model_compiler/serialization_mappings"
|
|
17
|
+
autoload :StringFormats, "lutaml/lml/model_compiler/string_formats"
|
|
18
|
+
|
|
8
19
|
class ValidationError < Error; end
|
|
9
20
|
|
|
21
|
+
# The default generated namespace nests under Lutaml::Lml so
|
|
22
|
+
# compiled constants never collide with top-level constants.
|
|
23
|
+
DEFAULT_NAMESPACE = 'Lutaml::Lml::Generated'
|
|
24
|
+
|
|
10
25
|
TYPE_MAP = {
|
|
11
26
|
'String' => :string,
|
|
12
27
|
'string' => :string,
|
|
@@ -20,13 +35,19 @@ module Lutaml
|
|
|
20
35
|
'date_time' => :date_time,
|
|
21
36
|
'DateTime' => :date_time,
|
|
22
37
|
'Time' => :time,
|
|
23
|
-
'Uri' => :string
|
|
38
|
+
'Uri' => :string,
|
|
24
39
|
}.freeze
|
|
25
40
|
|
|
26
41
|
# Tokens that denote unbounded cardinality in LML attribute
|
|
27
42
|
# declarations. Anything else must parse as an Integer.
|
|
28
43
|
UNBOUNDED_TOKENS = %w[* n N unbounded].freeze
|
|
29
44
|
|
|
45
|
+
include Hydration
|
|
46
|
+
include Validation
|
|
47
|
+
include Enums
|
|
48
|
+
include SerializationMappings
|
|
49
|
+
include StringFormats
|
|
50
|
+
|
|
30
51
|
def initialize(namespace: nil, artifact_paths: {}, default_artifact: nil)
|
|
31
52
|
@namespace = namespace
|
|
32
53
|
@artifact_paths = artifact_paths
|
|
@@ -81,117 +102,6 @@ module Lutaml
|
|
|
81
102
|
|
|
82
103
|
private
|
|
83
104
|
|
|
84
|
-
def validate_instance(instance, errors, path = 'root')
|
|
85
|
-
if instance.instance && Array(instance.attributes).empty?
|
|
86
|
-
validate_instance(instance.instance, errors, "#{path}.instance")
|
|
87
|
-
return
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
attrs = Array(instance.attributes)
|
|
91
|
-
type_attr = attrs.find { |a| a.name == 'type' }
|
|
92
|
-
type_name = type_attr ? type_attr.value.to_s : instance.type
|
|
93
|
-
klass = @compiled[demodulize(type_name)]
|
|
94
|
-
unless klass
|
|
95
|
-
errors << "#{path}: unknown type '#{type_name}'"
|
|
96
|
-
return
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
schema_attrs = klass.attributes.keys.map(&:to_s)
|
|
100
|
-
present_attrs = attrs.map(&:name)
|
|
101
|
-
schema_set = schema_attrs.to_set
|
|
102
|
-
present_set = present_attrs.to_set
|
|
103
|
-
|
|
104
|
-
unknown = present_set - schema_set - Set.new(%w[type])
|
|
105
|
-
unknown.each do |name|
|
|
106
|
-
errors << "#{path}.#{name}: attribute not defined on #{type_name}"
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
required = klass.attributes.select do |_k, v|
|
|
110
|
-
!v.collection? && !v.options.key?(:default)
|
|
111
|
-
end.keys.map(&:to_s).to_set
|
|
112
|
-
|
|
113
|
-
missing = required - present_set
|
|
114
|
-
missing.each do |name|
|
|
115
|
-
errors << "#{path}.#{name}: required attribute missing (cardinality min >= 1)"
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
attrs.each do |attr|
|
|
119
|
-
next unless schema_attrs.include?(attr.name)
|
|
120
|
-
|
|
121
|
-
attr_def = klass.attributes[attr.name.to_sym]
|
|
122
|
-
next unless attr_def
|
|
123
|
-
|
|
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}]")
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
return unless instance.instance
|
|
132
|
-
|
|
133
|
-
validate_instance(instance.instance, errors, "#{path}.instance")
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def hydrate_instance(instance)
|
|
137
|
-
inner = instance.instance
|
|
138
|
-
return hydrate_instance(inner) if inner && Array(instance.attributes).empty?
|
|
139
|
-
|
|
140
|
-
type_name = resolve_instance_type(instance)
|
|
141
|
-
klass = @compiled[type_name]
|
|
142
|
-
return hydrate_as_untyped(instance, type_name) unless klass
|
|
143
|
-
|
|
144
|
-
attrs = extract_instance_attributes(instance, klass)
|
|
145
|
-
klass.new(**attrs)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def resolve_instance_type(instance)
|
|
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)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def demodulize(name)
|
|
157
|
-
name.split('::').last.to_s
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def hydrate_as_untyped(instance, type_name)
|
|
161
|
-
{ _name: type_name, **extract_raw_attributes(instance) }
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def extract_raw_attributes(instance)
|
|
165
|
-
instance.each_attribute.each_with_object({}) do |(name, value, nested), hash|
|
|
166
|
-
hash[name.to_sym] = resolve_instance_value(value, nested)
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def extract_instance_attributes(instance, klass)
|
|
171
|
-
schema_keys = klass.attributes.keys.to_set
|
|
172
|
-
instance.each_attribute.each_with_object({}) do |(name, value, nested), hash|
|
|
173
|
-
key = name.to_sym
|
|
174
|
-
next unless schema_keys.include?(key)
|
|
175
|
-
|
|
176
|
-
hash[key] = resolve_instance_value(value, nested, klass.attributes[key])
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def resolve_instance_value(value, nested, attr_def = nil)
|
|
181
|
-
if nested.any?
|
|
182
|
-
nested.map { |i| hydrate_instance(i) }
|
|
183
|
-
elsif value.is_a?(Array)
|
|
184
|
-
value
|
|
185
|
-
elsif (enum_class = attr_def && enum_class_for(attr_def.type))
|
|
186
|
-
value_name = enum_value_name(value.to_s)
|
|
187
|
-
description = enum_class.values
|
|
188
|
-
.find { |v| v.name == value_name }&.description
|
|
189
|
-
enum_class.new(value: value_name, description: description)
|
|
190
|
-
elsif !value.nil?
|
|
191
|
-
value
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
105
|
def compile_document(doc)
|
|
196
106
|
@declared_namespaces = Array(doc.namespaces)
|
|
197
107
|
@serialization_mappings = Array(doc.serialization_mappings)
|
|
@@ -205,18 +115,6 @@ module Lutaml
|
|
|
205
115
|
register_string_formats(doc)
|
|
206
116
|
end
|
|
207
117
|
|
|
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
|
-
|
|
220
118
|
def resolve_forward_references
|
|
221
119
|
@forward_refs.each do |class_name, deferred_attrs|
|
|
222
120
|
klass = @compiled[class_name]
|
|
@@ -237,57 +135,7 @@ module Lutaml
|
|
|
237
135
|
register(name, compiled_klass)
|
|
238
136
|
end
|
|
239
137
|
|
|
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
138
|
alias compile_data_type compile_class
|
|
256
|
-
|
|
257
|
-
EnumValue = Struct.new(:name, :description, :payload)
|
|
258
|
-
|
|
259
|
-
def compile_enum(enum_def)
|
|
260
|
-
name = enum_def.name.to_s
|
|
261
|
-
expand_from_table(enum_def)
|
|
262
|
-
values = extract_enum_values(enum_def)
|
|
263
|
-
|
|
264
|
-
payload_fields = Array(enum_def.member_fields).map(&:name).map(&:to_s)
|
|
265
|
-
compiled_klass = Class.new(Lutaml::Model::Serializable) do
|
|
266
|
-
attribute :value, :string, default: values.first.name
|
|
267
|
-
attribute :description, :string
|
|
268
|
-
payload_fields.each do |field|
|
|
269
|
-
attribute field.to_sym, :string
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
define_method(:to_s) { value }
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
values.each do |value|
|
|
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 || {}))
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
compiled_klass.define_singleton_method(:values) { values }
|
|
282
|
-
|
|
283
|
-
@enum_names << name
|
|
284
|
-
@enum_classes[name] = compiled_klass
|
|
285
|
-
register(name, compiled_klass)
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
# Builds a compiled Serializable subclass from a class or data-type
|
|
289
|
-
# definition: declares attributes and stashes forward-referenced
|
|
290
|
-
# attributes for post-registration resolution.
|
|
291
139
|
def build_compiled_class(def_obj)
|
|
292
140
|
all_attrs = build_attributes(def_obj)
|
|
293
141
|
immediate, deferred = all_attrs.partition do |(_, raw_type, type, _)|
|
|
@@ -318,125 +166,6 @@ module Lutaml
|
|
|
318
166
|
compiled_klass
|
|
319
167
|
end
|
|
320
168
|
|
|
321
|
-
# Apply a lutaml-model XML mapping to each compiled class so they
|
|
322
|
-
# support from_xml/to_xml. Runs after forward references are resolved
|
|
323
|
-
# so all attributes (immediate + deferred) get element mappings.
|
|
324
|
-
def apply_xml_mappings
|
|
325
|
-
@compiled.each do |name, klass|
|
|
326
|
-
next if @enum_names.include?(name)
|
|
327
|
-
|
|
328
|
-
custom = serialization_mapping_for(name, 'xml')
|
|
329
|
-
next apply_custom_xml_mapping(klass, custom) if custom
|
|
330
|
-
|
|
331
|
-
apply_xml_mapping(klass, name)
|
|
332
|
-
end
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
def apply_xml_mapping(klass, root_name)
|
|
336
|
-
attr_names = klass.attributes.keys
|
|
337
|
-
return if attr_names.empty?
|
|
338
|
-
|
|
339
|
-
klass.xml do |mapping|
|
|
340
|
-
mapping.root(root_name)
|
|
341
|
-
attr_names.each do |attr_name|
|
|
342
|
-
mapping.map_element(attr_name.to_s, to: attr_name)
|
|
343
|
-
end
|
|
344
|
-
end
|
|
345
|
-
end
|
|
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
|
-
|
|
440
169
|
def build_attributes(klass_def)
|
|
441
170
|
Array(klass_def.attributes).map do |attr|
|
|
442
171
|
attr_name = attr.name.to_sym
|
|
@@ -447,33 +176,12 @@ module Lutaml
|
|
|
447
176
|
end
|
|
448
177
|
end
|
|
449
178
|
|
|
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
|
-
|
|
467
179
|
def resolve_type(type_name)
|
|
468
|
-
if TYPE_MAP.key?(type_name)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
@compiled[type_name]
|
|
474
|
-
else
|
|
475
|
-
:string
|
|
476
|
-
end
|
|
180
|
+
return TYPE_MAP[type_name] if TYPE_MAP.key?(type_name)
|
|
181
|
+
return :string if type_name.start_with?('reference:(')
|
|
182
|
+
return @compiled[type_name] if @compiled.key?(type_name)
|
|
183
|
+
|
|
184
|
+
:string
|
|
477
185
|
end
|
|
478
186
|
|
|
479
187
|
def build_options(attr)
|
|
@@ -489,9 +197,9 @@ module Lutaml
|
|
|
489
197
|
min = parse_cardinality_value(card.min)
|
|
490
198
|
max = parse_cardinality_value(card.max)
|
|
491
199
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
200
|
+
collection = (max && (max > 1 || max == Float::INFINITY)) ||
|
|
201
|
+
(max.nil? && min && min > 1)
|
|
202
|
+
if collection
|
|
495
203
|
options[:collection] = true
|
|
496
204
|
elsif min.zero?
|
|
497
205
|
options[:default] = nil
|
|
@@ -519,9 +227,8 @@ module Lutaml
|
|
|
519
227
|
# render_nil is a lutaml-model MAPPING option, not an attribute
|
|
520
228
|
# option: collect the declaration for the mapping passes
|
|
521
229
|
def declared_render_nil(attr)
|
|
522
|
-
Array(attr.properties).find
|
|
523
|
-
|
|
524
|
-
end&.then { |prop| prop.value.to_s == 'true' }
|
|
230
|
+
prop = Array(attr.properties).find { |prop| prop.name.to_s == 'render_nil' }
|
|
231
|
+
prop ? prop.value.to_s == 'true' : nil
|
|
525
232
|
end
|
|
526
233
|
|
|
527
234
|
def parse_cardinality_value(val)
|
|
@@ -532,104 +239,6 @@ module Lutaml
|
|
|
532
239
|
raise ValidationError, "Unrecognized cardinality token: #{val.inspect}"
|
|
533
240
|
end
|
|
534
241
|
|
|
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
|
-
|
|
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
|
|
586
|
-
enum_def.attributes.map do |attr|
|
|
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)
|
|
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})"
|
|
619
|
-
end
|
|
620
|
-
|
|
621
|
-
def enum_class_for(type)
|
|
622
|
-
type.is_a?(Class) && @enum_classes.value?(type) and return type
|
|
623
|
-
end
|
|
624
|
-
|
|
625
|
-
def enum_value_name(raw)
|
|
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
242
|
def declare_derived_fields(compiled_klass, def_obj)
|
|
634
243
|
return unless def_obj.is_a?(UmlClass) || def_obj.is_a?(DataType)
|
|
635
244
|
|
|
@@ -647,35 +256,6 @@ module Lutaml
|
|
|
647
256
|
end
|
|
648
257
|
end
|
|
649
258
|
|
|
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
259
|
def artifact_path_for(reference)
|
|
680
260
|
mapped = @artifact_paths && @artifact_paths[reference]
|
|
681
261
|
return mapped if mapped && File.exist?(mapped.to_s)
|
|
@@ -700,7 +280,20 @@ module Lutaml
|
|
|
700
280
|
@compiled[name] = klass
|
|
701
281
|
return unless @namespace
|
|
702
282
|
|
|
703
|
-
|
|
283
|
+
namespace_module.const_set(name, klass)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def namespace_module
|
|
287
|
+
return @namespace if @namespace.is_a?(Module)
|
|
288
|
+
|
|
289
|
+
@namespace_module ||= begin
|
|
290
|
+
chain = @namespace.to_s.split('::')
|
|
291
|
+
chain.reduce(Object) do |parent, name|
|
|
292
|
+
parent.const_get(name)
|
|
293
|
+
rescue NameError
|
|
294
|
+
parent.const_set(name, Module.new)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
704
297
|
end
|
|
705
298
|
end
|
|
706
299
|
end
|
|
@@ -17,6 +17,9 @@ module Lutaml
|
|
|
17
17
|
attribute :enums, "Lutaml::Lml::Enum", collection: true, default: -> { [] }
|
|
18
18
|
attribute :packages, "Lutaml::Lml::Package", collection: true, default: -> { [] }
|
|
19
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: -> { {} }
|
|
20
23
|
attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
|
|
21
24
|
attribute :primitives, "Lutaml::Lml::PrimitiveType", collection: true, default: -> { [] }
|
|
22
25
|
attribute :associations, "Lutaml::Lml::Association", collection: true, default: -> { [] }
|
|
@@ -27,6 +30,32 @@ module Lutaml
|
|
|
27
30
|
attribute :instance, "Lutaml::Lml::Instance"
|
|
28
31
|
attribute :requires, :string, collection: true, default: -> { [] }
|
|
29
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
|
|
30
59
|
attribute :view_imports, "Lutaml::Lml::ViewImport", collection: true, default: -> { [] }
|
|
31
60
|
attribute :show_filter, "Lutaml::Lml::ViewFilter"
|
|
32
61
|
attribute :hide_filter, "Lutaml::Lml::ViewFilter"
|