lutaml-lml 0.3.3 → 0.3.4
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/exe/lutaml-lml-lsp +11 -0
- data/lib/lutaml/lml/cli.rb +165 -17
- data/lib/lutaml/lml/data_processor/attribute_processing.rb +4 -2
- data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +1 -0
- data/lib/lutaml/lml/data_processor.rb +8 -1
- data/lib/lutaml/lml/diagnostic.rb +149 -0
- data/lib/lutaml/lml/document_builder.rb +53 -21
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +22 -23
- data/lib/lutaml/lml/formatter/canonical.rb +719 -0
- data/lib/lutaml/lml/formatter.rb +2 -0
- data/lib/lutaml/lml/grammar/lml.artifact.json +2626 -2439
- data/lib/lutaml/lml/grammar/lml.parg +15 -4
- data/lib/lutaml/lml/grammar.rb +13 -0
- data/lib/lutaml/lml/import_resolver.rb +13 -4
- data/lib/lutaml/lml/lsp/conversions.rb +78 -0
- data/lib/lutaml/lml/lsp/keywords.rb +43 -0
- data/lib/lutaml/lml/lsp/server.rb +378 -0
- data/lib/lutaml/lml/lsp/store.rb +68 -0
- data/lib/lutaml/lml/lsp/transport.rb +53 -0
- data/lib/lutaml/lml/lsp.rb +16 -0
- data/lib/lutaml/lml/model_compiler.rb +16 -1
- data/lib/lutaml/lml/models/association.rb +3 -0
- data/lib/lutaml/lml/models/constraint.rb +3 -0
- data/lib/lutaml/lml/models/data_type.rb +3 -0
- data/lib/lutaml/lml/models/diagram.rb +3 -0
- data/lib/lutaml/lml/models/document.rb +10 -3
- data/lib/lutaml/lml/models/enum.rb +4 -1
- data/lib/lutaml/lml/models/instance.rb +3 -0
- data/lib/lutaml/lml/models/instance_collection.rb +3 -0
- data/lib/lutaml/lml/models/member_field.rb +2 -0
- data/lib/lutaml/lml/models/namespace.rb +2 -0
- data/lib/lutaml/lml/models/operation.rb +4 -1
- data/lib/lutaml/lml/models/package.rb +3 -0
- data/lib/lutaml/lml/models/primitive_type.rb +6 -3
- data/lib/lutaml/lml/models/serialization_mapping.rb +2 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +3 -0
- data/lib/lutaml/lml/models/uml_class.rb +3 -0
- data/lib/lutaml/lml/models/value.rb +3 -0
- data/lib/lutaml/lml/pipeline.rb +25 -8
- data/lib/lutaml/lml/positioned.rb +13 -0
- data/lib/lutaml/lml/preprocessor.rb +44 -13
- data/lib/lutaml/lml/quoting.rb +60 -0
- data/lib/lutaml/lml/source_position.rb +20 -0
- data/lib/lutaml/lml/symbol_index.rb +200 -0
- data/lib/lutaml/lml/transform.rb +49 -2
- data/lib/lutaml/lml/validator.rb +134 -21
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +18 -1
- metadata +16 -2
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Formatter
|
|
5
|
+
# The canonical LML emitter (RS 3001 informative "Canonical form"):
|
|
6
|
+
# a deterministic rendering of the document model. Canonicality is a
|
|
7
|
+
# formatting operation, not a validity constraint. The rules live in
|
|
8
|
+
# TODO.next/sota/canon.md.
|
|
9
|
+
#
|
|
10
|
+
# Laws: format(format(x)) == format(x) (idempotence), and
|
|
11
|
+
# parse(format(parse(x))) equals parse(x) under the document-model
|
|
12
|
+
# comparison.
|
|
13
|
+
class Canonical
|
|
14
|
+
include Lutaml::Lml::Quoting
|
|
15
|
+
|
|
16
|
+
GROUPS = %i[
|
|
17
|
+
namespaces packages classes enums data_types primitives
|
|
18
|
+
serialization_mappings associations diagrams
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
VISIBILITY_SYMBOLS = {
|
|
22
|
+
'private' => '-', 'protected' => '#', 'package' => '~'
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
VISIBILITY_WORDS = %w[private protected package].freeze
|
|
26
|
+
|
|
27
|
+
def initialize(document)
|
|
28
|
+
@document = document
|
|
29
|
+
@lines = []
|
|
30
|
+
@level = 0
|
|
31
|
+
@bodies = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render
|
|
35
|
+
render_document(@document)
|
|
36
|
+
trim_trailing_blanks
|
|
37
|
+
return '' if @lines.empty?
|
|
38
|
+
|
|
39
|
+
"#{@lines.join("\n")}\n"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# ---- document ----
|
|
45
|
+
|
|
46
|
+
def render_document(doc)
|
|
47
|
+
unless doc.lml_version.to_s.empty?
|
|
48
|
+
line("lml #{quote_always(doc.lml_version)}")
|
|
49
|
+
blank_line
|
|
50
|
+
end
|
|
51
|
+
Array(doc.requires).each { |path| line("require #{quote_always(path)}") }
|
|
52
|
+
blank_line if Array(doc.requires).any?
|
|
53
|
+
|
|
54
|
+
Array(doc.comments).each { |comment| top { render_comment(comment) } }
|
|
55
|
+
|
|
56
|
+
render_scope(doc)
|
|
57
|
+
top { render_instance(doc.instance) } if doc.instance
|
|
58
|
+
top { render_instance_collection(doc.instances) } if doc.instances
|
|
59
|
+
render_views(doc)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_scope(doc)
|
|
63
|
+
return render_model_members(doc) if doc.name.to_s.empty?
|
|
64
|
+
|
|
65
|
+
top do
|
|
66
|
+
render_block("models #{quote_scalar(doc.name)}#{version_suffix(doc)}") do
|
|
67
|
+
render_model_members(doc)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def version_suffix(doc)
|
|
73
|
+
version = doc.model_versions[doc.name]
|
|
74
|
+
version.nil? ? '' : " version #{quote_always(version)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_model_members(doc)
|
|
78
|
+
%i[title caption fontname].each do |attribute|
|
|
79
|
+
value = doc.public_send(attribute)
|
|
80
|
+
next if value.to_s.empty?
|
|
81
|
+
|
|
82
|
+
top { line("#{attribute} #{quote_always(value)}") }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
GROUPS.each do |collection|
|
|
86
|
+
Array(doc.public_send(collection)).each do |item|
|
|
87
|
+
top { render_group_member(collection, item) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_group_member(collection, item)
|
|
93
|
+
case collection
|
|
94
|
+
when :namespaces then render_namespace(item)
|
|
95
|
+
when :packages then render_package(item)
|
|
96
|
+
when :classes then render_class(item)
|
|
97
|
+
when :enums then render_enum(item)
|
|
98
|
+
when :data_types then render_data_type(item)
|
|
99
|
+
when :primitives then render_primitive(item)
|
|
100
|
+
when :serialization_mappings then render_mapping(item)
|
|
101
|
+
when :associations then render_association(item)
|
|
102
|
+
when :diagrams then render_diagram(item)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render_views(doc)
|
|
107
|
+
imports = Array(doc.view_imports)
|
|
108
|
+
show = doc.show_filter
|
|
109
|
+
hide = doc.hide_filter
|
|
110
|
+
return if imports.empty? && show.nil? && hide.nil?
|
|
111
|
+
|
|
112
|
+
top do
|
|
113
|
+
render_block("view #{quote_scalar(doc.name)}") do
|
|
114
|
+
imports.each { |entry| member { line("import #{quote_always(entry.path)}") } }
|
|
115
|
+
member { line("show #{show.entity_names.join(', ')}") } if show
|
|
116
|
+
member { line("hide #{hide.entity_names.join(', ')}") } if hide
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ---- declarations ----
|
|
122
|
+
|
|
123
|
+
def render_namespace(namespace)
|
|
124
|
+
render_block("namespace #{namespace.name}") do
|
|
125
|
+
line("uri #{quote_scalar(namespace.uri)}") if namespace.uri
|
|
126
|
+
line("prefix #{quote_scalar(namespace.prefix)}") if namespace.prefix
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def render_package(package)
|
|
131
|
+
render_block("#{declaration_visibility(package.visibility)}package #{package.name}") do
|
|
132
|
+
render_collection(package, :classes) { |item| render_class(item) }
|
|
133
|
+
render_collection(package, :enums) { |item| render_enum(item) }
|
|
134
|
+
render_collection(package, :data_types) { |item| render_data_type(item) }
|
|
135
|
+
render_collection(package, :packages) { |item| render_package(item) }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def render_class(klass)
|
|
140
|
+
render_block(class_header(klass)) do
|
|
141
|
+
render_comments(klass.comments)
|
|
142
|
+
member { render_definition(klass.definition) } if definition_present?(klass.definition)
|
|
143
|
+
Array(klass.attributes).each { |attribute| member { render_attribute(attribute) } }
|
|
144
|
+
Array(klass.operations).each { |operation| member { render_operation(operation) } }
|
|
145
|
+
Array(klass.derived_fields).each do |field|
|
|
146
|
+
member { line("derive #{field.name}: #{field.type}") }
|
|
147
|
+
end
|
|
148
|
+
Array(klass.string_formats).each { |format| member { render_string_format(format) } }
|
|
149
|
+
Array(klass.serialization_mappings).each do |mapping|
|
|
150
|
+
member { render_mapping(mapping, with_target: false) }
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def class_header(klass)
|
|
156
|
+
text = "#{declaration_visibility(klass.visibility)}#{class_modifier(klass)}"
|
|
157
|
+
text += "class #{klass.name}"
|
|
158
|
+
parent = klass.parent_class.to_s.empty? ? klass.type : klass.parent_class
|
|
159
|
+
text += " < #{parent}" if parent.to_s != ''
|
|
160
|
+
text + stereotype_suffix(klass.stereotype)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def class_modifier(klass)
|
|
164
|
+
return "#{klass.modifier} " if klass.modifier.to_s != ''
|
|
165
|
+
|
|
166
|
+
klass.is_abstract ? 'abstract ' : ''
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def render_enum(enum)
|
|
170
|
+
render_block("#{declaration_visibility(enum.visibility)}enum #{enum.name}#{stereotype_suffix(enum.stereotype)}") do
|
|
171
|
+
member { render_definition(enum.definition) } if definition_present?(enum.definition)
|
|
172
|
+
member { line('abstract true') } if enum.is_abstract
|
|
173
|
+
member { render_member_fields(enum.member_fields) } if Array(enum.member_fields).any?
|
|
174
|
+
member { render_from_table(enum) } if enum.from_table_name
|
|
175
|
+
Array(enum.attributes).each { |value| member { render_enum_member(value) } }
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def render_member_fields(fields)
|
|
180
|
+
render_block('member_fields') do
|
|
181
|
+
Array(fields).each { |field| line("#{field.name} #{field.type}") }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def render_from_table(enum)
|
|
186
|
+
header = "from_table #{quote_scalar(enum.from_table_name)}"
|
|
187
|
+
if enum.from_table_artifact.to_s.empty?
|
|
188
|
+
line(header)
|
|
189
|
+
else
|
|
190
|
+
render_block(header) do
|
|
191
|
+
line("artifact #{quote_scalar(enum.from_table_artifact)}")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def render_enum_member(value)
|
|
197
|
+
return render_attribute(value) if attribute_member?(value)
|
|
198
|
+
|
|
199
|
+
description = Array(value.attributes).find { |attribute| attribute.name == 'description' }
|
|
200
|
+
properties = Array(value.properties)
|
|
201
|
+
instances = Array(value.instances)
|
|
202
|
+
payload = description || properties.any? || instances.any?
|
|
203
|
+
|
|
204
|
+
unless payload
|
|
205
|
+
line("value #{quote_scalar(value.name)}")
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
render_block("value #{quote_scalar(value.name)}") do
|
|
210
|
+
member { render_definition(description.type) } if description
|
|
211
|
+
properties.each do |property|
|
|
212
|
+
member { line("#{property.name} = #{render_data_value(unwrap(property.value))}") }
|
|
213
|
+
end
|
|
214
|
+
instances.each { |instance| member { render_instance(instance) } }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# A member carrying the attribute shape (type, visibility, direct
|
|
219
|
+
# definition, cardinality, options) came from a bare attribute
|
|
220
|
+
# declaration, not from the `value` construct. Rendering it as a
|
|
221
|
+
# `value` block would move a direct definition into the nested
|
|
222
|
+
# description payload and change the model.
|
|
223
|
+
def attribute_member?(value)
|
|
224
|
+
value.type.to_s != '' ||
|
|
225
|
+
!public_visibility?(value.visibility) ||
|
|
226
|
+
definition_present?(value.definition) ||
|
|
227
|
+
value.cardinality || Array(value.value_set).any? ||
|
|
228
|
+
value.pattern || value.default ||
|
|
229
|
+
Array(value.stereotype).any? || value.literal
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def render_data_type(data_type)
|
|
233
|
+
render_block("#{declaration_visibility(data_type.visibility)}data_type #{data_type.name}#{stereotype_suffix(data_type.stereotype)}") do
|
|
234
|
+
member { render_definition(data_type.definition) } if definition_present?(data_type.definition)
|
|
235
|
+
Array(data_type.attributes).each { |attribute| member { render_attribute(attribute) } }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def render_primitive(primitive)
|
|
240
|
+
line("#{declaration_visibility(primitive.visibility)}primitive #{primitive.name}")
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def render_mapping(mapping, with_target: true)
|
|
244
|
+
target = with_target && mapping.target.to_s != '' ? " #{mapping.target}" : ''
|
|
245
|
+
render_block("mapping #{mapping.format}#{target}") do
|
|
246
|
+
line("element #{quote_always(mapping.element_name)}") if mapping.element_name
|
|
247
|
+
line("namespace #{mapping.namespace_ref}") if mapping.namespace_ref
|
|
248
|
+
line('mixed_content') if mapping.mixed_content
|
|
249
|
+
Array(mapping.rules).each do |rule|
|
|
250
|
+
text = mapping_rule(rule)
|
|
251
|
+
line(text) if text
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def mapping_rule(rule)
|
|
257
|
+
case rule.kind.to_s
|
|
258
|
+
when 'element' then "map_element #{wire_and_field(rule)}"
|
|
259
|
+
when 'attribute' then "map_attribute #{wire_and_field(rule)}"
|
|
260
|
+
when 'content' then "map_content #{rule.field}"
|
|
261
|
+
when 'namespace' then "namespace #{rule.field || rule.wire}"
|
|
262
|
+
when 'key' then "map_key to_instance #{rule.field}"
|
|
263
|
+
when 'value'
|
|
264
|
+
mode = rule.mode.to_s.empty? ? 'to_instance' : rule.mode
|
|
265
|
+
"map_value #{mode} #{rule.field}"
|
|
266
|
+
else "map #{wire_and_field(rule)}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def wire_and_field(rule)
|
|
271
|
+
parts = []
|
|
272
|
+
parts << quote_always(rule.wire) if rule.wire
|
|
273
|
+
parts << rule.field if rule.field
|
|
274
|
+
parts.join(' ')
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def render_association(association)
|
|
278
|
+
name = association.name.to_s.empty? ? '' : " #{association.name}"
|
|
279
|
+
render_block("association#{name}") do
|
|
280
|
+
member { line("owner_type #{association.owner_end_type}") } if association.owner_end_type
|
|
281
|
+
member { line("member_type #{association.member_end_type}") } if association.member_end_type
|
|
282
|
+
member { line(association_end('owner', association.owner_end, association.owner_end_attribute_name, association.owner_end_cardinality)) } if association.owner_end
|
|
283
|
+
member { line(association_end('member', association.member_end, association.member_end_attribute_name, association.member_end_cardinality)) } if association.member_end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def association_end(keyword, name, attribute_name, cardinality)
|
|
288
|
+
text = "#{keyword} #{quote_scalar(name)}"
|
|
289
|
+
text += "##{attribute_name}" if attribute_name.to_s != ''
|
|
290
|
+
text += " [#{cardinality_token(cardinality)}]" if cardinality
|
|
291
|
+
text
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def render_diagram(diagram)
|
|
295
|
+
return if diagram.name.to_s.empty?
|
|
296
|
+
|
|
297
|
+
render_block("diagram #{diagram.name}") do
|
|
298
|
+
member { render_definition(diagram.definition) } if definition_present?(diagram.definition)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# ---- attributes ----
|
|
303
|
+
|
|
304
|
+
def render_attribute(attribute)
|
|
305
|
+
return if attribute.name.to_s.empty?
|
|
306
|
+
|
|
307
|
+
if terse_attribute?(attribute)
|
|
308
|
+
line("attribute #{attribute.name}, #{attribute.type}")
|
|
309
|
+
elsif naked_attribute?(attribute)
|
|
310
|
+
line("#{visibility_symbol(attribute.visibility)}#{attribute_name_text(attribute.name)}")
|
|
311
|
+
elsif bare_attribute?(attribute)
|
|
312
|
+
render_bare_attribute(attribute)
|
|
313
|
+
else
|
|
314
|
+
render_attribute_block(attribute)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def terse_attribute?(attribute)
|
|
319
|
+
public_visibility?(attribute.visibility) &&
|
|
320
|
+
bare_name?(attribute.name) &&
|
|
321
|
+
attribute.type.to_s != '' &&
|
|
322
|
+
attribute_cardinality(attribute).nil? &&
|
|
323
|
+
Array(attribute.value_set).empty? &&
|
|
324
|
+
Array(attribute.properties).empty? &&
|
|
325
|
+
attribute.pattern.nil? && attribute.default.nil? &&
|
|
326
|
+
Array(attribute.stereotype).empty? && !attribute.literal &&
|
|
327
|
+
!definition_present?(attribute.definition)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def naked_attribute?(attribute)
|
|
331
|
+
public_visibility?(attribute.visibility) &&
|
|
332
|
+
attribute.type.to_s.empty? &&
|
|
333
|
+
attribute_cardinality(attribute).nil? &&
|
|
334
|
+
Array(attribute.value_set).empty? &&
|
|
335
|
+
Array(attribute.properties).empty? &&
|
|
336
|
+
attribute.pattern.nil? && attribute.default.nil? &&
|
|
337
|
+
Array(attribute.stereotype).empty? && !attribute.literal &&
|
|
338
|
+
!definition_present?(attribute.definition)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def bare_attribute?(attribute)
|
|
342
|
+
!public_visibility?(attribute.visibility) ||
|
|
343
|
+
!bare_name?(attribute.name) ||
|
|
344
|
+
Array(attribute.stereotype).any? || attribute.literal ||
|
|
345
|
+
definition_present?(attribute.definition)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def render_bare_attribute(attribute)
|
|
349
|
+
header = "#{visibility_symbol(attribute.visibility)}#{attribute_name_text(attribute.name)}"
|
|
350
|
+
type = attribute_type_text(attribute)
|
|
351
|
+
header += ": #{type}" if type != ''
|
|
352
|
+
cardinality = attribute_cardinality(attribute)
|
|
353
|
+
header += " [#{cardinality}]" if cardinality
|
|
354
|
+
|
|
355
|
+
if definition_present?(attribute.definition)
|
|
356
|
+
render_block(header) { render_definition(attribute.definition) }
|
|
357
|
+
else
|
|
358
|
+
line(header)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def bare_name?(name)
|
|
363
|
+
name.to_s.match?(%r{\A[A-Za-z0-9_/+]+\z})
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def attribute_name_text(name)
|
|
367
|
+
bare_name?(name) ? name.to_s : quote_always(name)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def attribute_type_text(attribute)
|
|
371
|
+
return '' if attribute.type.to_s.empty?
|
|
372
|
+
|
|
373
|
+
stereotypes = Array(attribute.stereotype)
|
|
374
|
+
type = attribute.literal ? quote_always(attribute.type) : attribute.type.to_s
|
|
375
|
+
stereotypes.any? ? "<<#{stereotypes.first}>> #{type}" : type
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def render_attribute_block(attribute)
|
|
379
|
+
render_block("attribute #{attribute_name_text(attribute.name)}") do
|
|
380
|
+
line("type #{attribute.type}") if attribute.type.to_s != ''
|
|
381
|
+
cardinality = attribute_cardinality(attribute)
|
|
382
|
+
line("cardinality #{cardinality}") if cardinality
|
|
383
|
+
if Array(attribute.value_set).any?
|
|
384
|
+
values = attribute.value_set.map { |value| quote_scalar(value) }.join(', ')
|
|
385
|
+
line("values { #{values} }")
|
|
386
|
+
end
|
|
387
|
+
line("default #{render_data_value(attribute.default)}") if attribute.default
|
|
388
|
+
|
|
389
|
+
properties = Array(attribute.properties)
|
|
390
|
+
defaults, others = properties.partition { |property| property.name == 'default' }
|
|
391
|
+
defaults.each { |property| line(property_line(property)) }
|
|
392
|
+
line("pattern /#{attribute.pattern}/") if attribute.pattern
|
|
393
|
+
others.sort_by { |property| property.name.to_s }
|
|
394
|
+
.each { |property| line(property_line(property)) }
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def property_line(property)
|
|
399
|
+
"#{property.name} = #{render_data_value(unwrap(property.value))}"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def render_operation(operation)
|
|
403
|
+
parameters = Array(operation.owned_parameter).map do |parameter|
|
|
404
|
+
operation_parameter(parameter)
|
|
405
|
+
end.join(', ')
|
|
406
|
+
text = "#{visibility_symbol(operation.visibility)}#{operation.name}(#{parameters})"
|
|
407
|
+
text += ": #{operation.return_type}" if operation.return_type.to_s != ''
|
|
408
|
+
stereotypes = Array(operation.stereotype)
|
|
409
|
+
text += " { #{stereotypes.join(', ')} }" if stereotypes.any?
|
|
410
|
+
line(text)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def operation_parameter(parameter)
|
|
414
|
+
parts = []
|
|
415
|
+
direction = parameter.direction.to_s
|
|
416
|
+
parts << direction if !direction.empty? && direction != 'in'
|
|
417
|
+
parts << parameter.name
|
|
418
|
+
parts << ": #{parameter.type}" if parameter.type.to_s != ''
|
|
419
|
+
parts << "= #{render_data_value(parameter.default)}" unless parameter.default.nil?
|
|
420
|
+
parts.join(' ')
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def render_string_format(format)
|
|
424
|
+
render_block("string_format #{quote_scalar(format.format)}") do
|
|
425
|
+
line("artifact #{quote_scalar(format.artifact)}") if format.artifact
|
|
426
|
+
line("root #{quote_scalar(format.root)}") if format.root
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# ---- instances ----
|
|
431
|
+
|
|
432
|
+
def render_instance_collection(collection)
|
|
433
|
+
name = collection.name.to_s.empty? ? '' : " #{quote_scalar(collection.name)}"
|
|
434
|
+
render_block("instances#{name}") do
|
|
435
|
+
Array(collection.imports).each { |entry| member { render_import(entry) } }
|
|
436
|
+
Array(collection.collections).each { |entry| member { render_collection_decl(entry) } }
|
|
437
|
+
Array(collection.exports).each { |entry| member { render_export(entry) } }
|
|
438
|
+
Array(collection.instances).each { |entry| member { render_instance(entry) } }
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def render_import(entry)
|
|
443
|
+
render_block('import') do
|
|
444
|
+
member do
|
|
445
|
+
render_block("#{entry.format_type} #{quote_always(entry.file)}") do
|
|
446
|
+
Array(entry.attributes).each do |attribute|
|
|
447
|
+
member { render_instance_attribute(attribute) }
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def render_export(entry)
|
|
455
|
+
render_block('export') do
|
|
456
|
+
member do
|
|
457
|
+
render_block("format #{entry.format_type}") do
|
|
458
|
+
Array(entry.attributes).each do |attribute|
|
|
459
|
+
member { render_instance_attribute(attribute) }
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def render_collection_decl(collection)
|
|
467
|
+
render_block("collection #{quote_always(collection.name)}") do
|
|
468
|
+
if Array(collection.includes).any?
|
|
469
|
+
values = collection.includes.map { |value| quote_always(value) }.join(', ')
|
|
470
|
+
member { line("includes [#{values}]") }
|
|
471
|
+
end
|
|
472
|
+
next if Array(collection.validations).empty?
|
|
473
|
+
|
|
474
|
+
member do
|
|
475
|
+
render_block('validation') do
|
|
476
|
+
Array(collection.validations).each do |condition|
|
|
477
|
+
line("condition #{quote_always(condition)}")
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def render_instance(instance, prefix = '')
|
|
485
|
+
render_block("#{prefix}#{instance_header(instance)}") do
|
|
486
|
+
member { line("isa #{quote_scalar(instance.isa)}") } if instance.isa.to_s != ''
|
|
487
|
+
if Array(instance.template).any?
|
|
488
|
+
member { render_template(instance.template) }
|
|
489
|
+
else
|
|
490
|
+
Array(instance.attributes).each do |attribute|
|
|
491
|
+
member { render_instance_attribute(attribute) }
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
member { render_instance(instance.instance) } if instance.instance
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def instance_header(instance)
|
|
499
|
+
label =
|
|
500
|
+
if instance.name.to_s.empty?
|
|
501
|
+
instance.type.to_s.empty? ? 'instance' : "instance #{instance.type}"
|
|
502
|
+
elsif instance.type.to_s.empty?
|
|
503
|
+
"instance #{quote_always(instance.name)}"
|
|
504
|
+
else
|
|
505
|
+
"#{instance.type} #{quote_always(instance.name)}"
|
|
506
|
+
end
|
|
507
|
+
label += " extends #{quote_always(instance.parent)}" if instance.parent.to_s != ''
|
|
508
|
+
label
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def render_instance_attribute(attribute)
|
|
512
|
+
if attribute.name.to_s.empty?
|
|
513
|
+
render_definition(attribute.definition, force_block: true) if definition_present?(attribute.definition)
|
|
514
|
+
return
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# `# text` inside an instance body parses as an attribute named
|
|
518
|
+
# Comment with no type. Emitting it as a normal assignment would
|
|
519
|
+
# reintroduce a String type on the next parse.
|
|
520
|
+
if attribute.name == 'Comment' && attribute.type.to_s.empty?
|
|
521
|
+
line("# #{unwrap(attribute.value)}")
|
|
522
|
+
return
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
if Array(attribute.instances).any?
|
|
526
|
+
render_instance_values(attribute)
|
|
527
|
+
else
|
|
528
|
+
line("#{assignment_prefix(attribute)}#{render_data_value(unwrap(attribute.value))}")
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# `name += value` extends an inherited value; the grammar stores
|
|
533
|
+
# that as the `extended` flag.
|
|
534
|
+
def assignment_prefix(attribute)
|
|
535
|
+
marker = attribute.extended ? '+' : ''
|
|
536
|
+
"#{quote_scalar(attribute.name)} #{marker}= "
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def render_instance_values(attribute)
|
|
540
|
+
values = Array(attribute.instances)
|
|
541
|
+
if values.one?
|
|
542
|
+
render_instance(values.first, assignment_prefix(attribute))
|
|
543
|
+
return
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
line("#{assignment_prefix(attribute)}[")
|
|
547
|
+
@level += 1
|
|
548
|
+
values.each { |instance| render_instance(instance) }
|
|
549
|
+
@level -= 1
|
|
550
|
+
line(']')
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def render_template(entries)
|
|
554
|
+
render_block('template') do
|
|
555
|
+
Array(entries).each do |entry|
|
|
556
|
+
member { render_instance_attribute(entry) }
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# ---- values ----
|
|
562
|
+
|
|
563
|
+
def render_data_value(value)
|
|
564
|
+
value = unwrap(value)
|
|
565
|
+
case value
|
|
566
|
+
when nil then 'null'
|
|
567
|
+
when Float then number_scalar(value)
|
|
568
|
+
when true, false, Integer then value.to_s
|
|
569
|
+
when String then quote_always(value)
|
|
570
|
+
when Array
|
|
571
|
+
"[#{value.map { |item| render_data_value(item) }.join(', ')}]"
|
|
572
|
+
when Hash
|
|
573
|
+
reference = value[:reference] || value['reference']
|
|
574
|
+
return "ref:(#{reference})" if reference
|
|
575
|
+
|
|
576
|
+
pairs = value.map do |key, item|
|
|
577
|
+
"#{quote_scalar(key)} = #{render_data_value(item)}"
|
|
578
|
+
end
|
|
579
|
+
"{ #{pairs.join(' ')} }"
|
|
580
|
+
else
|
|
581
|
+
quote_always(value.to_s)
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def unwrap(value)
|
|
586
|
+
value = value.value while value.is_a?(Lutaml::Lml::LiteralValue)
|
|
587
|
+
value
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def cardinality_token(cardinality)
|
|
591
|
+
min = cardinality.min.to_s
|
|
592
|
+
max = cardinality.max.to_s
|
|
593
|
+
max = 'n' if max == '*'
|
|
594
|
+
max.empty? ? min : "#{min}..#{max}"
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def attribute_cardinality(attribute)
|
|
598
|
+
cardinality = attribute.cardinality
|
|
599
|
+
return nil if cardinality.nil?
|
|
600
|
+
|
|
601
|
+
token = cardinality_token(cardinality)
|
|
602
|
+
token == '0..1' ? nil : token
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# ---- definitions and comments ----
|
|
606
|
+
|
|
607
|
+
def definition_present?(definition)
|
|
608
|
+
text = definition_text(definition)
|
|
609
|
+
!(text.nil? || text.empty?)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def definition_text(definition)
|
|
613
|
+
definition&.to_s
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def render_definition(definition, force_block: false)
|
|
617
|
+
text = definition_text(definition)
|
|
618
|
+
return if text.nil? || text.empty?
|
|
619
|
+
|
|
620
|
+
if force_block || text.include?("\n")
|
|
621
|
+
render_definition_block(text)
|
|
622
|
+
else
|
|
623
|
+
line("definition #{quote_always(text)}")
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def render_definition_block(text)
|
|
628
|
+
line('definition {')
|
|
629
|
+
@level += 1
|
|
630
|
+
text.split("\n", -1).each do |part|
|
|
631
|
+
part.empty? ? (@lines << '') : line(escape_braces(part))
|
|
632
|
+
end
|
|
633
|
+
@level -= 1
|
|
634
|
+
trim_trailing_blanks
|
|
635
|
+
line('}')
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def escape_braces(text)
|
|
639
|
+
text.gsub(/[{}]/) { |brace| "\\#{brace}" }
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def render_comments(comments)
|
|
643
|
+
Array(comments).each { |comment| member { render_comment(comment) } }
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def render_comment(comment)
|
|
647
|
+
text = comment.to_s
|
|
648
|
+
if text.include?("\n")
|
|
649
|
+
line("*|#{text}|*")
|
|
650
|
+
elsif text.empty?
|
|
651
|
+
line('**')
|
|
652
|
+
else
|
|
653
|
+
line("** #{text}")
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# ---- headers ----
|
|
658
|
+
|
|
659
|
+
def declaration_visibility(visibility)
|
|
660
|
+
word = visibility.to_s
|
|
661
|
+
VISIBILITY_WORDS.include?(word) ? "#{word} " : ''
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def visibility_symbol(visibility)
|
|
665
|
+
VISIBILITY_SYMBOLS.fetch(visibility.to_s, '')
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def public_visibility?(visibility)
|
|
669
|
+
visibility.nil? || visibility.to_s.empty? || visibility.to_s == 'public'
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def stereotype_suffix(stereotypes)
|
|
673
|
+
values = Array(stereotypes)
|
|
674
|
+
values.empty? ? '' : " <<#{values.first}>>"
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# ---- layout ----
|
|
678
|
+
|
|
679
|
+
def render_collection(owner, collection)
|
|
680
|
+
Array(owner.public_send(collection)).each do |item|
|
|
681
|
+
member { yield item }
|
|
682
|
+
end
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def render_block(header)
|
|
686
|
+
line("#{header} {")
|
|
687
|
+
@level += 1
|
|
688
|
+
@bodies.push(@lines.length)
|
|
689
|
+
yield
|
|
690
|
+
@bodies.pop
|
|
691
|
+
@level -= 1
|
|
692
|
+
trim_trailing_blanks
|
|
693
|
+
line('}')
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def member
|
|
697
|
+
blank_line if @bodies.any? && @lines.length > @bodies.last
|
|
698
|
+
yield
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def top
|
|
702
|
+
blank_line if @bodies.empty? || @lines.length > @bodies.last
|
|
703
|
+
yield
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def line(text)
|
|
707
|
+
@lines << "#{' ' * @level}#{text}"
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def blank_line
|
|
711
|
+
@lines << '' unless @lines.empty? || @lines.last == ''
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def trim_trailing_blanks
|
|
715
|
+
@lines.pop while @lines.last == ''
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
end
|