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