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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41662922310b1ec54e30485a3ec783c99f385f5e80eb0e4a98277b72ac5a71dc
|
|
4
|
+
data.tar.gz: 2b6030ded9c8bb82941ceefdf5e1df9a8531af5a61888faee4ecbc23df0ca961
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94f5275ce72d247c2842a60b265bdc8980cf82b49ce45d1970b606a0a9329505776235efb2df3247165b40cd5aaccc977b9aadcf77eb4ad044119ec71e4ea0a7
|
|
7
|
+
data.tar.gz: bfe8561322f20e76736e00a6c67ab862207e344637097f270a1111c7a5edbc540f8137989858e8fe3584dbaded200fe150c35da83523a637d26523f9cc64f8e6
|
data/lib/lutaml/lml/cli.rb
CHANGED
|
@@ -37,7 +37,12 @@ module Lutaml
|
|
|
37
37
|
|
|
38
38
|
def initialize(*args)
|
|
39
39
|
super
|
|
40
|
-
@formatter =
|
|
40
|
+
@formatter = case options[:layout]
|
|
41
|
+
when 'elk'
|
|
42
|
+
::Lutaml::Formatter::Elk.new
|
|
43
|
+
else
|
|
44
|
+
::Lutaml::Formatter::Graphviz.new
|
|
45
|
+
end
|
|
41
46
|
@out_object = $stdout
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -71,6 +76,8 @@ module Lutaml
|
|
|
71
76
|
desc: 'Node attributes (key=value,key2=value2)'
|
|
72
77
|
method_option :all, type: :string, aliases: '-a',
|
|
73
78
|
desc: 'Set attributes for graph, edge, and node'
|
|
79
|
+
method_option :layout, type: :string, enum: %w[graphviz elk], default: 'graphviz',
|
|
80
|
+
desc: 'Layout engine (elk requires the elkrb gem)'
|
|
74
81
|
def generate(*paths)
|
|
75
82
|
assert_input_paths(paths)
|
|
76
83
|
|
|
@@ -186,13 +193,22 @@ module Lutaml
|
|
|
186
193
|
input_path = Pathname.new(path_string)
|
|
187
194
|
return report_file_error(input_path, 'File does not exist') unless input_path.exist?
|
|
188
195
|
|
|
189
|
-
parse_document(input_path)
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
document = parse_document(input_path)
|
|
197
|
+
violations = Lutaml::Lml::Validator.violations(document)
|
|
198
|
+
if violations.any?
|
|
199
|
+
report_file_error(input_path, violations_message(violations))
|
|
200
|
+
else
|
|
201
|
+
say "✓ #{input_path}", :green
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
192
204
|
rescue StandardError => e
|
|
193
205
|
report_file_error(input_path, e.message)
|
|
194
206
|
end
|
|
195
207
|
|
|
208
|
+
def violations_message(violations)
|
|
209
|
+
violations.map { |v| "[#{v.rule}] #{v.message}" }.join('; ')
|
|
210
|
+
end
|
|
211
|
+
|
|
196
212
|
def report_file_error(input_path, message)
|
|
197
213
|
say "✗ #{input_path}: #{message}", :red
|
|
198
214
|
"#{input_path}: #{message}"
|
|
@@ -15,18 +15,52 @@ module Lutaml
|
|
|
15
15
|
|
|
16
16
|
def process_attributes_array(obj)
|
|
17
17
|
return [] if obj.empty?
|
|
18
|
-
return obj.map { |item| process_attributes(item) } unless
|
|
18
|
+
return obj.map { |item| process_attributes(item) } unless named_attribute_entries?(obj)
|
|
19
|
+
return merge_named_entries(obj) unless single_key_hashes?(obj)
|
|
19
20
|
|
|
21
|
+
obj.each_with_object({}) { |item, hash| append_uniform_entry(hash, item) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def append_uniform_entry(hash, item)
|
|
25
|
+
hash[:properties] ||= []
|
|
26
|
+
if item.key?(:properties)
|
|
27
|
+
hash[:properties] << process_attributes(item[:properties])
|
|
28
|
+
else
|
|
29
|
+
hash.merge!(process_attributes(item))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Attribute-definition groups split across capture depths (e.g. the
|
|
34
|
+
# comma form: {name, type} beside a nested {value_set}) combine into
|
|
35
|
+
# one attribute entry. Key/value pair lists (import/export maps)
|
|
36
|
+
# carry :key and are never merged.
|
|
37
|
+
def named_attribute_entries?(obj)
|
|
38
|
+
obj.any? { |e| e.is_a?(Hash) && e.key?(:name) } &&
|
|
39
|
+
obj.all? { |e| e.is_a?(Hash) && !e.key?(:key) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
COLLECTION_KEYS = %i[properties attributes instances].freeze
|
|
43
|
+
|
|
44
|
+
def merge_named_entries(obj)
|
|
20
45
|
obj.each_with_object({}) do |item, hash|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
hash[:properties] << process_attributes(item[:properties])
|
|
24
|
-
else
|
|
25
|
-
hash.merge!(process_attributes(item))
|
|
46
|
+
process_attributes(item).each do |key, value|
|
|
47
|
+
hash[key] = merge_attribute_value(hash[key], key, value)
|
|
26
48
|
end
|
|
27
49
|
end
|
|
28
50
|
end
|
|
29
51
|
|
|
52
|
+
def merge_attribute_value(existing, key, value)
|
|
53
|
+
return merge_collection_value(existing, value) if COLLECTION_KEYS.include?(key)
|
|
54
|
+
return existing + value if existing.is_a?(Array) && value.is_a?(Array)
|
|
55
|
+
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def merge_collection_value(existing, value)
|
|
60
|
+
current = existing.is_a?(Array) ? existing : (existing ? [existing] : [])
|
|
61
|
+
current + (value.is_a?(Array) ? value : [value])
|
|
62
|
+
end
|
|
63
|
+
|
|
30
64
|
def process_attributes_hash(obj)
|
|
31
65
|
result = extract_name_and_value(obj)
|
|
32
66
|
convert_instance_type(result)
|
|
@@ -34,18 +68,17 @@ module Lutaml
|
|
|
34
68
|
result[:literal] = true if obj.key?(:type_literal)
|
|
35
69
|
apply_nested_attributes(result, obj)
|
|
36
70
|
apply_remaining_keys(result, obj)
|
|
71
|
+
translate_values_argument(result)
|
|
37
72
|
result
|
|
38
73
|
end
|
|
39
74
|
|
|
40
|
-
private
|
|
41
|
-
|
|
42
75
|
def single_key_hashes?(arr)
|
|
43
76
|
arr.all? { |e| e.is_a?(Hash) && e.keys.size == 1 }
|
|
44
77
|
end
|
|
45
78
|
|
|
46
79
|
def extract_name_and_value(obj)
|
|
47
80
|
result = {}
|
|
48
|
-
result[:name] = obj[:key] if obj.key?(:key)
|
|
81
|
+
result[:name] = strip_argument_colon(obj[:key]) if obj.key?(:key)
|
|
49
82
|
|
|
50
83
|
if obj.key?(:comments)
|
|
51
84
|
result[:name] = 'Comment'
|
|
@@ -74,9 +107,16 @@ module Lutaml
|
|
|
74
107
|
obj.each do |key, value|
|
|
75
108
|
next if EXCLUDED_PASS_THROUGH_KEYS.include?(key)
|
|
76
109
|
|
|
110
|
+
value = strip_argument_colon(value) if key == :name
|
|
77
111
|
result[key] = value unless result.key?(key)
|
|
78
112
|
end
|
|
79
113
|
end
|
|
114
|
+
|
|
115
|
+
# `default: "Clear"` uses the colon separator (RS 3001 §Attribute);
|
|
116
|
+
# the colon lands in the captured name and must not.
|
|
117
|
+
def strip_argument_colon(name)
|
|
118
|
+
name.to_s.sub(/:\z/, "")
|
|
119
|
+
end
|
|
80
120
|
end
|
|
81
121
|
end
|
|
82
122
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
class DataProcessor
|
|
6
|
+
# RS 3001 §Value/§Attribute argument normalization: the canonical
|
|
7
|
+
# `value` construct and the attribute `values` argument.
|
|
8
|
+
module ValueDeclarationProcessing
|
|
9
|
+
# `value <name> { block }` is the canonical enum member; the bare
|
|
10
|
+
# identifier member is its documented shortcut. Normalize into the
|
|
11
|
+
# member shape the shortcut produces: the value block's `definition`
|
|
12
|
+
# lines become the member description and its attribute pairs become
|
|
13
|
+
# member payload properties.
|
|
14
|
+
def process_value_declaration(decl)
|
|
15
|
+
inner = { name: unwrap_string(decl[:value_name]) }
|
|
16
|
+
Array(decl[:members]).each do |member|
|
|
17
|
+
normalize_value_member(inner, member)
|
|
18
|
+
end
|
|
19
|
+
{ attributes: inner }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# `values <enum>` types the attribute with the named enum;
|
|
23
|
+
# `values { v1, v2 }` carries an inline value set that the compiler
|
|
24
|
+
# expands into an anonymous enum.
|
|
25
|
+
def translate_values_argument(result)
|
|
26
|
+
if result.key?(:value_set_ref)
|
|
27
|
+
ref = unwrap_string(result.delete(:value_set_ref))
|
|
28
|
+
result[:type] = ref unless result[:type]
|
|
29
|
+
end
|
|
30
|
+
return unless result.key?(:value_set)
|
|
31
|
+
|
|
32
|
+
result[:value_set] = Array(result[:value_set][:items]).map { |item| unwrap_data_value(item[:item]) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def normalize_value_member(inner, member)
|
|
38
|
+
if member.key?(:definition)
|
|
39
|
+
(inner[:attributes] ||= []) << { name: 'description', type: unwrap_string(member[:definition]) }
|
|
40
|
+
elsif member.key?(:instance)
|
|
41
|
+
(inner[:instances] ||= []).concat(Array(member[:instance]))
|
|
42
|
+
elsif member.key?(:attributes)
|
|
43
|
+
(inner[:properties] ||= []) << member[:attributes]
|
|
44
|
+
elsif member.key?(:key)
|
|
45
|
+
(inner[:properties] ||= []) << extract_name_and_value(member)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def unwrap_data_value(item)
|
|
50
|
+
return item unless item.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
item[:string] || item[:number] || item[:float] ||
|
|
53
|
+
item[:boolean] || item[:name] || item
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def unwrap_string(value)
|
|
57
|
+
value.is_a?(Hash) && value.key?(:string) ? value[:string] : value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -5,12 +5,14 @@ module Lutaml
|
|
|
5
5
|
class DataProcessor
|
|
6
6
|
autoload :ValueProcessing, "lutaml/lml/data_processor/value_processing"
|
|
7
7
|
autoload :AttributeProcessing, "lutaml/lml/data_processor/attribute_processing"
|
|
8
|
+
autoload :ValueDeclarationProcessing, "lutaml/lml/data_processor/value_declaration_processing"
|
|
8
9
|
autoload :InstanceProcessing, "lutaml/lml/data_processor/instance_processing"
|
|
9
10
|
autoload :CollectionProcessing, "lutaml/lml/data_processor/collection_processing"
|
|
10
11
|
autoload :ViewProcessing, "lutaml/lml/data_processor/view_processing"
|
|
11
12
|
|
|
12
13
|
include ValueProcessing
|
|
13
14
|
include AttributeProcessing
|
|
15
|
+
include ValueDeclarationProcessing
|
|
14
16
|
include InstanceProcessing
|
|
15
17
|
include CollectionProcessing
|
|
16
18
|
include ViewProcessing
|
|
@@ -20,6 +22,7 @@ module Lutaml
|
|
|
20
22
|
instances: :process_instances,
|
|
21
23
|
instance: :process_instance,
|
|
22
24
|
attributes: :process_attributes,
|
|
25
|
+
value_decl: :process_value_declaration,
|
|
23
26
|
show_list: :process_show_list,
|
|
24
27
|
hide_list: :process_hide_list
|
|
25
28
|
}.freeze
|
|
@@ -24,7 +24,9 @@ module Lutaml
|
|
|
24
24
|
mapping: ::Lutaml::Lml::Mapping,
|
|
25
25
|
string_format: ::Lutaml::Lml::StringFormat,
|
|
26
26
|
derived_field: ::Lutaml::Lml::DerivedField,
|
|
27
|
-
member_field: ::Lutaml::Lml::MemberField
|
|
27
|
+
member_field: ::Lutaml::Lml::MemberField,
|
|
28
|
+
namespace: ::Lutaml::Lml::Namespace,
|
|
29
|
+
serialization_mapping: ::Lutaml::Lml::SerializationMapping
|
|
28
30
|
}.freeze
|
|
29
31
|
|
|
30
32
|
attr_reader :registry
|
|
@@ -36,7 +38,7 @@ module Lutaml
|
|
|
36
38
|
FACTORY_KEYS = %i[
|
|
37
39
|
document package class enum data_type diagram view_import view_filter
|
|
38
40
|
attribute association operation constraint value cardinality mapping
|
|
39
|
-
string_format derived_field
|
|
41
|
+
string_format derived_field namespace serialization_mapping
|
|
40
42
|
].freeze
|
|
41
43
|
|
|
42
44
|
MEMBER_KEY_MAP = {
|
|
@@ -52,7 +54,9 @@ module Lutaml
|
|
|
52
54
|
constraints: :constraint,
|
|
53
55
|
values: :value,
|
|
54
56
|
mappings: :mapping,
|
|
55
|
-
member_fields: :member_field
|
|
57
|
+
member_fields: :member_field,
|
|
58
|
+
namespaces: :namespace,
|
|
59
|
+
serialization_mappings: :serialization_mapping
|
|
56
60
|
}.freeze
|
|
57
61
|
|
|
58
62
|
FACTORY_KEYS.each do |key|
|
|
@@ -68,6 +72,16 @@ module Lutaml
|
|
|
68
72
|
def set_model(model, hash)
|
|
69
73
|
hash = build_members(model, hash)
|
|
70
74
|
set_model_attributes(model, hash)
|
|
75
|
+
expand_class_specializer(model, hash)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The prefix specializer (`abstract class Foo`) expands to the
|
|
79
|
+
# canonical `abstract true` declaration (RS 3001 par. Declaration
|
|
80
|
+
# modifiers); the class-level modifier capture is not a block member.
|
|
81
|
+
def expand_class_specializer(model, hash)
|
|
82
|
+
return unless hash[:modifier].to_s == 'abstract' && model.class.attributes.key?(:is_abstract)
|
|
83
|
+
|
|
84
|
+
model.is_abstract = true
|
|
71
85
|
end
|
|
72
86
|
|
|
73
87
|
def set_model_attributes(model, hash)
|
|
@@ -79,12 +93,24 @@ module Lutaml
|
|
|
79
93
|
def build_members(model, hash)
|
|
80
94
|
members = hash.delete(:members)
|
|
81
95
|
members.to_a.each do |member_hash|
|
|
96
|
+
record_model_version(model, member_hash)
|
|
82
97
|
add_members(model, member_hash)
|
|
83
98
|
set_model_attributes(model, member_hash)
|
|
84
99
|
end
|
|
85
100
|
hash
|
|
86
101
|
end
|
|
87
102
|
|
|
103
|
+
# A `models <Name> version "x.y.z"` header travels with the block's
|
|
104
|
+
# member hash; the block itself flattens into the document, so the
|
|
105
|
+
# version is recorded on the document before the name is dropped.
|
|
106
|
+
def record_model_version(model, hash)
|
|
107
|
+
return unless model.class.attributes.key?(:model_versions)
|
|
108
|
+
return unless hash.key?(:version)
|
|
109
|
+
|
|
110
|
+
name = hash[:name].to_s
|
|
111
|
+
model.model_versions[name] = unquote(hash.delete(:version)).to_s
|
|
112
|
+
end
|
|
113
|
+
|
|
88
114
|
def apply_attribute(model, key, value)
|
|
89
115
|
return unless model.class.attributes.key?(key.to_sym)
|
|
90
116
|
|
|
@@ -102,9 +128,11 @@ module Lutaml
|
|
|
102
128
|
end
|
|
103
129
|
|
|
104
130
|
def add_members(model, hash)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
131
|
+
expand_declared_members(model, hash)
|
|
132
|
+
normalize_namespace_declaration(hash)
|
|
133
|
+
normalize_serialization_mapping(hash)
|
|
134
|
+
expand_instances_collection_name(hash)
|
|
135
|
+
expand_nested_members(model, hash)
|
|
108
136
|
MEMBER_KEY_MAP.each do |plural_key, singular_key|
|
|
109
137
|
data = hash.delete(plural_key)
|
|
110
138
|
next if data.nil?
|
|
@@ -116,6 +144,185 @@ module Lutaml
|
|
|
116
144
|
remap_filter_keys(hash, model)
|
|
117
145
|
end
|
|
118
146
|
|
|
147
|
+
# RS 3001 §Enums: an enum-level `values { v1, v2 }` shorthand
|
|
148
|
+
# expands into plain members.
|
|
149
|
+
def expand_value_set_declaration(model, hash)
|
|
150
|
+
decl = hash.delete(:value_set_decl) or return
|
|
151
|
+
Array(decl[:items]).each do |item|
|
|
152
|
+
name = unquote(item[:item])
|
|
153
|
+
model.attributes << TopElementAttribute.new(name: name)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# RS 3001 §Value: the canonical `value` construct normalizes into
|
|
158
|
+
# the same member shape the bare-identifier shortcut produces.
|
|
159
|
+
def expand_value_declaration(model, hash)
|
|
160
|
+
decl = hash.delete(:value_decl) or return
|
|
161
|
+
apply_attribute(model, :attributes, decl.fetch(:attributes))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Namespace declaration bodies arrive as keyword-line members
|
|
165
|
+
# ({uri: …}, {prefix: …}); flatten them onto the namespace hash.
|
|
166
|
+
def normalize_namespace_declaration(hash)
|
|
167
|
+
ns = hash[:namespaces] or return
|
|
168
|
+
Array(ns.delete(:members)).each do |pair|
|
|
169
|
+
pair.each { |key, value| ns[key] = unquote(value) }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Serialization mapping rules arrive keyed by their line construct
|
|
174
|
+
# ({map_element: {wire:, field:}}); normalize into plain MappingRule
|
|
175
|
+
# hashes so the model cast lands typed rules.
|
|
176
|
+
def normalize_serialization_mapping(hash)
|
|
177
|
+
mapping = hash[:serialization_mappings] or return
|
|
178
|
+
rules = Array(mapping[:rules])
|
|
179
|
+
return if rules.any? && rules.all? { |e| e.is_a?(Hash) && e.key?(:kind) }
|
|
180
|
+
|
|
181
|
+
rules = rules.filter_map do |entry|
|
|
182
|
+
next nil unless entry.is_a?(Hash)
|
|
183
|
+
|
|
184
|
+
next { kind: 'map', wire: unquote(entry[:wire]), field: unquote(entry[:to]) } if entry[:wire] && entry[:to]
|
|
185
|
+
|
|
186
|
+
first_key = entry.keys.first
|
|
187
|
+
kind = first_key.to_s
|
|
188
|
+
body = entry[first_key] || {}
|
|
189
|
+
unless body.is_a?(Hash)
|
|
190
|
+
kind_n = normalize_mapping_kind(kind)
|
|
191
|
+
next nil if body.to_s.empty?
|
|
192
|
+
|
|
193
|
+
term = unquote(body)
|
|
194
|
+
next { kind: kind_n, wire: term, field: term } if %w[element attribute map].include?(kind_n)
|
|
195
|
+
|
|
196
|
+
if kind_n == 'namespace'
|
|
197
|
+
mapping[:namespace_ref] = term
|
|
198
|
+
next nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
next { kind: kind_n, field: term }
|
|
202
|
+
end
|
|
203
|
+
if kind == 'element_name'
|
|
204
|
+
mapping[:element_name] = unquote(body[:element_name])
|
|
205
|
+
next nil
|
|
206
|
+
end
|
|
207
|
+
if kind == 'map_namespace'
|
|
208
|
+
mapping[:namespace_ref] = unquote(body[:namespace_ref])
|
|
209
|
+
next nil
|
|
210
|
+
end
|
|
211
|
+
normalized = { kind: normalize_mapping_kind(kind) }
|
|
212
|
+
wire = body[:wire] || body[:element_wire]
|
|
213
|
+
normalized[:wire] = unquote(wire) if wire
|
|
214
|
+
field = body[:field] || body[:to_field] || body[:element_field] ||
|
|
215
|
+
body[:attribute_field] || body[:to]
|
|
216
|
+
normalized[:field] = unquote(field) if field
|
|
217
|
+
normalized[:mode] = body[:mode] if body[:mode]
|
|
218
|
+
normalized[:namespace_ref] = unquote(body[:namespace_ref]) if body[:namespace_ref]
|
|
219
|
+
normalized
|
|
220
|
+
end
|
|
221
|
+
mapping[:rules] = rules
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def normalize_mapping_kind(kind)
|
|
225
|
+
{
|
|
226
|
+
'map_element' => 'element', 'map_attribute' => 'attribute',
|
|
227
|
+
'map_content' => 'content', 'map_namespace' => 'namespace',
|
|
228
|
+
'map_key' => 'key', 'map_value' => 'value', 'map' => 'map',
|
|
229
|
+
'legacy_map' => 'map'
|
|
230
|
+
}.fetch(kind, kind)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# A named instances collection (`instances "Glazes" { ... }`) arrives
|
|
234
|
+
# with its name as a sibling of the collection body; move it inside
|
|
235
|
+
# so the InstanceCollection carries it.
|
|
236
|
+
def expand_instances_collection_name(hash)
|
|
237
|
+
return unless hash.key?(:instances) && hash.key?(:name)
|
|
238
|
+
|
|
239
|
+
name = unquote(hash.delete(:name))
|
|
240
|
+
body = hash[:instances].is_a?(Hash) ? hash[:instances] : {}
|
|
241
|
+
body[:name] = name unless body.key?(:name)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The document root is a repetition of top-level constructs (RS 3001:
|
|
245
|
+
# documents interleave declarations), so models/packages blocks nest
|
|
246
|
+
# their members one level down; recurse into them.
|
|
247
|
+
def expand_nested_members(model, hash)
|
|
248
|
+
return unless hash.key?(:members)
|
|
249
|
+
|
|
250
|
+
members = hash.delete(:members)
|
|
251
|
+
members.to_a.each do |nested|
|
|
252
|
+
add_members(model, nested)
|
|
253
|
+
set_model_attributes(model, nested)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Declaration-level member hooks: single-line definitions, canonical
|
|
258
|
+
# `value` members, mapping sections, class declarations, enum
|
|
259
|
+
# member_fields and from_table all arrive as bare member hashes.
|
|
260
|
+
def expand_declared_members(model, hash)
|
|
261
|
+
expand_abstract_declaration(model, hash)
|
|
262
|
+
expand_definition_text(model, hash)
|
|
263
|
+
expand_value_declaration(model, hash)
|
|
264
|
+
expand_value_set_declaration(model, hash)
|
|
265
|
+
expand_mapping_section(model, hash)
|
|
266
|
+
expand_class_declarations(model, hash)
|
|
267
|
+
expand_enum_member_fields(model, hash)
|
|
268
|
+
expand_from_table(model, hash)
|
|
269
|
+
expand_operation_members(model, hash)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Operation members arrive with grammar capture names
|
|
273
|
+
# (`visibility_modifier`, `param_default`) and per-iteration
|
|
274
|
+
# stereotype hashes; normalize to the model's field names.
|
|
275
|
+
def expand_operation_members(model, hash)
|
|
276
|
+
ops = hash.delete(:operations)
|
|
277
|
+
return unless ops && model.class.attributes.key?(:operations)
|
|
278
|
+
|
|
279
|
+
Array(ops.is_a?(Hash) ? [ops] : ops).each do |op|
|
|
280
|
+
model.operations << build(:operation, normalize_operation_hash(op))
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def normalize_operation_hash(op)
|
|
285
|
+
op = op.dup
|
|
286
|
+
if (vis = op.delete(:visibility_modifier))
|
|
287
|
+
op[:visibility] = Transform::VISIBILITY_MAP.fetch(vis.to_s, 'public')
|
|
288
|
+
end
|
|
289
|
+
stereo = op[:stereotype]
|
|
290
|
+
if stereo
|
|
291
|
+
items = stereo.is_a?(Array) ? stereo : [stereo]
|
|
292
|
+
op[:stereotype] = items.map { |x| x.is_a?(Hash) ? x[:stereotype].to_s : x.to_s }
|
|
293
|
+
end
|
|
294
|
+
params = op[:owned_parameter]
|
|
295
|
+
if params
|
|
296
|
+
list = params.is_a?(Array) ? params : [params]
|
|
297
|
+
op[:owned_parameter] = list.map { |param| normalize_operation_param(param) }
|
|
298
|
+
end
|
|
299
|
+
op
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def normalize_operation_param(param)
|
|
303
|
+
param = param.dup
|
|
304
|
+
if (default = param.delete(:param_default))
|
|
305
|
+
param[:default] = default.is_a?(Hash) ? default[:string].to_s : default.to_s
|
|
306
|
+
end
|
|
307
|
+
param
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# RS 3001 §Definition single-line form (`definition "text"`) arrives
|
|
311
|
+
# as a quoted-string capture; unwrap it for the entity's definition.
|
|
312
|
+
# RS 3001 declaration modifiers: `abstract true` maps onto the
|
|
313
|
+
# is_abstract field (the visibility declaration flows through the
|
|
314
|
+
# generic attribute path).
|
|
315
|
+
def expand_abstract_declaration(model, hash)
|
|
316
|
+
value = hash.delete(:abstract)
|
|
317
|
+
model.is_abstract = true if value.to_s == 'true' && model.class.attributes.key?(:is_abstract)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def expand_definition_text(model, hash)
|
|
321
|
+
return unless hash.key?(:definition) && model.class.attributes.key?(:definition)
|
|
322
|
+
|
|
323
|
+
model.definition = unquote(hash.delete(:definition))
|
|
324
|
+
end
|
|
325
|
+
|
|
119
326
|
# A mapping section arrives as a bare member hash
|
|
120
327
|
# { format:, maps: [{ wire:, to: }] }; expand to one Mapping entry
|
|
121
328
|
# per map line, each carrying the format, and strip the section keys
|
|
@@ -158,6 +365,23 @@ module Lutaml
|
|
|
158
365
|
type: unquote(derived[:type]))
|
|
159
366
|
end
|
|
160
367
|
|
|
368
|
+
# L7a: from_table declares the enum's members as an artifact table
|
|
369
|
+
# { from_table_name:, from_table_artifact? } — expansion happens at
|
|
370
|
+
# compile time (deterministic materialization, no runtime data dep)
|
|
371
|
+
def expand_from_table(model, hash)
|
|
372
|
+
decl = hash.delete(:from_table_decl)
|
|
373
|
+
return unless decl && model.is_a?(Enum)
|
|
374
|
+
|
|
375
|
+
# the members repetition splits the declaration across iteration
|
|
376
|
+
# hashes (name part, artifact part) — merge the parts
|
|
377
|
+
parts = decl.is_a?(Array) ? decl : [decl]
|
|
378
|
+
decl = parts.each_with_object({}) do |part, acc|
|
|
379
|
+
acc.merge!(part) { |_key, _old, new_val| new_val }
|
|
380
|
+
end
|
|
381
|
+
model.from_table_name = unquote(decl[:from_table_name])
|
|
382
|
+
model.from_table_artifact = unquote(decl[:artifact_table]) if decl[:artifact_table]
|
|
383
|
+
end
|
|
384
|
+
|
|
161
385
|
def expand_enum_member_fields(model, hash)
|
|
162
386
|
decl = hash.delete(:member_fields_decl)
|
|
163
387
|
return unless decl && model.is_a?(Enum)
|
|
@@ -70,6 +70,8 @@ module Lutaml
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def format_value(prefix, key, value, indent)
|
|
73
|
+
return "#{prefix}#{key} = ref:(#{value[:reference]})" if reference_value?(value)
|
|
74
|
+
|
|
73
75
|
case value
|
|
74
76
|
when Array
|
|
75
77
|
format_array(prefix, key, value, indent)
|
|
@@ -84,11 +86,15 @@ module Lutaml
|
|
|
84
86
|
|
|
85
87
|
def format_array(prefix, key, items, indent)
|
|
86
88
|
elements = items.map do |item|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
format_nested_instance(item, indent + 1)
|
|
89
|
+
if reference_value?(item)
|
|
90
|
+
" ref:(#{item[:reference]})"
|
|
90
91
|
else
|
|
91
|
-
|
|
92
|
+
case item
|
|
93
|
+
when Hash
|
|
94
|
+
format_nested_instance(item, indent + 1)
|
|
95
|
+
else
|
|
96
|
+
" #{quote_value(item)}"
|
|
97
|
+
end
|
|
92
98
|
end
|
|
93
99
|
end
|
|
94
100
|
|
|
@@ -111,6 +117,13 @@ module Lutaml
|
|
|
111
117
|
"#{prefix}instance#{type_clause} {\n#{inner}\n#{prefix}}"
|
|
112
118
|
end
|
|
113
119
|
|
|
120
|
+
# RS 3001 par. Value type: a `reference` map is the wire form of a
|
|
121
|
+
# reference literal; it emits as `ref:(path)`, never as a nested
|
|
122
|
+
# instance.
|
|
123
|
+
def reference_value?(value)
|
|
124
|
+
value.is_a?(Hash) && value.keys == [:reference]
|
|
125
|
+
end
|
|
126
|
+
|
|
114
127
|
def quote_value(val)
|
|
115
128
|
return val.to_s if val.is_a?(Numeric) || val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
|
116
129
|
str = val.to_s
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Formatter
|
|
5
|
+
# ELK-layout diagram formatter: builds an Elkrb::Graph from the
|
|
6
|
+
# document, lays it out in-process (no external tools) and emits SVG,
|
|
7
|
+
# or converts the SVG to PS/EPS/EMF through vectory.
|
|
8
|
+
class Elk
|
|
9
|
+
VALID_TYPES = %i[svg ps eps emf].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :type
|
|
12
|
+
|
|
13
|
+
def initialize(_attributes = {})
|
|
14
|
+
@type = :svg
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def type=(value)
|
|
18
|
+
@type = value.to_sym
|
|
19
|
+
return if VALID_TYPES.include?(@type)
|
|
20
|
+
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"unsupported output type #{@type.inspect} (valid: #{VALID_TYPES.join(', ')})"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format(document)
|
|
26
|
+
graph = Lutaml::Layout::ElkGraphBuilder.new.build(document)
|
|
27
|
+
svg = Lutaml::Layout::ElkEngine.new(input: graph).render(:svg)
|
|
28
|
+
@type == :svg ? svg : convert(svg)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def convert(svg)
|
|
34
|
+
require 'vectory'
|
|
35
|
+
Vectory::Svg.new(svg).public_send(:"to_#{@type}").content
|
|
36
|
+
rescue LoadError
|
|
37
|
+
raise Error, 'The vectory gem is required for PS/EPS/EMF output: ' \
|
|
38
|
+
'add `gem "vectory"` to your Gemfile'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|