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
|
@@ -19,6 +19,8 @@ module Lutaml
|
|
|
19
19
|
attribute :definition, Lutaml::Lml::Types::TextType
|
|
20
20
|
attribute :association, :string
|
|
21
21
|
attribute :default, :string
|
|
22
|
+
attribute :value_set, :string, collection: true, default: -> { [] }
|
|
23
|
+
attribute :pattern, :string
|
|
22
24
|
|
|
23
25
|
# LML-specific attributes
|
|
24
26
|
attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
|
|
@@ -53,6 +55,8 @@ module Lutaml
|
|
|
53
55
|
map "definition", to: :definition
|
|
54
56
|
map "association", to: :association
|
|
55
57
|
map "default", to: :default
|
|
58
|
+
map "value_set", to: :value_set
|
|
59
|
+
map "pattern", to: :pattern
|
|
56
60
|
map "properties", to: :properties
|
|
57
61
|
map "value", to: :value, with: { to: :value_to, from: :value_from }
|
|
58
62
|
map "attributes", to: :attributes
|
|
@@ -79,6 +83,18 @@ module Lutaml
|
|
|
79
83
|
model.value = value
|
|
80
84
|
end
|
|
81
85
|
|
|
86
|
+
# RS 3001 par. Value type: the typed view of a reference value
|
|
87
|
+
# (`reference:(...)` / `ref:(...)`). Nil for every other value shape;
|
|
88
|
+
# for a list of references, the first reference.
|
|
89
|
+
def reference
|
|
90
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
91
|
+
cargo = literal.is_a?(::Array) ? literal.first : literal
|
|
92
|
+
cargo = cargo.value if cargo.is_a?(LiteralValue)
|
|
93
|
+
return nil unless cargo.is_a?(::Hash) && cargo[:reference]
|
|
94
|
+
|
|
95
|
+
Reference.new(path: cargo[:reference].to_s)
|
|
96
|
+
end
|
|
97
|
+
|
|
82
98
|
private
|
|
83
99
|
|
|
84
100
|
# A LiteralValue sits at the top level for a map literal, and inside the
|
|
@@ -20,12 +20,13 @@ module Lutaml
|
|
|
20
20
|
attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true
|
|
21
21
|
attribute :modifier, :string
|
|
22
22
|
attribute :constraints, "Lutaml::Lml::Constraint", collection: true
|
|
23
|
-
attribute :operations, "Lutaml::Lml::Operation", collection: true
|
|
23
|
+
attribute :operations, "Lutaml::Lml::Operation", collection: true, default: -> { [] }
|
|
24
24
|
attribute :data_types, "Lutaml::Lml::DataType", collection: true
|
|
25
25
|
attribute :associations, "Lutaml::Lml::Association", collection: true
|
|
26
26
|
attribute :wire_mappings, "Lutaml::Lml::Mapping", collection: true, default: -> { [] }
|
|
27
27
|
attribute :string_formats, "Lutaml::Lml::StringFormat", collection: true, default: -> { [] }
|
|
28
28
|
attribute :derived_fields, "Lutaml::Lml::DerivedField", collection: true, default: -> { [] }
|
|
29
|
+
attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
|
|
29
30
|
|
|
30
31
|
# LML-specific
|
|
31
32
|
attribute :parent_class, :string
|
|
@@ -21,16 +21,20 @@ module Lutaml
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
+
# Comments (`//`, `/* */`) are consumed by the grammar's trivia
|
|
25
|
+
# skip (parsanol-ruby#134); only include directives are expanded
|
|
26
|
+
# here. Line structure is preserved so parse-error positions
|
|
27
|
+
# match the source.
|
|
24
28
|
def expand_lines(text, base_dir, chain)
|
|
29
|
+
validate_block_comments(text)
|
|
25
30
|
text.split(/\r?\n/)
|
|
26
31
|
.flat_map { |line| expand_line(line, base_dir, chain) }
|
|
27
32
|
.join("\n")
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def expand_line(line, base_dir, chain)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return [cleaned] unless path
|
|
36
|
+
path = include_path(line, base_dir)
|
|
37
|
+
return [line] unless path
|
|
34
38
|
|
|
35
39
|
raise Error, "circular include detected: #{path}" if chain.include?(path)
|
|
36
40
|
|
|
@@ -38,20 +42,57 @@ module Lutaml
|
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
def include_path(line, base_dir)
|
|
41
|
-
match = line.match(/^\s*include\s+(.+)
|
|
45
|
+
match = line.match(/^\s*include\s+(.+)$/)
|
|
42
46
|
return nil unless match
|
|
43
47
|
|
|
44
|
-
File.expand_path(match[1].strip, base_dir)
|
|
48
|
+
File.expand_path(trim_trailing_comment(match[1].strip), base_dir)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def trim_trailing_comment(path)
|
|
52
|
+
path.sub(%r{\s+(//|/\*).*}, '')
|
|
45
53
|
end
|
|
46
54
|
|
|
47
55
|
def read_included(path)
|
|
48
|
-
File.read(path)
|
|
56
|
+
File.read(path, encoding: 'UTF-8')
|
|
49
57
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
50
58
|
raise Error, "cannot read include #{path}: #{e.message}"
|
|
51
59
|
end
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
# Quote-aware scan: a `/*` opened outside a string literal or a
|
|
62
|
+
# line comment must close before end of input. The grammar's
|
|
63
|
+
# trivia would otherwise reject the text with an unrelated
|
|
64
|
+
# expected-token error.
|
|
65
|
+
def validate_block_comments(text)
|
|
66
|
+
quote = nil
|
|
67
|
+
pos = 0
|
|
68
|
+
open_index = nil
|
|
69
|
+
while pos < text.length
|
|
70
|
+
ch = text[pos]
|
|
71
|
+
if quote
|
|
72
|
+
if ch == '\\'
|
|
73
|
+
pos += 2
|
|
74
|
+
next
|
|
75
|
+
end
|
|
76
|
+
quote = nil if ch == quote
|
|
77
|
+
elsif open_index
|
|
78
|
+
if text[pos, 2] == '*/'
|
|
79
|
+
open_index = nil
|
|
80
|
+
pos += 1
|
|
81
|
+
end
|
|
82
|
+
elsif ['"', "'"].include?(ch)
|
|
83
|
+
quote = ch
|
|
84
|
+
elsif text[pos, 2] == '//'
|
|
85
|
+
pos = text.index("\n", pos) || text.length
|
|
86
|
+
next
|
|
87
|
+
elsif text[pos, 2] == '/*'
|
|
88
|
+
open_index = pos
|
|
89
|
+
end
|
|
90
|
+
pos += 1
|
|
91
|
+
end
|
|
92
|
+
return unless open_index
|
|
93
|
+
|
|
94
|
+
line = text[0...open_index].count("\n") + 1
|
|
95
|
+
raise Error, "unterminated block comment at line #{line}"
|
|
55
96
|
end
|
|
56
97
|
end
|
|
57
98
|
end
|
data/lib/lutaml/lml/transform.rb
CHANGED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# RS 3001 §Validation rules evaluated over a parsed document:
|
|
6
|
+
# class constraints (unique names, unique attribute names), attribute
|
|
7
|
+
# constraints (mandatory cardinality-1 attributes populated, values
|
|
8
|
+
# matching declared types) and reference validity (ref: paths resolve
|
|
9
|
+
# to instances and are acyclic).
|
|
10
|
+
class Validator
|
|
11
|
+
Violation = Struct.new(:rule, :message, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
PRIMITIVE_CONFORMITY = {
|
|
14
|
+
'Integer' => /\A-?\d+\z/,
|
|
15
|
+
'Number' => /\A-?\d+\z/,
|
|
16
|
+
'Float' => /\A-?\d+\.\d+\z/,
|
|
17
|
+
'Boolean' => /\A(true|false)\z/
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
COLLECTION_KEYS = %i[properties attributes instances].freeze
|
|
21
|
+
|
|
22
|
+
def self.violations(document)
|
|
23
|
+
new(document).violations
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(document)
|
|
27
|
+
@document = document
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def violations
|
|
31
|
+
unique_name_violations +
|
|
32
|
+
unique_attribute_violations +
|
|
33
|
+
instance_violations +
|
|
34
|
+
circular_reference_violations
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# RS 3001 scopes definitions to packages: uniqueness is per scope
|
|
38
|
+
# (the document root and each package, recursively), not global.
|
|
39
|
+
def definition_scopes
|
|
40
|
+
scopes = [[nil, document_definitions]]
|
|
41
|
+
Array(@document.packages).each { |pkg| collect_package_scopes(pkg, scopes) }
|
|
42
|
+
scopes
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def document_definitions
|
|
46
|
+
[
|
|
47
|
+
*Array(@document.classes),
|
|
48
|
+
*Array(@document.enums),
|
|
49
|
+
*Array(@document.data_types)
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def collect_package_scopes(pkg, scopes, parent = nil)
|
|
54
|
+
path = parent ? "#{parent}.#{pkg.name}" : pkg.name.to_s
|
|
55
|
+
scopes << [path, [*Array(pkg.classes), *Array(pkg.enums), *Array(pkg.data_types)]]
|
|
56
|
+
Array(pkg.packages).each { |nested| collect_package_scopes(nested, scopes, path) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def all_definitions
|
|
62
|
+
@all_definitions ||= begin
|
|
63
|
+
pairs = []
|
|
64
|
+
Array(@document.classes).each { |c| pairs << [c.name.to_s, c] }
|
|
65
|
+
Array(@document.enums).each { |e| pairs << [e.name.to_s, e] }
|
|
66
|
+
Array(@document.data_types).each { |d| pairs << [d.name.to_s, d] }
|
|
67
|
+
Array(@document.packages).each { |pkg| collect_package_definitions(pkg, pairs) }
|
|
68
|
+
pairs
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def collect_package_definitions(pkg, pairs)
|
|
73
|
+
Array(pkg.classes).each { |c| pairs << [c.name.to_s, c] }
|
|
74
|
+
Array(pkg.enums).each { |e| pairs << [e.name.to_s, e] }
|
|
75
|
+
Array(pkg.data_types).each { |d| pairs << [d.name.to_s, d] }
|
|
76
|
+
Array(pkg.packages).each { |nested| collect_package_definitions(nested, pairs) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def type_definitions
|
|
80
|
+
@type_definitions ||= all_definitions.to_h
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def unique_name_violations
|
|
84
|
+
definition_scopes.flat_map do |scope, definitions|
|
|
85
|
+
scope_label = scope ? "package '#{scope}'" : 'the document root'
|
|
86
|
+
definitions.map { |d| d.name.to_s }
|
|
87
|
+
.tally
|
|
88
|
+
.filter_map do |name, count|
|
|
89
|
+
next if count == 1
|
|
90
|
+
|
|
91
|
+
Violation.new(
|
|
92
|
+
rule: 'unique_names',
|
|
93
|
+
message: "'#{name}' is defined #{count} times in #{scope_label}"
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def unique_attribute_violations
|
|
100
|
+
type_definitions.filter_map do |name, definition|
|
|
101
|
+
dupes = attribute_names(definition)
|
|
102
|
+
.tally.select { |_attr, count| count > 1 }.keys
|
|
103
|
+
next if dupes.empty?
|
|
104
|
+
|
|
105
|
+
Violation.new(
|
|
106
|
+
rule: 'unique_attribute_names',
|
|
107
|
+
message: "#{name}: duplicate attribute(s) #{dupes.inspect}"
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def attribute_names(definition)
|
|
113
|
+
Array(definition.attributes).map(&:name).map(&:to_s)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def instance_violations
|
|
117
|
+
instances.flat_map do |instance|
|
|
118
|
+
mandatory_violations(instance) +
|
|
119
|
+
type_conformity_violations(instance) +
|
|
120
|
+
values_set_violations(instance) +
|
|
121
|
+
unknown_attribute_violations(instance) +
|
|
122
|
+
reference_violations(instance)
|
|
123
|
+
end.compact
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# RS 3001 par. Attribute: a `values` declaration restricts an
|
|
127
|
+
# attribute to its declared set.
|
|
128
|
+
def values_set_violations(instance)
|
|
129
|
+
definition = type_definitions[definition_type(instance)]
|
|
130
|
+
return [] unless definition.is_a?(UmlClass) || definition.is_a?(DataType)
|
|
131
|
+
|
|
132
|
+
instance_attribute_pairs(instance).filter_map do |name, value|
|
|
133
|
+
attr = Array(definition.attributes).find { |a| a.name.to_s == name }
|
|
134
|
+
next unless attr && !attr.value_set.empty?
|
|
135
|
+
|
|
136
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
137
|
+
next if attr.value_set.map(&:to_s).include?(literal.to_s)
|
|
138
|
+
|
|
139
|
+
Violation.new(
|
|
140
|
+
rule: 'values_set_membership',
|
|
141
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
142
|
+
"must be one of #{attr.value_set.inspect}, got #{literal.to_s.inspect}"
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def unknown_attribute_violations(instance)
|
|
148
|
+
definition = type_definitions[definition_type(instance)]
|
|
149
|
+
return [] unless definition.is_a?(UmlClass) || definition.is_a?(DataType)
|
|
150
|
+
|
|
151
|
+
declared = attribute_names(definition)
|
|
152
|
+
instance_attribute_pairs(instance)
|
|
153
|
+
.reject { |name, _value| declared.include?(name) }
|
|
154
|
+
.map do |name, _value|
|
|
155
|
+
Violation.new(
|
|
156
|
+
rule: 'unknown_attributes',
|
|
157
|
+
message: "instance '#{instance_name(instance)}' (#{definition_type(instance)}): " \
|
|
158
|
+
"unknown attribute '#{name}'"
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def instances
|
|
164
|
+
Array(@document.instances&.instances)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def mandatory_violations(instance)
|
|
168
|
+
definition = type_definitions[definition_type(instance)]
|
|
169
|
+
return [] unless definition.is_a?(UmlClass) || definition.is_a?(DataType)
|
|
170
|
+
|
|
171
|
+
definition.attributes.filter_map do |attr|
|
|
172
|
+
next unless mandatory?(attr)
|
|
173
|
+
next if populated?(instance, attr.name.to_s)
|
|
174
|
+
|
|
175
|
+
Violation.new(
|
|
176
|
+
rule: 'mandatory_attributes',
|
|
177
|
+
message: "instance '#{instance_name(instance)}' (#{definition_type(instance)}): " \
|
|
178
|
+
"mandatory attribute '#{attr.name}' is not populated"
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def mandatory?(attr)
|
|
184
|
+
attr.cardinality && attr.cardinality.min == '1'
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def type_conformity_violations(instance)
|
|
190
|
+
definition = type_definitions[definition_type(instance)]
|
|
191
|
+
return [] unless definition.respond_to?(:attributes)
|
|
192
|
+
|
|
193
|
+
instance_attribute_pairs(instance).filter_map do |name, value|
|
|
194
|
+
attr = Array(definition.attributes).find { |a| a.name.to_s == name }
|
|
195
|
+
next unless attr
|
|
196
|
+
|
|
197
|
+
conformity_violation(instance, name, attr, value)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def conformity_violation(instance, name, attr, value)
|
|
202
|
+
pattern = PRIMITIVE_CONFORMITY[attr.type.to_s]
|
|
203
|
+
return unless pattern
|
|
204
|
+
return if pattern.match?(value.to_s)
|
|
205
|
+
|
|
206
|
+
Violation.new(
|
|
207
|
+
rule: 'type_conformity',
|
|
208
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
209
|
+
"expects #{attr.type}, got #{value.to_s.inspect}"
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def reference_violations(instance)
|
|
214
|
+
reference_values(instance).filter_map do |name, path|
|
|
215
|
+
segments = path.to_s.split('.')
|
|
216
|
+
unless resolvable_reference?(segments)
|
|
217
|
+
next Violation.new(
|
|
218
|
+
rule: 'reference_validity',
|
|
219
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
220
|
+
"references unknown path '#{path}'"
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
attribute_violation(instance, name, path, segments.first)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# RS 3001 reference paths: `instance` / `instance.attribute` /
|
|
229
|
+
# `collection.instance`.
|
|
230
|
+
def resolvable_reference?(segments)
|
|
231
|
+
case segments.length
|
|
232
|
+
when 1 then instance_names.include?(segments.first)
|
|
233
|
+
when 2 then instance_names.include?(segments.first) ||
|
|
234
|
+
collection_names.include?(segments.first)
|
|
235
|
+
else false
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def collection_names
|
|
240
|
+
@collection_names ||= [@document.instances&.name].compact
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def attribute_violation(instance, _name, path, target)
|
|
244
|
+
segment = path.to_s.split('.')[1]
|
|
245
|
+
target_instance = instances.find { |i| instance_name(i) == target }
|
|
246
|
+
return if segment.nil? || target_instance.nil? ||
|
|
247
|
+
instance_attribute(target_instance, segment)
|
|
248
|
+
|
|
249
|
+
Violation.new(
|
|
250
|
+
rule: 'reference_validity',
|
|
251
|
+
message: "instance '#{instance_name(instance)}': reference '#{path}' " \
|
|
252
|
+
"does not resolve (no attribute '#{segment}' on '#{target}')"
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def instance_attribute_pairs(instance)
|
|
257
|
+
pairs = []
|
|
258
|
+
instance.each_attribute do |name, value, instances|
|
|
259
|
+
pairs << [name.to_s, value_for(value, instances)]
|
|
260
|
+
end
|
|
261
|
+
pairs
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def instance_attribute(instance, attr_name)
|
|
265
|
+
instance_attribute_pairs(instance).find { |name, _value| name == attr_name }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def value_for(value, instances)
|
|
269
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
270
|
+
return instances.first if literal.is_a?(Hash) && literal[:instances]
|
|
271
|
+
|
|
272
|
+
literal
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def populated?(instance, attr_name)
|
|
276
|
+
pair = instance_attribute(instance, attr_name)
|
|
277
|
+
return false unless pair
|
|
278
|
+
|
|
279
|
+
value = pair[1]
|
|
280
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
281
|
+
!literal.to_s.empty?
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def reference_values(instance)
|
|
285
|
+
Array(instance.attributes).filter_map do |attr|
|
|
286
|
+
ref = attr.reference
|
|
287
|
+
next unless ref
|
|
288
|
+
|
|
289
|
+
[attr.name.to_s, ref.path]
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def instance_names
|
|
294
|
+
@instance_names ||= instances.map { |i| instance_name(i) }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# RS 3001: circular references are invalid.
|
|
298
|
+
def circular_reference_violations
|
|
299
|
+
graph = instances.to_h do |instance|
|
|
300
|
+
targets = reference_values(instance)
|
|
301
|
+
.map { |_name, path| path.to_s.split('.').first }
|
|
302
|
+
[instance_name(instance), targets]
|
|
303
|
+
end
|
|
304
|
+
cycles = cycles_in(graph)
|
|
305
|
+
cycles.map do |cycle|
|
|
306
|
+
Violation.new(
|
|
307
|
+
rule: 'reference_circularity',
|
|
308
|
+
message: "circular reference: #{cycle.join(' -> ')}"
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def cycles_in(graph)
|
|
314
|
+
cycles = []
|
|
315
|
+
graph.each_key do |start|
|
|
316
|
+
path = walk_for_cycle(graph, start)
|
|
317
|
+
cycles << path if path
|
|
318
|
+
end
|
|
319
|
+
cycles.uniq { |cycle| cycle.sort }
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def walk_for_cycle(graph, start)
|
|
323
|
+
path = [start]
|
|
324
|
+
node = start
|
|
325
|
+
loop do
|
|
326
|
+
nexts = graph[node] || []
|
|
327
|
+
node = nexts.first
|
|
328
|
+
return nil if node.nil? || node == '' || !graph.key?(node)
|
|
329
|
+
return path + [node] if path.include?(node)
|
|
330
|
+
|
|
331
|
+
path << node
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def instance_name(instance)
|
|
336
|
+
instance.respond_to?(:name) ? instance.name.to_s : ''
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def definition_type(instance)
|
|
340
|
+
(instance.isa || instance.type).to_s
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
data/lib/lutaml/lml/version.rb
CHANGED
data/lib/lutaml/lml.rb
CHANGED
|
@@ -44,6 +44,7 @@ module Lutaml
|
|
|
44
44
|
autoload :ModelCompiler, "lutaml/lml/model_compiler"
|
|
45
45
|
autoload :ImportResolver, "lutaml/lml/import_resolver"
|
|
46
46
|
autoload :ViewResolver, "lutaml/lml/view_resolver"
|
|
47
|
+
autoload :Validator, "lutaml/lml/validator"
|
|
47
48
|
autoload :ViewResolution, "lutaml/lml/view_resolution"
|
|
48
49
|
autoload :EntityTypes, "lutaml/lml/entity_types"
|
|
49
50
|
autoload :AssociationLabelResolver, "lutaml/lml/association_label_resolver"
|
|
@@ -79,11 +80,15 @@ module Lutaml
|
|
|
79
80
|
autoload :OperationParameter, "lutaml/lml/models/operation_parameter"
|
|
80
81
|
autoload :Package, "lutaml/lml/models/package"
|
|
81
82
|
autoload :PrimitiveType, "lutaml/lml/models/primitive_type"
|
|
83
|
+
autoload :Reference, "lutaml/lml/models/reference"
|
|
82
84
|
autoload :TopElementAttribute, "lutaml/lml/models/top_element_attribute"
|
|
83
85
|
autoload :UmlClass, "lutaml/lml/models/uml_class"
|
|
84
86
|
autoload :Value, "lutaml/lml/models/value"
|
|
85
87
|
autoload :ViewFilter, "lutaml/lml/models/view_filter"
|
|
86
88
|
autoload :ViewImport, "lutaml/lml/models/view_import"
|
|
89
|
+
autoload :MappingRule, "lutaml/lml/models/mapping_rule"
|
|
90
|
+
autoload :Namespace, "lutaml/lml/models/namespace"
|
|
91
|
+
autoload :SerializationMapping, "lutaml/lml/models/serialization_mapping"
|
|
87
92
|
|
|
88
93
|
# Namespaces with their own autoloads
|
|
89
94
|
autoload :Grammar, "lutaml/lml/grammar"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-lml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 1.3.
|
|
33
|
+
version: 1.3.68
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 1.3.
|
|
40
|
+
version: 1.3.68
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: moxml
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/lutaml/lml/data_processor/attribute_processing.rb
|
|
85
85
|
- lib/lutaml/lml/data_processor/collection_processing.rb
|
|
86
86
|
- lib/lutaml/lml/data_processor/instance_processing.rb
|
|
87
|
+
- lib/lutaml/lml/data_processor/value_declaration_processing.rb
|
|
87
88
|
- lib/lutaml/lml/data_processor/value_processing.rb
|
|
88
89
|
- lib/lutaml/lml/data_processor/view_processing.rb
|
|
89
90
|
- lib/lutaml/lml/document_builder.rb
|
|
@@ -101,6 +102,7 @@ files:
|
|
|
101
102
|
- lib/lutaml/lml/format/adapter/transform.rb
|
|
102
103
|
- lib/lutaml/lml/formatter.rb
|
|
103
104
|
- lib/lutaml/lml/formatter/base.rb
|
|
105
|
+
- lib/lutaml/lml/formatter/elk.rb
|
|
104
106
|
- lib/lutaml/lml/formatter/graphviz.rb
|
|
105
107
|
- lib/lutaml/lml/formatter/graphviz/document_formatter.rb
|
|
106
108
|
- lib/lutaml/lml/formatter/graphviz/html_builder.rb
|
|
@@ -112,6 +114,10 @@ files:
|
|
|
112
114
|
- lib/lutaml/lml/has_attributes.rb
|
|
113
115
|
- lib/lutaml/lml/import_resolver.rb
|
|
114
116
|
- lib/lutaml/lml/layout.rb
|
|
117
|
+
- lib/lutaml/lml/layout/elk_engine.rb
|
|
118
|
+
- lib/lutaml/lml/layout/elk_graph_builder.rb
|
|
119
|
+
- lib/lutaml/lml/layout/elk_svg_markers.rb
|
|
120
|
+
- lib/lutaml/lml/layout/elk_svg_renderer.rb
|
|
115
121
|
- lib/lutaml/lml/layout/engine.rb
|
|
116
122
|
- lib/lutaml/lml/layout/graph_viz_engine.rb
|
|
117
123
|
- lib/lutaml/lml/literal_value.rb
|
|
@@ -133,11 +139,15 @@ files:
|
|
|
133
139
|
- lib/lutaml/lml/models/instances_export.rb
|
|
134
140
|
- lib/lutaml/lml/models/instances_import.rb
|
|
135
141
|
- lib/lutaml/lml/models/mapping.rb
|
|
142
|
+
- lib/lutaml/lml/models/mapping_rule.rb
|
|
136
143
|
- lib/lutaml/lml/models/member_field.rb
|
|
144
|
+
- lib/lutaml/lml/models/namespace.rb
|
|
137
145
|
- lib/lutaml/lml/models/operation.rb
|
|
138
146
|
- lib/lutaml/lml/models/operation_parameter.rb
|
|
139
147
|
- lib/lutaml/lml/models/package.rb
|
|
140
148
|
- lib/lutaml/lml/models/primitive_type.rb
|
|
149
|
+
- lib/lutaml/lml/models/reference.rb
|
|
150
|
+
- lib/lutaml/lml/models/serialization_mapping.rb
|
|
141
151
|
- lib/lutaml/lml/models/string_format.rb
|
|
142
152
|
- lib/lutaml/lml/models/top_element_attribute.rb
|
|
143
153
|
- lib/lutaml/lml/models/uml_class.rb
|
|
@@ -151,6 +161,7 @@ files:
|
|
|
151
161
|
- lib/lutaml/lml/transform.rb
|
|
152
162
|
- lib/lutaml/lml/types.rb
|
|
153
163
|
- lib/lutaml/lml/types/text_type.rb
|
|
164
|
+
- lib/lutaml/lml/validator.rb
|
|
154
165
|
- lib/lutaml/lml/version.rb
|
|
155
166
|
- lib/lutaml/lml/view_resolution.rb
|
|
156
167
|
- lib/lutaml/lml/view_resolver.rb
|