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,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Lml
|
|
5
|
+
# A flat, positioned index of the document's symbols and the uses
|
|
6
|
+
# that reference them. Backs navigation, hover, and completion in
|
|
7
|
+
# tooling, and is available as Document#symbols.
|
|
8
|
+
class SymbolIndex
|
|
9
|
+
Entry = Struct.new(:name, :kind, :container, :position, :detail, :record,
|
|
10
|
+
keyword_init: true)
|
|
11
|
+
|
|
12
|
+
# LSP SymbolKind values.
|
|
13
|
+
KINDS = {
|
|
14
|
+
package: 4, class: 5, method: 6, field: 8,
|
|
15
|
+
enum: 10, instance: 19, enum_member: 22, data_type: 23
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :entries
|
|
19
|
+
|
|
20
|
+
def initialize(document)
|
|
21
|
+
@entries = []
|
|
22
|
+
return unless document
|
|
23
|
+
|
|
24
|
+
walk_document(document)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def definitions(name)
|
|
28
|
+
@entries.select { |entry| entry.name.to_s == name.to_s }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def uses(name)
|
|
32
|
+
token = name.to_s
|
|
33
|
+
@entries.select do |entry|
|
|
34
|
+
if entry.record.is_a?(TopElementAttribute)
|
|
35
|
+
reference?(entry.record, token)
|
|
36
|
+
elsif entry.record.is_a?(Instance)
|
|
37
|
+
[entry.record.type.to_s, entry.record.isa.to_s].include?(token)
|
|
38
|
+
else
|
|
39
|
+
false
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def attribute_entries
|
|
45
|
+
@entries.select { |entry| entry.kind == :field }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def entity_entries
|
|
49
|
+
@entries.reject { |entry| %i[field method enum_member].include?(entry.kind) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def names
|
|
53
|
+
@entries.map(&:name).compact.uniq
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def walk_document(document)
|
|
59
|
+
walk_members(document, nil)
|
|
60
|
+
walk_instance(document.instance, nil) if document.instance
|
|
61
|
+
walk_collection(document.instances) if document.instances
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def walk_members(owner, container)
|
|
65
|
+
Array(owner.classes).each { |klass| walk_class(klass, container) }
|
|
66
|
+
Array(owner.enums).each { |enum| walk_enum(enum, container) }
|
|
67
|
+
Array(owner.data_types).each { |type| walk_data_type(type, container) }
|
|
68
|
+
Array(owner.packages).each { |package| walk_package(package, container) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def walk_package(package, container)
|
|
72
|
+
name = package.name.to_s
|
|
73
|
+
add(name, :package, container, package.source_position, nil, package)
|
|
74
|
+
walk_members(package, name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def walk_class(klass, container)
|
|
78
|
+
name = klass.name.to_s
|
|
79
|
+
add(name, :class, container, klass.source_position, parent_detail(klass), klass)
|
|
80
|
+
Array(klass.attributes).each do |attribute|
|
|
81
|
+
add(attribute.name, :field, name, attribute.source_position,
|
|
82
|
+
attribute_detail(attribute), attribute)
|
|
83
|
+
end
|
|
84
|
+
Array(klass.operations).each do |operation|
|
|
85
|
+
add(operation.name, :method, name, operation.source_position,
|
|
86
|
+
operation_detail(operation), operation)
|
|
87
|
+
end
|
|
88
|
+
Array(klass.data_types).each { |type| walk_data_type(type, name) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def walk_enum(enum, container)
|
|
92
|
+
name = enum.name.to_s
|
|
93
|
+
add(name, :enum, container, enum.source_position, nil, enum)
|
|
94
|
+
Array(enum.attributes).each do |member|
|
|
95
|
+
add(member.name, :enum_member, name, member.source_position,
|
|
96
|
+
enum_member_detail(member), member)
|
|
97
|
+
end
|
|
98
|
+
Array(enum.member_fields).each do |field|
|
|
99
|
+
add(field.name, :field, name, field.source_position,
|
|
100
|
+
field.type.to_s, field)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def walk_data_type(type, container)
|
|
105
|
+
name = type.name.to_s
|
|
106
|
+
add(name, :data_type, container, type.source_position, nil, type)
|
|
107
|
+
Array(type.attributes).each do |attribute|
|
|
108
|
+
add(attribute.name, :field, name, attribute.source_position,
|
|
109
|
+
attribute_detail(attribute), attribute)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def walk_collection(collection)
|
|
114
|
+
return unless collection.is_a?(InstanceCollection)
|
|
115
|
+
|
|
116
|
+
name = collection.name.to_s
|
|
117
|
+
add(name, :package, nil, collection.source_position, 'instances', collection)
|
|
118
|
+
Array(collection.instances).each { |instance| walk_instance(instance, name) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def walk_instance(instance, container)
|
|
122
|
+
name = instance_label(instance)
|
|
123
|
+
add(name, :instance, container, instance.source_position, name, instance)
|
|
124
|
+
Array(instance.attributes).each do |attribute|
|
|
125
|
+
next if attribute.name.to_s.empty?
|
|
126
|
+
|
|
127
|
+
add(attribute.name, :field, name, attribute.source_position,
|
|
128
|
+
attribute_value_detail(attribute), attribute)
|
|
129
|
+
Array(attribute.instances).each { |nested| walk_instance(nested, name) }
|
|
130
|
+
end
|
|
131
|
+
walk_instance(instance.instance, name) if instance.instance
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def add(name, kind, container, position, detail, record)
|
|
135
|
+
return if name.to_s.empty? || position.nil?
|
|
136
|
+
|
|
137
|
+
@entries << Entry.new(
|
|
138
|
+
name: name.to_s, kind: kind, container: container,
|
|
139
|
+
position: position, detail: detail, record: record
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parent_detail(klass)
|
|
144
|
+
klass.parent_class.to_s.empty? ? nil : "< #{klass.parent_class}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def attribute_detail(attribute)
|
|
148
|
+
detail = attribute.type.to_s.dup
|
|
149
|
+
cardinality = attribute.cardinality
|
|
150
|
+
if cardinality
|
|
151
|
+
max = cardinality.max.to_s
|
|
152
|
+
detail << " [#{cardinality.min}#{"..#{max}" unless max.empty?}]"
|
|
153
|
+
end
|
|
154
|
+
detail.empty? ? nil : detail
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def operation_detail(operation)
|
|
158
|
+
params = Array(operation.owned_parameter)
|
|
159
|
+
.map { |param| "#{param.name}: #{param.type}" }.join(', ')
|
|
160
|
+
detail = "(#{params})"
|
|
161
|
+
detail += " : #{operation.return_type}" if operation.return_type.to_s != ''
|
|
162
|
+
detail
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def enum_member_detail(member)
|
|
166
|
+
description = Array(member.attributes).find { |a| a.name == 'description' }
|
|
167
|
+
description&.type&.to_s
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def attribute_value_detail(attribute)
|
|
171
|
+
value = attribute.value
|
|
172
|
+
value = value.value if value.is_a?(LiteralValue)
|
|
173
|
+
value.nil? ? attribute.type.to_s : value.to_s
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def instance_label(instance)
|
|
177
|
+
type = instance.type.to_s
|
|
178
|
+
name = instance.name.to_s
|
|
179
|
+
return name unless name.empty?
|
|
180
|
+
return type unless type.empty?
|
|
181
|
+
|
|
182
|
+
'instance'
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def reference?(attribute, token)
|
|
186
|
+
type = attribute.type.to_s
|
|
187
|
+
return true if type == token || type.split('[]').first == token
|
|
188
|
+
|
|
189
|
+
value = attribute.value
|
|
190
|
+
value = value.value if value.is_a?(LiteralValue)
|
|
191
|
+
return false unless value.is_a?(Hash)
|
|
192
|
+
|
|
193
|
+
reference = value[:reference] || value['reference']
|
|
194
|
+
return false unless reference
|
|
195
|
+
|
|
196
|
+
reference.to_s.split('.').include?(token)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
data/lib/lutaml/lml/transform.rb
CHANGED
|
@@ -4,11 +4,13 @@ require 'parsanol'
|
|
|
4
4
|
|
|
5
5
|
module Lutaml
|
|
6
6
|
module Lml
|
|
7
|
+
# Converts the raw parsanol tree into the processed hash, retaining
|
|
8
|
+
# source positions as non-serialized metadata.
|
|
7
9
|
class Transform < Parsanol::Transform
|
|
8
10
|
VISIBILITY_MAP = {
|
|
9
11
|
'-' => 'private',
|
|
10
12
|
'#' => 'protected',
|
|
11
|
-
'~' => 'package'
|
|
13
|
+
'~' => 'package',
|
|
12
14
|
}.freeze
|
|
13
15
|
|
|
14
16
|
rule(visibility_modifier: simple(:visibility_value)) do
|
|
@@ -50,7 +52,52 @@ module Lutaml
|
|
|
50
52
|
BLOCK_KEYS = %i[definition comments].freeze
|
|
51
53
|
|
|
52
54
|
def apply(tree, context = nil)
|
|
53
|
-
|
|
55
|
+
processed = super
|
|
56
|
+
attach_positions(tree, processed)
|
|
57
|
+
strip_block_layout(processed)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Retains the source position of name/key captures as a :position
|
|
61
|
+
# hash beside the transformed value. The document builder moves it
|
|
62
|
+
# onto the model as non-serialized metadata (see Positioned).
|
|
63
|
+
def attach_positions(raw, processed)
|
|
64
|
+
case raw
|
|
65
|
+
when Hash
|
|
66
|
+
return unless processed.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
slice = capture_slice(raw[:name]) || capture_slice(raw[:key]) ||
|
|
69
|
+
capture_slice(raw[:value_name]) || capture_slice(raw[:lml_version])
|
|
70
|
+
processed[:position] = position_hash(slice) if slice
|
|
71
|
+
|
|
72
|
+
raw.each_key do |key|
|
|
73
|
+
attach_positions(raw[key], processed[key]) if processed.key?(key)
|
|
74
|
+
end
|
|
75
|
+
when Array
|
|
76
|
+
return unless processed.is_a?(Array)
|
|
77
|
+
|
|
78
|
+
raw.each_with_index do |item, index|
|
|
79
|
+
attach_positions(item, processed[index]) if index < processed.length
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# A name capture is either a bare token (a Slice) or a quoted
|
|
85
|
+
# string (a hash holding the inner Slice).
|
|
86
|
+
def capture_slice(node)
|
|
87
|
+
return node if node.is_a?(Parsanol::Slice)
|
|
88
|
+
return node[:string] if node.is_a?(Hash) && node[:string].is_a?(Parsanol::Slice)
|
|
89
|
+
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def position_hash(slice)
|
|
94
|
+
line, column = slice.line_and_column
|
|
95
|
+
{
|
|
96
|
+
line: line,
|
|
97
|
+
column: column,
|
|
98
|
+
offset: slice.offset,
|
|
99
|
+
length: slice.content.to_s.bytesize,
|
|
100
|
+
}
|
|
54
101
|
end
|
|
55
102
|
|
|
56
103
|
def strip_block_layout(node)
|
data/lib/lutaml/lml/validator.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rubygems/version"
|
|
4
|
+
|
|
3
5
|
module Lutaml
|
|
4
6
|
module Lml
|
|
5
7
|
# RS 3001 §Validation rules evaluated over a parsed document:
|
|
@@ -8,14 +10,27 @@ module Lutaml
|
|
|
8
10
|
# matching declared types) and reference validity (ref: paths resolve
|
|
9
11
|
# to instances and are acyclic).
|
|
10
12
|
class Validator
|
|
11
|
-
Violation = Struct.new(:rule, :message, keyword_init: true)
|
|
13
|
+
Violation = Struct.new(:rule, :message, :position, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
RULE_CODES = {
|
|
16
|
+
'unique_names' => 'validation/LML-V01',
|
|
17
|
+
'unique_attribute_names' => 'validation/LML-V02',
|
|
18
|
+
'mandatory_attributes' => 'validation/LML-V03',
|
|
19
|
+
'type_conformity' => 'validation/LML-V04',
|
|
20
|
+
'values_set_membership' => 'validation/LML-V05',
|
|
21
|
+
'unknown_attributes' => 'validation/LML-V06',
|
|
22
|
+
'reference_validity' => 'validation/LML-V07',
|
|
23
|
+
'reference_circularity' => 'validation/LML-V08',
|
|
24
|
+
'pattern_conformity' => 'validation/LML-V09',
|
|
25
|
+
'pattern_compiles' => 'validation/LML-V10',
|
|
26
|
+
}.freeze
|
|
12
27
|
|
|
13
28
|
PRIMITIVE_CONFORMITY = {
|
|
14
29
|
'Integer' => /\A-?\d+\z/,
|
|
15
30
|
'Number' => /\A-?\d+\z/,
|
|
16
31
|
'Float' => /\A-?\d+\.\d+\z/,
|
|
17
32
|
'Boolean' => /\A(true|false)\z/,
|
|
18
|
-
'Null' => /\Anull\z
|
|
33
|
+
'Null' => /\Anull\z/,
|
|
19
34
|
}.freeze
|
|
20
35
|
|
|
21
36
|
COLLECTION_KEYS = %i[properties attributes instances].freeze
|
|
@@ -24,10 +39,55 @@ module Lutaml
|
|
|
24
39
|
new(document).violations
|
|
25
40
|
end
|
|
26
41
|
|
|
42
|
+
# The structured form of the same rules: one Diagnostic per
|
|
43
|
+
# violation, with its code and the source position of the
|
|
44
|
+
# offending member when the parser retained one.
|
|
45
|
+
def self.diagnostics(document)
|
|
46
|
+
new(document).diagnostics
|
|
47
|
+
end
|
|
48
|
+
|
|
27
49
|
def initialize(document)
|
|
28
50
|
@document = document
|
|
29
51
|
end
|
|
30
52
|
|
|
53
|
+
def diagnostics
|
|
54
|
+
version_diagnostics + violations.map do |violation|
|
|
55
|
+
Diagnostic.build(
|
|
56
|
+
RULE_CODES.fetch(violation.rule),
|
|
57
|
+
message: violation.message,
|
|
58
|
+
file: @document.source_path,
|
|
59
|
+
line: violation.position&.line,
|
|
60
|
+
column: violation.position&.column,
|
|
61
|
+
length: violation.position&.length,
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Version negotiation: a document may declare the language version
|
|
67
|
+
# it speaks with an `lml "x.y"` prologue. A version newer than this
|
|
68
|
+
# build is a warning, not an error (forward compatibility).
|
|
69
|
+
def version_diagnostics
|
|
70
|
+
declared = @document.lml_version.to_s
|
|
71
|
+
return [] if declared.empty?
|
|
72
|
+
return [] unless newer_language_version?(declared)
|
|
73
|
+
|
|
74
|
+
position = @document.source_position
|
|
75
|
+
[Diagnostic.build(
|
|
76
|
+
'contract/LML-C06',
|
|
77
|
+
message: "document declares LML #{declared}; this build supports #{Grammar.version}",
|
|
78
|
+
file: @document.source_path,
|
|
79
|
+
line: position&.line,
|
|
80
|
+
column: position&.column,
|
|
81
|
+
length: position&.length,
|
|
82
|
+
)]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def newer_language_version?(declared)
|
|
86
|
+
Gem::Version.new(declared) > Gem::Version.new(Grammar.version)
|
|
87
|
+
rescue ArgumentError
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
31
91
|
def violations
|
|
32
92
|
unique_name_violations +
|
|
33
93
|
unique_attribute_violations +
|
|
@@ -47,7 +107,7 @@ module Lutaml
|
|
|
47
107
|
[
|
|
48
108
|
*Array(@document.classes),
|
|
49
109
|
*Array(@document.enums),
|
|
50
|
-
*Array(@document.data_types)
|
|
110
|
+
*Array(@document.data_types),
|
|
51
111
|
]
|
|
52
112
|
end
|
|
53
113
|
|
|
@@ -61,10 +121,9 @@ module Lutaml
|
|
|
61
121
|
|
|
62
122
|
def all_definitions
|
|
63
123
|
@all_definitions ||= begin
|
|
64
|
-
pairs = []
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Array(@document.data_types).each { |d| pairs << [d.name.to_s, d] }
|
|
124
|
+
pairs = Array(@document.classes).map { |c| [c.name.to_s, c] } +
|
|
125
|
+
Array(@document.enums).map { |e| [e.name.to_s, e] } +
|
|
126
|
+
Array(@document.data_types).map { |d| [d.name.to_s, d] }
|
|
68
127
|
Array(@document.packages).each { |pkg| collect_package_definitions(pkg, pairs) }
|
|
69
128
|
pairs
|
|
70
129
|
end
|
|
@@ -91,7 +150,8 @@ module Lutaml
|
|
|
91
150
|
|
|
92
151
|
Violation.new(
|
|
93
152
|
rule: 'unique_names',
|
|
94
|
-
message: "'#{name}' is defined #{count} times in #{scope_label}"
|
|
153
|
+
message: "'#{name}' is defined #{count} times in #{scope_label}",
|
|
154
|
+
position: definitions.find { |definition| definition.name.to_s == name }&.source_position,
|
|
95
155
|
)
|
|
96
156
|
end
|
|
97
157
|
end
|
|
@@ -105,7 +165,8 @@ module Lutaml
|
|
|
105
165
|
|
|
106
166
|
Violation.new(
|
|
107
167
|
rule: 'unique_attribute_names',
|
|
108
|
-
message: "#{name}: duplicate attribute(s) #{dupes.inspect}"
|
|
168
|
+
message: "#{name}: duplicate attribute(s) #{dupes.inspect}",
|
|
169
|
+
position: definition.source_position,
|
|
109
170
|
)
|
|
110
171
|
end
|
|
111
172
|
end
|
|
@@ -114,11 +175,58 @@ module Lutaml
|
|
|
114
175
|
Array(definition.attributes).map(&:name).map(&:to_s)
|
|
115
176
|
end
|
|
116
177
|
|
|
178
|
+
def entry_position(instance, name)
|
|
179
|
+
entry = Array(instance.attributes).find { |attribute| attribute.name.to_s == name }
|
|
180
|
+
entry&.source_position || instance.source_position
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# RS 3001 par. Attribute: a `pattern` declaration constrains
|
|
184
|
+
# string values; a pattern that does not compile is itself a
|
|
185
|
+
# definition error.
|
|
186
|
+
def pattern_violations(instance)
|
|
187
|
+
definition = type_definitions[definition_type(instance)]
|
|
188
|
+
return [] unless definition.is_a?(UmlClass) || definition.is_a?(DataType)
|
|
189
|
+
|
|
190
|
+
instance_attribute_pairs(instance).filter_map do |name, value|
|
|
191
|
+
attr = Array(definition.attributes).find { |a| a.name.to_s == name }
|
|
192
|
+
next unless attr&.pattern
|
|
193
|
+
|
|
194
|
+
regexp = compile_pattern(attr.pattern)
|
|
195
|
+
next invalid_pattern_violation(definition_type(instance), name, attr) unless regexp
|
|
196
|
+
|
|
197
|
+
literal = value.respond_to?(:value) ? value.value : value
|
|
198
|
+
next unless literal.is_a?(String)
|
|
199
|
+
next if regexp.match?(literal)
|
|
200
|
+
|
|
201
|
+
Violation.new(
|
|
202
|
+
rule: 'pattern_conformity',
|
|
203
|
+
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
204
|
+
"does not match #{attr.pattern.inspect}, got #{literal.inspect}",
|
|
205
|
+
position: entry_position(instance, name),
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def compile_pattern(pattern)
|
|
211
|
+
Regexp.new(pattern)
|
|
212
|
+
rescue RegexpError
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def invalid_pattern_violation(type, name, attr)
|
|
217
|
+
Violation.new(
|
|
218
|
+
rule: 'pattern_compiles',
|
|
219
|
+
message: "#{type}: attribute '#{name}' has an invalid pattern #{attr.pattern.inspect}",
|
|
220
|
+
position: attr.source_position,
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
117
224
|
def instance_violations
|
|
118
225
|
instances.flat_map do |instance|
|
|
119
226
|
mandatory_violations(instance) +
|
|
120
227
|
type_conformity_violations(instance) +
|
|
121
228
|
values_set_violations(instance) +
|
|
229
|
+
pattern_violations(instance) +
|
|
122
230
|
unknown_attribute_violations(instance) +
|
|
123
231
|
reference_violations(instance)
|
|
124
232
|
end.compact
|
|
@@ -140,7 +248,8 @@ module Lutaml
|
|
|
140
248
|
Violation.new(
|
|
141
249
|
rule: 'values_set_membership',
|
|
142
250
|
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
143
|
-
"must be one of #{attr.value_set.inspect}, got #{literal.to_s.inspect}"
|
|
251
|
+
"must be one of #{attr.value_set.inspect}, got #{literal.to_s.inspect}",
|
|
252
|
+
position: entry_position(instance, name),
|
|
144
253
|
)
|
|
145
254
|
end
|
|
146
255
|
end
|
|
@@ -151,12 +260,13 @@ module Lutaml
|
|
|
151
260
|
|
|
152
261
|
declared = attribute_names(definition)
|
|
153
262
|
instance_attribute_pairs(instance)
|
|
154
|
-
.reject { |
|
|
263
|
+
.reject { |pair| declared.include?(pair[0]) }
|
|
155
264
|
.map do |name, _value|
|
|
156
265
|
Violation.new(
|
|
157
266
|
rule: 'unknown_attributes',
|
|
158
267
|
message: "instance '#{instance_name(instance)}' (#{definition_type(instance)}): " \
|
|
159
|
-
"unknown attribute '#{name}'"
|
|
268
|
+
"unknown attribute '#{name}'",
|
|
269
|
+
position: entry_position(instance, name),
|
|
160
270
|
)
|
|
161
271
|
end
|
|
162
272
|
end
|
|
@@ -176,7 +286,8 @@ module Lutaml
|
|
|
176
286
|
Violation.new(
|
|
177
287
|
rule: 'mandatory_attributes',
|
|
178
288
|
message: "instance '#{instance_name(instance)}' (#{definition_type(instance)}): " \
|
|
179
|
-
"mandatory attribute '#{attr.name}' is not populated"
|
|
289
|
+
"mandatory attribute '#{attr.name}' is not populated",
|
|
290
|
+
position: instance.source_position,
|
|
180
291
|
)
|
|
181
292
|
end
|
|
182
293
|
end
|
|
@@ -185,8 +296,6 @@ module Lutaml
|
|
|
185
296
|
attr.cardinality && attr.cardinality.min == '1'
|
|
186
297
|
end
|
|
187
298
|
|
|
188
|
-
|
|
189
|
-
|
|
190
299
|
def type_conformity_violations(instance)
|
|
191
300
|
definition = type_definitions[definition_type(instance)]
|
|
192
301
|
return [] unless definition.respond_to?(:attributes)
|
|
@@ -207,7 +316,8 @@ module Lutaml
|
|
|
207
316
|
Violation.new(
|
|
208
317
|
rule: 'type_conformity',
|
|
209
318
|
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
210
|
-
"expects #{attr.type}, got #{value.to_s.inspect}"
|
|
319
|
+
"expects #{attr.type}, got #{value.to_s.inspect}",
|
|
320
|
+
position: entry_position(instance, name),
|
|
211
321
|
)
|
|
212
322
|
end
|
|
213
323
|
|
|
@@ -218,7 +328,8 @@ module Lutaml
|
|
|
218
328
|
next Violation.new(
|
|
219
329
|
rule: 'reference_validity',
|
|
220
330
|
message: "instance '#{instance_name(instance)}': attribute '#{name}' " \
|
|
221
|
-
"references unknown path '#{path}'"
|
|
331
|
+
"references unknown path '#{path}'",
|
|
332
|
+
position: entry_position(instance, name),
|
|
222
333
|
)
|
|
223
334
|
end
|
|
224
335
|
|
|
@@ -241,7 +352,7 @@ module Lutaml
|
|
|
241
352
|
@collection_names ||= [@document.instances&.name].compact
|
|
242
353
|
end
|
|
243
354
|
|
|
244
|
-
def attribute_violation(instance,
|
|
355
|
+
def attribute_violation(instance, name, path, target)
|
|
245
356
|
segment = path.to_s.split('.')[1]
|
|
246
357
|
target_instance = instances.find { |i| instance_name(i) == target }
|
|
247
358
|
return if segment.nil? || target_instance.nil? ||
|
|
@@ -250,7 +361,8 @@ module Lutaml
|
|
|
250
361
|
Violation.new(
|
|
251
362
|
rule: 'reference_validity',
|
|
252
363
|
message: "instance '#{instance_name(instance)}': reference '#{path}' " \
|
|
253
|
-
"does not resolve (no attribute '#{segment}' on '#{target}')"
|
|
364
|
+
"does not resolve (no attribute '#{segment}' on '#{target}')",
|
|
365
|
+
position: entry_position(instance, name),
|
|
254
366
|
)
|
|
255
367
|
end
|
|
256
368
|
|
|
@@ -306,7 +418,8 @@ module Lutaml
|
|
|
306
418
|
cycles.map do |cycle|
|
|
307
419
|
Violation.new(
|
|
308
420
|
rule: 'reference_circularity',
|
|
309
|
-
message: "circular reference: #{cycle.join(' -> ')}"
|
|
421
|
+
message: "circular reference: #{cycle.join(' -> ')}",
|
|
422
|
+
position: instances.find { |instance| instance_name(instance) == cycle.first }&.source_position,
|
|
310
423
|
)
|
|
311
424
|
end
|
|
312
425
|
end
|
|
@@ -317,7 +430,7 @@ module Lutaml
|
|
|
317
430
|
path = walk_for_cycle(graph, start)
|
|
318
431
|
cycles << path if path
|
|
319
432
|
end
|
|
320
|
-
cycles.uniq
|
|
433
|
+
cycles.uniq(&:sort)
|
|
321
434
|
end
|
|
322
435
|
|
|
323
436
|
def walk_for_cycle(graph, start)
|
data/lib/lutaml/lml/version.rb
CHANGED
data/lib/lutaml/lml.rb
CHANGED
|
@@ -17,9 +17,20 @@ module Lutaml
|
|
|
17
17
|
# and renders diagrams. See the entry-point contract under .parse /
|
|
18
18
|
# .parse_document below.
|
|
19
19
|
module Lml
|
|
20
|
-
|
|
20
|
+
# Base error for LML failures; carries an optional Diagnostic.
|
|
21
|
+
class Error < Lutaml::Error
|
|
22
|
+
# Structured record for the failure, when one could be built.
|
|
23
|
+
attr_reader :diagnostic
|
|
24
|
+
|
|
25
|
+
def initialize(message = nil, diagnostic: nil)
|
|
26
|
+
@diagnostic = diagnostic
|
|
27
|
+
super(message)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
class ParsingError < Error; end
|
|
22
32
|
class ImportError < Error; end
|
|
33
|
+
class HydrationError < Error; end
|
|
23
34
|
|
|
24
35
|
def self.compile(input, namespace: nil)
|
|
25
36
|
ModelCompiler.new(namespace: namespace).compile(input)
|
|
@@ -64,8 +75,14 @@ module Lutaml
|
|
|
64
75
|
autoload :EntityTypes, "lutaml/lml/entity_types"
|
|
65
76
|
autoload :AssociationLabelResolver, "lutaml/lml/association_label_resolver"
|
|
66
77
|
autoload :Executor, "lutaml/lml/executor"
|
|
78
|
+
autoload :Lsp, "lutaml/lml/lsp"
|
|
79
|
+
autoload :SymbolIndex, "lutaml/lml/symbol_index"
|
|
67
80
|
autoload :YamlParser, "lutaml/lml/yaml_parser"
|
|
68
81
|
autoload :HasAttributes, "lutaml/lml/has_attributes"
|
|
82
|
+
autoload :Quoting, "lutaml/lml/quoting"
|
|
83
|
+
autoload :Diagnostic, "lutaml/lml/diagnostic"
|
|
84
|
+
autoload :SourcePosition, "lutaml/lml/source_position"
|
|
85
|
+
autoload :Positioned, "lutaml/lml/positioned"
|
|
69
86
|
autoload :VERSION, "lutaml/lml/version"
|
|
70
87
|
|
|
71
88
|
# Type classes (Lutaml::Model::Type::Value subclasses, not models)
|
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.3.
|
|
4
|
+
version: 0.3.4
|
|
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-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -73,10 +73,12 @@ email:
|
|
|
73
73
|
- open.source@ribose.com
|
|
74
74
|
executables:
|
|
75
75
|
- lutaml-lml
|
|
76
|
+
- lutaml-lml-lsp
|
|
76
77
|
extensions: []
|
|
77
78
|
extra_rdoc_files: []
|
|
78
79
|
files:
|
|
79
80
|
- exe/lutaml-lml
|
|
81
|
+
- exe/lutaml-lml-lsp
|
|
80
82
|
- lib/lutaml/lml.rb
|
|
81
83
|
- lib/lutaml/lml/association_label_resolver.rb
|
|
82
84
|
- lib/lutaml/lml/cli.rb
|
|
@@ -87,6 +89,7 @@ files:
|
|
|
87
89
|
- lib/lutaml/lml/data_processor/value_declaration_processing.rb
|
|
88
90
|
- lib/lutaml/lml/data_processor/value_processing.rb
|
|
89
91
|
- lib/lutaml/lml/data_processor/view_processing.rb
|
|
92
|
+
- lib/lutaml/lml/diagnostic.rb
|
|
90
93
|
- lib/lutaml/lml/document_builder.rb
|
|
91
94
|
- lib/lutaml/lml/entity_types.rb
|
|
92
95
|
- lib/lutaml/lml/executor.rb
|
|
@@ -102,6 +105,7 @@ files:
|
|
|
102
105
|
- lib/lutaml/lml/format/adapter/transform.rb
|
|
103
106
|
- lib/lutaml/lml/formatter.rb
|
|
104
107
|
- lib/lutaml/lml/formatter/base.rb
|
|
108
|
+
- lib/lutaml/lml/formatter/canonical.rb
|
|
105
109
|
- lib/lutaml/lml/formatter/elk.rb
|
|
106
110
|
- lib/lutaml/lml/formatter/graphviz.rb
|
|
107
111
|
- lib/lutaml/lml/formatter/graphviz/document_formatter.rb
|
|
@@ -123,6 +127,12 @@ files:
|
|
|
123
127
|
- lib/lutaml/lml/layout/engine.rb
|
|
124
128
|
- lib/lutaml/lml/layout/graph_viz_engine.rb
|
|
125
129
|
- lib/lutaml/lml/literal_value.rb
|
|
130
|
+
- lib/lutaml/lml/lsp.rb
|
|
131
|
+
- lib/lutaml/lml/lsp/conversions.rb
|
|
132
|
+
- lib/lutaml/lml/lsp/keywords.rb
|
|
133
|
+
- lib/lutaml/lml/lsp/server.rb
|
|
134
|
+
- lib/lutaml/lml/lsp/store.rb
|
|
135
|
+
- lib/lutaml/lml/lsp/transport.rb
|
|
126
136
|
- lib/lutaml/lml/model_compiler.rb
|
|
127
137
|
- lib/lutaml/lml/model_compiler/enums.rb
|
|
128
138
|
- lib/lutaml/lml/model_compiler/hydration.rb
|
|
@@ -163,8 +173,12 @@ files:
|
|
|
163
173
|
- lib/lutaml/lml/models/view_import.rb
|
|
164
174
|
- lib/lutaml/lml/parser.rb
|
|
165
175
|
- lib/lutaml/lml/pipeline.rb
|
|
176
|
+
- lib/lutaml/lml/positioned.rb
|
|
166
177
|
- lib/lutaml/lml/preprocessor.rb
|
|
178
|
+
- lib/lutaml/lml/quoting.rb
|
|
167
179
|
- lib/lutaml/lml/source.rb
|
|
180
|
+
- lib/lutaml/lml/source_position.rb
|
|
181
|
+
- lib/lutaml/lml/symbol_index.rb
|
|
168
182
|
- lib/lutaml/lml/transform.rb
|
|
169
183
|
- lib/lutaml/lml/types.rb
|
|
170
184
|
- lib/lutaml/lml/types/text_type.rb
|