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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/exe/lutaml-lml-lsp +11 -0
  3. data/lib/lutaml/lml/cli.rb +165 -17
  4. data/lib/lutaml/lml/data_processor/attribute_processing.rb +4 -2
  5. data/lib/lutaml/lml/data_processor/value_declaration_processing.rb +1 -0
  6. data/lib/lutaml/lml/data_processor.rb +8 -1
  7. data/lib/lutaml/lml/diagnostic.rb +149 -0
  8. data/lib/lutaml/lml/document_builder.rb +53 -21
  9. data/lib/lutaml/lml/format/adapter/standard_adapter.rb +22 -23
  10. data/lib/lutaml/lml/formatter/canonical.rb +719 -0
  11. data/lib/lutaml/lml/formatter.rb +2 -0
  12. data/lib/lutaml/lml/grammar/lml.artifact.json +2626 -2439
  13. data/lib/lutaml/lml/grammar/lml.parg +15 -4
  14. data/lib/lutaml/lml/grammar.rb +13 -0
  15. data/lib/lutaml/lml/import_resolver.rb +13 -4
  16. data/lib/lutaml/lml/lsp/conversions.rb +78 -0
  17. data/lib/lutaml/lml/lsp/keywords.rb +43 -0
  18. data/lib/lutaml/lml/lsp/server.rb +378 -0
  19. data/lib/lutaml/lml/lsp/store.rb +68 -0
  20. data/lib/lutaml/lml/lsp/transport.rb +53 -0
  21. data/lib/lutaml/lml/lsp.rb +16 -0
  22. data/lib/lutaml/lml/model_compiler.rb +16 -1
  23. data/lib/lutaml/lml/models/association.rb +3 -0
  24. data/lib/lutaml/lml/models/constraint.rb +3 -0
  25. data/lib/lutaml/lml/models/data_type.rb +3 -0
  26. data/lib/lutaml/lml/models/diagram.rb +3 -0
  27. data/lib/lutaml/lml/models/document.rb +10 -3
  28. data/lib/lutaml/lml/models/enum.rb +4 -1
  29. data/lib/lutaml/lml/models/instance.rb +3 -0
  30. data/lib/lutaml/lml/models/instance_collection.rb +3 -0
  31. data/lib/lutaml/lml/models/member_field.rb +2 -0
  32. data/lib/lutaml/lml/models/namespace.rb +2 -0
  33. data/lib/lutaml/lml/models/operation.rb +4 -1
  34. data/lib/lutaml/lml/models/package.rb +3 -0
  35. data/lib/lutaml/lml/models/primitive_type.rb +6 -3
  36. data/lib/lutaml/lml/models/serialization_mapping.rb +2 -0
  37. data/lib/lutaml/lml/models/top_element_attribute.rb +3 -0
  38. data/lib/lutaml/lml/models/uml_class.rb +3 -0
  39. data/lib/lutaml/lml/models/value.rb +3 -0
  40. data/lib/lutaml/lml/pipeline.rb +25 -8
  41. data/lib/lutaml/lml/positioned.rb +13 -0
  42. data/lib/lutaml/lml/preprocessor.rb +44 -13
  43. data/lib/lutaml/lml/quoting.rb +60 -0
  44. data/lib/lutaml/lml/source_position.rb +20 -0
  45. data/lib/lutaml/lml/symbol_index.rb +200 -0
  46. data/lib/lutaml/lml/transform.rb +49 -2
  47. data/lib/lutaml/lml/validator.rb +134 -21
  48. data/lib/lutaml/lml/version.rb +1 -1
  49. data/lib/lutaml/lml.rb +18 -1
  50. 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
@@ -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
- strip_block_layout(super)
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)
@@ -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
- Array(@document.classes).each { |c| pairs << [c.name.to_s, c] }
66
- Array(@document.enums).each { |e| pairs << [e.name.to_s, e] }
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 { |name, _value| declared.include?(name) }
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, _name, path, target)
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 { |cycle| cycle.sort }
433
+ cycles.uniq(&:sort)
321
434
  end
322
435
 
323
436
  def walk_for_cycle(graph, start)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
- VERSION = "0.3.3"
5
+ VERSION = "0.3.4"
6
6
  end
7
7
  end
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
- class Error < Lutaml::Error; end
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.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-19 00:00:00.000000000 Z
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