lutaml-lml 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/lutaml/lml/cli.rb +12 -3
- 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 +161 -6
- data/lib/lutaml/lml/grammar/lml.artifact.json +5837 -2815
- data/lib/lutaml/lml/grammar/lml.parg +64 -17
- data/lib/lutaml/lml/model_compiler.rb +209 -18
- data/lib/lutaml/lml/models/data_type.rb +1 -0
- data/lib/lutaml/lml/models/document.rb +2 -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/serialization_mapping.rb +17 -0
- data/lib/lutaml/lml/models/top_element_attribute.rb +4 -0
- data/lib/lutaml/lml/models/uml_class.rb +1 -0
- data/lib/lutaml/lml/preprocessor.rb +82 -6
- data/lib/lutaml/lml/validator.rb +276 -0
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +4 -0
- metadata +6 -1
|
@@ -21,16 +21,19 @@ module Lutaml
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
+
# RS 3001 §Comment: `//` to end of line and `/* ... */` block
|
|
25
|
+
# comments are stripped quote-aware before include expansion, so
|
|
26
|
+
# string literals (e.g. URLs) survive intact.
|
|
24
27
|
def expand_lines(text, base_dir, chain)
|
|
28
|
+
text = strip_comments(text)
|
|
25
29
|
text.split(/\r?\n/)
|
|
26
30
|
.flat_map { |line| expand_line(line, base_dir, chain) }
|
|
27
31
|
.join("\n")
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def expand_line(line, base_dir, chain)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return [cleaned] unless path
|
|
35
|
+
path = include_path(line, base_dir)
|
|
36
|
+
return [line] unless path
|
|
34
37
|
|
|
35
38
|
raise Error, "circular include detected: #{path}" if chain.include?(path)
|
|
36
39
|
|
|
@@ -45,13 +48,86 @@ module Lutaml
|
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
def read_included(path)
|
|
48
|
-
File.read(path)
|
|
51
|
+
File.read(path, encoding: "UTF-8")
|
|
49
52
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
50
53
|
raise Error, "cannot read include #{path}: #{e.message}"
|
|
51
54
|
end
|
|
52
55
|
|
|
53
|
-
def
|
|
54
|
-
|
|
56
|
+
def strip_comments(text)
|
|
57
|
+
scanner = CommentScanner.new(text)
|
|
58
|
+
out = +""
|
|
59
|
+
until scanner.done?
|
|
60
|
+
scanner.emit(out)
|
|
61
|
+
end
|
|
62
|
+
out
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Quote-aware scanner removing `//` and `/* */` comments while
|
|
66
|
+
# preserving string literals and line structure.
|
|
67
|
+
class CommentScanner
|
|
68
|
+
def initialize(text)
|
|
69
|
+
@text = text
|
|
70
|
+
@pos = 0
|
|
71
|
+
@quote = nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def done?
|
|
75
|
+
@pos >= @text.length
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def emit(out)
|
|
79
|
+
return emit_quoted(out) if @quote
|
|
80
|
+
return emit_delimiter(out) if string_delimiter?(@text[@pos])
|
|
81
|
+
return emit_block_comment(out) if block_comment_start?
|
|
82
|
+
return skip_line_comment if line_comment_start?
|
|
83
|
+
|
|
84
|
+
out << @text[@pos]
|
|
85
|
+
@pos += 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def string_delimiter?(ch)
|
|
91
|
+
ch == '"' || ch == "'"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def emit_delimiter(out)
|
|
95
|
+
@quote = @text[@pos]
|
|
96
|
+
out << @text[@pos]
|
|
97
|
+
@pos += 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def emit_quoted(out)
|
|
101
|
+
ch = @text[@pos]
|
|
102
|
+
out << ch
|
|
103
|
+
if ch == "\\"
|
|
104
|
+
out << @text[@pos + 1].to_s
|
|
105
|
+
@pos += 2
|
|
106
|
+
else
|
|
107
|
+
@quote = nil if ch == @quote
|
|
108
|
+
@pos += 1
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def block_comment_start?
|
|
113
|
+
@text[@pos, 2] == "/*"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def emit_block_comment(out)
|
|
117
|
+
close = @text.index("*/", @pos + 2)
|
|
118
|
+
raise Lutaml::Lml::Error, "unterminated block comment" unless close
|
|
119
|
+
|
|
120
|
+
out << "\n" * @text[@pos...close].count("\n") << " "
|
|
121
|
+
@pos = close + 2
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def line_comment_start?
|
|
125
|
+
@text[@pos, 2] == "//"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def skip_line_comment
|
|
129
|
+
@pos = @text.index("\n", @pos) || @text.length
|
|
130
|
+
end
|
|
55
131
|
end
|
|
56
132
|
end
|
|
57
133
|
end
|
|
@@ -0,0 +1,276 @@
|
|
|
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
|
+
private
|
|
38
|
+
|
|
39
|
+
def all_definitions
|
|
40
|
+
@all_definitions ||= begin
|
|
41
|
+
pairs = []
|
|
42
|
+
Array(@document.classes).each { |c| pairs << [c.name.to_s, c] }
|
|
43
|
+
Array(@document.enums).each { |e| pairs << [e.name.to_s, e] }
|
|
44
|
+
Array(@document.data_types).each { |d| pairs << [d.name.to_s, d] }
|
|
45
|
+
Array(@document.packages).each { |pkg| collect_package_definitions(pkg, pairs) }
|
|
46
|
+
pairs
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collect_package_definitions(pkg, pairs)
|
|
51
|
+
Array(pkg.classes).each { |c| pairs << [c.name.to_s, c] }
|
|
52
|
+
Array(pkg.enums).each { |e| pairs << [e.name.to_s, e] }
|
|
53
|
+
Array(pkg.data_types).each { |d| pairs << [d.name.to_s, d] }
|
|
54
|
+
Array(pkg.packages).each { |nested| collect_package_definitions(nested, pairs) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def type_definitions
|
|
58
|
+
@type_definitions ||= all_definitions.to_h
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def unique_name_violations
|
|
62
|
+
names = all_definitions.map(&:first)
|
|
63
|
+
names.tally.filter_map do |name, count|
|
|
64
|
+
next if count == 1
|
|
65
|
+
|
|
66
|
+
Violation.new(
|
|
67
|
+
rule: 'unique_names',
|
|
68
|
+
message: "'#{name}' is defined #{count} times"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def unique_attribute_violations
|
|
74
|
+
type_definitions.filter_map do |name, definition|
|
|
75
|
+
dupes = attribute_names(definition)
|
|
76
|
+
.tally.select { |_attr, count| count > 1 }.keys
|
|
77
|
+
next if dupes.empty?
|
|
78
|
+
|
|
79
|
+
Violation.new(
|
|
80
|
+
rule: 'unique_attribute_names',
|
|
81
|
+
message: "#{name}: duplicate attribute(s) #{dupes.inspect}"
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attribute_names(definition)
|
|
87
|
+
Array(definition.attributes).map(&:name).map(&:to_s)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def instance_violations
|
|
91
|
+
instances.flat_map do |instance|
|
|
92
|
+
mandatory_violations(instance) +
|
|
93
|
+
type_conformity_violations(instance) +
|
|
94
|
+
reference_violations(instance)
|
|
95
|
+
end.compact
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def instances
|
|
99
|
+
Array(@document.instances&.instances)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def mandatory_violations(instance)
|
|
103
|
+
definition = type_definitions[definition_type(instance)]
|
|
104
|
+
return [] unless definition.is_a?(UmlClass) || definition.is_a?(DataType)
|
|
105
|
+
|
|
106
|
+
definition.attributes.filter_map do |attr|
|
|
107
|
+
next unless mandatory?(attr)
|
|
108
|
+
next if populated?(instance, attr.name.to_s)
|
|
109
|
+
|
|
110
|
+
Violation.new(
|
|
111
|
+
rule: 'mandatory_attributes',
|
|
112
|
+
message: "instance '#{instance_name(instance)}' (#{definition_type(instance)}): " \
|
|
113
|
+
"mandatory attribute '#{attr.name}' is not populated"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def mandatory?(attr)
|
|
119
|
+
attr.cardinality && attr.cardinality.min == '1'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def populated?(instance, attr_name)
|
|
123
|
+
pair = instance_attribute(instance, attr_name)
|
|
124
|
+
return false unless pair
|
|
125
|
+
|
|
126
|
+
value = pair[1]
|
|
127
|
+
value.respond_to?(:value) ? !value.to_s.empty? : !value.to_s.empty?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def type_conformity_violations(instance)
|
|
131
|
+
definition = type_definitions[definition_type(instance)]
|
|
132
|
+
return [] unless definition.respond_to?(:attributes)
|
|
133
|
+
|
|
134
|
+
instance_attribute_pairs(instance).filter_map do |name, value|
|
|
135
|
+
attr = Array(definition.attributes).find { |a| a.name.to_s == name }
|
|
136
|
+
next unless attr
|
|
137
|
+
|
|
138
|
+
conformity_violation(instance, name, attr, value)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def conformity_violation(instance, name, attr, value)
|
|
143
|
+
pattern = PRIMITIVE_CONFORMITY[attr.type.to_s]
|
|
144
|
+
return unless pattern
|
|
145
|
+
return if pattern.match?(value.to_s)
|
|
146
|
+
|
|
147
|
+
Violation.new(
|
|
148
|
+
rule: 'type_conformity',
|
|
149
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
150
|
+
"expects #{attr.type}, got #{value.to_s.inspect}"
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def reference_violations(instance)
|
|
155
|
+
reference_values(instance).filter_map do |name, path|
|
|
156
|
+
segments = path.to_s.split('.')
|
|
157
|
+
unless resolvable_reference?(segments)
|
|
158
|
+
next Violation.new(
|
|
159
|
+
rule: 'reference_validity',
|
|
160
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
161
|
+
"references unknown path '#{path}'"
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
attribute_violation(instance, name, path, segments.first)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# RS 3001 reference paths: `instance` / `instance.attribute` /
|
|
170
|
+
# `collection.instance`.
|
|
171
|
+
def resolvable_reference?(segments)
|
|
172
|
+
case segments.length
|
|
173
|
+
when 1 then instance_names.include?(segments.first)
|
|
174
|
+
when 2 then instance_names.include?(segments.first) ||
|
|
175
|
+
collection_names.include?(segments.first)
|
|
176
|
+
else false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def collection_names
|
|
181
|
+
@collection_names ||= [@document.instances&.name].compact
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def attribute_violation(instance, _name, path, target)
|
|
185
|
+
segment = path.to_s.split('.')[1]
|
|
186
|
+
target_instance = instances.find { |i| instance_name(i) == target }
|
|
187
|
+
return if segment.nil? || target_instance.nil? ||
|
|
188
|
+
instance_attribute(target_instance, segment)
|
|
189
|
+
|
|
190
|
+
Violation.new(
|
|
191
|
+
rule: 'reference_validity',
|
|
192
|
+
message: "instance '#{instance_name(instance)}': reference '#{path}' " \
|
|
193
|
+
"does not resolve (no attribute '#{segment}' on '#{target}')"
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def instance_attribute_pairs(instance)
|
|
198
|
+
pairs = []
|
|
199
|
+
instance.each_attribute do |name, value, instances|
|
|
200
|
+
pairs << [name.to_s, value_for(value, instances)]
|
|
201
|
+
end
|
|
202
|
+
pairs
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def instance_attribute(instance, attr_name)
|
|
206
|
+
instance_attribute_pairs(instance).find { |name, _value| name == attr_name }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def value_for(value, instances)
|
|
210
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
211
|
+
return instances.first if literal.is_a?(Hash) && literal[:instances]
|
|
212
|
+
|
|
213
|
+
literal
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def reference_values(instance)
|
|
217
|
+
instance_attribute_pairs(instance).filter_map do |name, value|
|
|
218
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
219
|
+
next unless literal.is_a?(Hash) && literal[:reference]
|
|
220
|
+
|
|
221
|
+
[name, literal[:reference]]
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def instance_names
|
|
226
|
+
@instance_names ||= instances.map { |i| instance_name(i) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# RS 3001: circular references are invalid.
|
|
230
|
+
def circular_reference_violations
|
|
231
|
+
graph = instances.to_h do |instance|
|
|
232
|
+
targets = reference_values(instance)
|
|
233
|
+
.map { |_name, path| path.to_s.split('.').first }
|
|
234
|
+
[instance_name(instance), targets]
|
|
235
|
+
end
|
|
236
|
+
cycles = cycles_in(graph)
|
|
237
|
+
cycles.map do |cycle|
|
|
238
|
+
Violation.new(
|
|
239
|
+
rule: 'reference_circularity',
|
|
240
|
+
message: "circular reference: #{cycle.join(' -> ')}"
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def cycles_in(graph)
|
|
246
|
+
cycles = []
|
|
247
|
+
graph.each_key do |start|
|
|
248
|
+
path = walk_for_cycle(graph, start)
|
|
249
|
+
cycles << path if path
|
|
250
|
+
end
|
|
251
|
+
cycles.uniq { |cycle| cycle.sort }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def walk_for_cycle(graph, start)
|
|
255
|
+
path = [start]
|
|
256
|
+
node = start
|
|
257
|
+
loop do
|
|
258
|
+
nexts = graph[node] || []
|
|
259
|
+
node = nexts.first
|
|
260
|
+
return nil if node.nil? || node == '' || !graph.key?(node)
|
|
261
|
+
return path + [node] if path.include?(node)
|
|
262
|
+
|
|
263
|
+
path << node
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def instance_name(instance)
|
|
268
|
+
instance.respond_to?(:name) ? instance.name.to_s : ''
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def definition_type(instance)
|
|
272
|
+
(instance.isa || instance.type).to_s
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
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"
|
|
@@ -84,6 +85,9 @@ module Lutaml
|
|
|
84
85
|
autoload :Value, "lutaml/lml/models/value"
|
|
85
86
|
autoload :ViewFilter, "lutaml/lml/models/view_filter"
|
|
86
87
|
autoload :ViewImport, "lutaml/lml/models/view_import"
|
|
88
|
+
autoload :MappingRule, "lutaml/lml/models/mapping_rule"
|
|
89
|
+
autoload :Namespace, "lutaml/lml/models/namespace"
|
|
90
|
+
autoload :SerializationMapping, "lutaml/lml/models/serialization_mapping"
|
|
87
91
|
|
|
88
92
|
# Namespaces with their own autoloads
|
|
89
93
|
autoload :Grammar, "lutaml/lml/grammar"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-lml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -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
|
|
@@ -133,11 +134,14 @@ files:
|
|
|
133
134
|
- lib/lutaml/lml/models/instances_export.rb
|
|
134
135
|
- lib/lutaml/lml/models/instances_import.rb
|
|
135
136
|
- lib/lutaml/lml/models/mapping.rb
|
|
137
|
+
- lib/lutaml/lml/models/mapping_rule.rb
|
|
136
138
|
- lib/lutaml/lml/models/member_field.rb
|
|
139
|
+
- lib/lutaml/lml/models/namespace.rb
|
|
137
140
|
- lib/lutaml/lml/models/operation.rb
|
|
138
141
|
- lib/lutaml/lml/models/operation_parameter.rb
|
|
139
142
|
- lib/lutaml/lml/models/package.rb
|
|
140
143
|
- lib/lutaml/lml/models/primitive_type.rb
|
|
144
|
+
- lib/lutaml/lml/models/serialization_mapping.rb
|
|
141
145
|
- lib/lutaml/lml/models/string_format.rb
|
|
142
146
|
- lib/lutaml/lml/models/top_element_attribute.rb
|
|
143
147
|
- lib/lutaml/lml/models/uml_class.rb
|
|
@@ -151,6 +155,7 @@ files:
|
|
|
151
155
|
- lib/lutaml/lml/transform.rb
|
|
152
156
|
- lib/lutaml/lml/types.rb
|
|
153
157
|
- lib/lutaml/lml/types/text_type.rb
|
|
158
|
+
- lib/lutaml/lml/validator.rb
|
|
154
159
|
- lib/lutaml/lml/version.rb
|
|
155
160
|
- lib/lutaml/lml/view_resolution.rb
|
|
156
161
|
- lib/lutaml/lml/view_resolver.rb
|