lutaml-lml 0.3.3 → 0.3.5

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/exe/lutaml-lml-lsp +11 -0
  3. data/lib/lutaml/lml/cli.rb +166 -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 +63 -21
  9. data/lib/lutaml/lml/format/adapter/standard_adapter.rb +22 -23
  10. data/lib/lutaml/lml/formatter/canonical.rb +736 -0
  11. data/lib/lutaml/lml/formatter.rb +2 -0
  12. data/lib/lutaml/lml/grammar/lml.artifact.json +2946 -2512
  13. data/lib/lutaml/lml/grammar/lml.parg +47 -9
  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/serialization_mappings.rb +36 -10
  23. data/lib/lutaml/lml/model_compiler.rb +25 -2
  24. data/lib/lutaml/lml/models/association.rb +3 -0
  25. data/lib/lutaml/lml/models/constraint.rb +3 -0
  26. data/lib/lutaml/lml/models/data_type.rb +3 -0
  27. data/lib/lutaml/lml/models/diagram.rb +3 -0
  28. data/lib/lutaml/lml/models/document.rb +10 -3
  29. data/lib/lutaml/lml/models/enum.rb +4 -1
  30. data/lib/lutaml/lml/models/instance.rb +3 -0
  31. data/lib/lutaml/lml/models/instance_collection.rb +3 -0
  32. data/lib/lutaml/lml/models/member_field.rb +2 -0
  33. data/lib/lutaml/lml/models/namespace.rb +2 -0
  34. data/lib/lutaml/lml/models/operation.rb +4 -1
  35. data/lib/lutaml/lml/models/package.rb +3 -0
  36. data/lib/lutaml/lml/models/primitive_type.rb +6 -3
  37. data/lib/lutaml/lml/models/serialization_mapping.rb +2 -0
  38. data/lib/lutaml/lml/models/top_element_attribute.rb +6 -0
  39. data/lib/lutaml/lml/models/uml_class.rb +6 -0
  40. data/lib/lutaml/lml/models/value.rb +3 -0
  41. data/lib/lutaml/lml/pipeline.rb +25 -8
  42. data/lib/lutaml/lml/positioned.rb +13 -0
  43. data/lib/lutaml/lml/preprocessor.rb +44 -13
  44. data/lib/lutaml/lml/quoting.rb +60 -0
  45. data/lib/lutaml/lml/source_position.rb +20 -0
  46. data/lib/lutaml/lml/symbol_index.rb +200 -0
  47. data/lib/lutaml/lml/transform.rb +49 -2
  48. data/lib/lutaml/lml/validator.rb +134 -21
  49. data/lib/lutaml/lml/version.rb +1 -1
  50. data/lib/lutaml/lml.rb +18 -1
  51. metadata +18 -4
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ module Lsp
6
+ # The open-document store. Every change reparses the full text
7
+ # (full-document sync) and recomputes diagnostics.
8
+ class Store
9
+ Entry = Struct.new(:uri, :text, :version, :model, :diagnostics,
10
+ keyword_init: true)
11
+
12
+ def initialize
13
+ @entries = {}
14
+ end
15
+
16
+ def open(uri:, text:, version: nil)
17
+ entry = Entry.new(uri: uri, text: text, version: version)
18
+ parse(entry)
19
+ @entries[uri] = entry
20
+ end
21
+
22
+ def change(uri:, text:, version: nil)
23
+ entry = @entries[uri]
24
+ return nil unless entry
25
+
26
+ entry.text = text
27
+ entry.version = version
28
+ parse(entry)
29
+ entry
30
+ end
31
+
32
+ def close(uri)
33
+ @entries.delete(uri)
34
+ end
35
+
36
+ def [](uri)
37
+ @entries[uri]
38
+ end
39
+
40
+ private
41
+
42
+ def parse(entry)
43
+ path = Conversions.uri_to_path(entry.uri)
44
+ entry.model = Lutaml::Lml.parse_document(entry.text)
45
+ entry.model.source_path = path
46
+ entry.diagnostics = Validator.diagnostics(entry.model)
47
+ rescue Lutaml::Lml::Error => e
48
+ entry.model = nil
49
+ entry.diagnostics = [diagnostic_for(e, path)]
50
+ end
51
+
52
+ def diagnostic_for(error, path)
53
+ record = error.diagnostic
54
+ return record if record&.file
55
+
56
+ Diagnostic.build(
57
+ record&.code || 'parse/LML-001',
58
+ message: record&.message || error.message.lines.first.to_s.strip,
59
+ file: path,
60
+ line: record&.line,
61
+ column: record&.column,
62
+ length: record&.length,
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Lutaml
6
+ module Lml
7
+ module Lsp
8
+ # LSP base protocol framing: `Content-Length: N\r\n\r\n<json>`.
9
+ # Reads one message at a time and writes responses to the output
10
+ # stream. stdout is the protocol channel; stderr is for logging.
11
+ class Transport
12
+ def initialize(input: $stdin, output: $stdout)
13
+ @input = input
14
+ @output = output
15
+ end
16
+
17
+ def read_message
18
+ headers = read_headers
19
+ return nil unless headers
20
+
21
+ length = headers["content-length"]
22
+ return nil unless length
23
+
24
+ body = @input.read(length.to_i)
25
+ body.nil? || body.empty? ? nil : JSON.parse(body)
26
+ end
27
+
28
+ def write_message(payload)
29
+ body = JSON.generate(payload)
30
+ @output.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
31
+ @output.flush
32
+ end
33
+
34
+ private
35
+
36
+ def read_headers
37
+ headers = {}
38
+ loop do
39
+ line = @input.gets
40
+ return nil if line.nil?
41
+
42
+ line = line.chomp
43
+ break if line.empty?
44
+
45
+ key, value = line.split(":", 2)
46
+ headers[key.to_s.strip.downcase] = value.to_s.strip
47
+ end
48
+ headers
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ # Language Server Protocol support: a stdio JSON-RPC server exposing
6
+ # diagnostics, symbols, navigation, hover, completion, and canonical
7
+ # formatting over the parsed document model.
8
+ module Lsp
9
+ autoload :Transport, "lutaml/lml/lsp/transport"
10
+ autoload :Server, "lutaml/lml/lsp/server"
11
+ autoload :Store, "lutaml/lml/lsp/store"
12
+ autoload :Keywords, "lutaml/lml/lsp/keywords"
13
+ autoload :Conversions, "lutaml/lml/lsp/conversions"
14
+ end
15
+ end
16
+ end
@@ -49,18 +49,40 @@ module Lutaml
49
49
  end
50
50
  end
51
51
 
52
+ # Default XML mapping: every attribute an element, except those
53
+ # declared `as :attribute`; an in-class `namespace "uri"` becomes
54
+ # the class's XML namespace (issue #49).
52
55
  def apply_xml_mapping(klass, root_name)
53
56
  attr_names = klass.attributes.keys
54
- return if attr_names.empty?
55
-
57
+ def_obj = @class_defs[root_name]
58
+ roles = xml_roles_of(def_obj)
59
+ ns_class = inline_namespace_class(def_obj)
56
60
  klass.xml do |mapping|
57
61
  mapping.element(root_name)
62
+ mapping.namespace(ns_class) if ns_class
58
63
  attr_names.each do |attr_name|
59
- mapping.map_element(attr_name.to_s, to: attr_name)
64
+ if roles[attr_name] == 'attribute'
65
+ mapping.map_attribute(attr_name.to_s, to: attr_name)
66
+ else
67
+ mapping.map_element(attr_name.to_s, to: attr_name)
68
+ end
60
69
  end
61
70
  end
62
71
  end
63
72
 
73
+ def xml_roles_of(def_obj)
74
+ Array(def_obj&.attributes).each_with_object({}) do |attr, roles|
75
+ roles[attr.name.to_sym] = attr.xml_role if attr.xml_role
76
+ end
77
+ end
78
+
79
+ def inline_namespace_class(def_obj)
80
+ decl = def_obj.xml_namespace if def_obj.is_a?(UmlClass)
81
+ return nil unless decl&.uri
82
+
83
+ build_namespace_class("#{decl.uri}|#{decl.prefix}", decl.uri, decl.prefix.to_s)
84
+ end
85
+
64
86
  # RS 3010 extension: key-value mapping blocks for the declared
65
87
  # formats (yaml/json/toml share lutaml-model's key_value model).
66
88
 
@@ -89,7 +111,7 @@ module Lutaml
89
111
  check_namespace_ambiguity(mapping)
90
112
  ns_class = namespace_class_for(mapping.namespace_ref) if mapping.namespace_ref
91
113
  rule_ns = mapping.rules.select { |r| r.kind == 'namespace' }
92
- .to_h { |r| [r.field, namespace_class_for(r.field)] }
114
+ .to_h { |r| [r.field, namespace_class_for(r.field)] }
93
115
  klass.xml do |m|
94
116
  m.element(mapping.element_name) if mapping.element_name
95
117
  m.namespace(ns_class) if ns_class
@@ -149,12 +171,16 @@ module Lutaml
149
171
  declared = @declared_namespaces.find { |ns| ns.name.to_s == ref }
150
172
  raise ArgumentError, "unknown namespace '#{ref}'" unless declared
151
173
 
152
- ns_class = Class.new(Lutaml::Xml::Namespace)
153
- uri = declared.uri
154
- prefix = declared.prefix.to_s
155
- ns_class.define_singleton_method(:uri) { uri }
156
- ns_class.define_singleton_method(:prefix_default) { prefix }
157
- @namespace_classes[ref] = ns_class
174
+ build_namespace_class(ref, declared.uri, declared.prefix.to_s)
175
+ end
176
+
177
+ def build_namespace_class(key, uri, prefix)
178
+ require 'lutaml/xml/namespace' unless defined?(Lutaml::Xml::Namespace)
179
+
180
+ @namespace_classes[key] ||= Class.new(Lutaml::Xml::Namespace).tap do |ns_class|
181
+ ns_class.define_singleton_method(:uri) { uri }
182
+ ns_class.define_singleton_method(:prefix_default) { prefix }
183
+ end
158
184
  end
159
185
  end
160
186
  end
@@ -59,6 +59,7 @@ module Lutaml
59
59
  @declared_namespaces = []
60
60
  @serialization_mappings = []
61
61
  @namespace_classes = {}
62
+ @class_defs = {}
62
63
  end
63
64
 
64
65
  def compile(input)
@@ -73,6 +74,17 @@ module Lutaml
73
74
  return {} unless doc.instance
74
75
 
75
76
  hydrate_instance(doc.instance)
77
+ rescue Lutaml::Lml::Error
78
+ raise
79
+ rescue StandardError => e
80
+ raise HydrationError.new(
81
+ "hydration failed: #{e.message}",
82
+ diagnostic: Diagnostic.build(
83
+ 'hydration/LML-H01',
84
+ message: e.message,
85
+ file: input.is_a?(Document) ? input.source_path : nil,
86
+ ),
87
+ )
76
88
  end
77
89
 
78
90
  def compiled_classes
@@ -129,6 +141,7 @@ module Lutaml
129
141
 
130
142
  def compile_class(klass_def)
131
143
  name = klass_def.name.to_s
144
+ @class_defs[name] = klass_def
132
145
  compiled_klass = build_compiled_class(klass_def)
133
146
  apply_declared_mappings(compiled_klass, klass_def)
134
147
  declare_derived_fields(compiled_klass, klass_def)
@@ -236,7 +249,11 @@ module Lutaml
236
249
  return Float::INFINITY if UNBOUNDED_TOKENS.include?(val.to_s)
237
250
  return val.to_i if val.to_s.match?(/\A\d+\z/)
238
251
 
239
- raise ValidationError, "Unrecognized cardinality token: #{val.inspect}"
252
+ message = "Unrecognized cardinality token: #{val.inspect}"
253
+ raise ValidationError.new(
254
+ message,
255
+ diagnostic: Diagnostic.build('contract/LML-C04', message: message),
256
+ )
240
257
  end
241
258
 
242
259
  def declare_derived_fields(compiled_klass, def_obj)
@@ -278,11 +295,17 @@ module Lutaml
278
295
 
279
296
  def register(name, klass)
280
297
  @compiled[name] = klass
281
- return unless @namespace
298
+ return name_anonymous_class(klass, name) unless @namespace
282
299
 
283
300
  namespace_module.const_set(name, klass)
284
301
  end
285
302
 
303
+ # Without a target namespace the class stays anonymous, and
304
+ # lutaml-model's schema generators need a class name for $defs/$ref.
305
+ def name_anonymous_class(klass, name)
306
+ klass.define_singleton_method(:name) { name }
307
+ end
308
+
286
309
  def namespace_module
287
310
  return @namespace if @namespace.is_a?(Module)
288
311
 
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A relationship between two class ends.
5
6
  class Association < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # From TopElement
7
10
  attribute :name, :string
8
11
  attribute :definition, Lutaml::Lml::Types::TextType
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A named constraint on a class.
5
6
  class Constraint < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # From TopElement
7
10
  attribute :name, :string
8
11
  attribute :definition, Lutaml::Lml::Types::TextType
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A user-defined data type declaration.
5
6
  class DataType < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # From TopElement
7
10
  attribute :name, :string
8
11
  attribute :wire_mappings, "Lutaml::Lml::Mapping", collection: true, default: -> { [] }
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A diagram declaration.
5
6
  class Diagram < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  attribute :name, :string
7
10
  attribute :definition, Lutaml::Lml::Types::TextType
8
11
  attribute :keyword, :string
@@ -2,9 +2,18 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # The parsed LML document model.
5
6
  class Document < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
9
+ # Non-serialized navigation index (definitions and uses).
10
+ def symbols
11
+ @symbols ||= SymbolIndex.new(self)
12
+ end
13
+
6
14
  # Core attributes
7
15
  attribute :name, :string
16
+ attribute :lml_version, :string
8
17
  attribute :title, :string
9
18
  attribute :caption, :string
10
19
  attribute :groups, "Lutaml::Lml::Group", collection: true, default: -> { [] }
@@ -47,9 +56,7 @@ module Lutaml
47
56
  def merge_instances(incoming)
48
57
  names = @instances.collections.map(&:name)
49
58
  incoming.collections.each do |collection|
50
- if names.include?(collection.name)
51
- raise Lutaml::Lml::Error, "duplicate collection name '#{collection.name}'"
52
- end
59
+ raise Lutaml::Lml::Error, "duplicate collection name '#{collection.name}'" if names.include?(collection.name)
53
60
  end
54
61
  @instances.instances.concat(incoming.instances)
55
62
  @instances.imports.concat(incoming.imports)
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # An enumeration declaration with its value members.
5
6
  class Enum < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # From TopElement
7
10
  attribute :name, :string
8
11
  attribute :definition, Lutaml::Lml::Types::TextType
@@ -15,7 +18,7 @@ module Lutaml
15
18
 
16
19
  # From Enum
17
20
  attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true,
18
- default: -> { [] }
21
+ default: -> { [] }
19
22
  attribute :modifier, :string
20
23
  attribute :member_fields, "Lutaml::Lml::MemberField", collection: true, default: -> { [] }
21
24
  attribute :from_table_name, :string
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # An instance with attribute assignments.
5
6
  class Instance < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  attribute :name, :string
7
10
  attribute :type, :string
8
11
  attribute :isa, :string
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A named instances collection with its instances and imports/exports.
5
6
  class InstanceCollection < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  attribute :name, :string
7
10
  attribute :instances, "Lutaml::Lml::Instance", collection: true, default: []
8
11
  attribute :imports, "Lutaml::Lml::InstancesImport", collection: true, default: []
@@ -5,6 +5,8 @@ module Lutaml
5
5
  # A typed payload field declared on an enum:
6
6
  # member_fields { abbreviation String urn_segment String }
7
7
  class MemberField < Lutaml::Model::Serializable
8
+ include Lutaml::Lml::Positioned
9
+
8
10
  attribute :name, :string
9
11
  attribute :type, :string
10
12
  end
@@ -6,6 +6,8 @@ module Lutaml
6
6
  # The compiler generates the corresponding Lutaml::Model::XmlNamespace
7
7
  # subclass for the compiled model namespace.
8
8
  class Namespace < Lutaml::Model::Serializable
9
+ include Lutaml::Lml::Positioned
10
+
9
11
  attribute :name, :string
10
12
  attribute :uri, :string
11
13
  attribute :prefix, :string
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A class operation with parameters and a return type.
5
6
  class Operation < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # From TopElement
7
10
  attribute :name, :string
8
11
  attribute :definition, Lutaml::Lml::Types::TextType
@@ -17,7 +20,7 @@ module Lutaml
17
20
  attribute :is_static, :boolean, default: false
18
21
  attribute :is_abstract, :boolean, default: false
19
22
  attribute :owned_parameter, "Lutaml::Lml::OperationParameter", collection: true,
20
- default: -> { [] }
23
+ default: -> { [] }
21
24
  end
22
25
  end
23
26
  end
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A package grouping model definitions.
5
6
  class Package < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # From TopElement
7
10
  attribute :name, :string
8
11
  attribute :definition, Lutaml::Lml::Types::TextType
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A primitive type declaration.
5
6
  class PrimitiveType < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # Primitives in LML are name-only entities, but the formatter
7
10
  # accesses the same attributes as DataType via format_class.
8
11
  attribute :name, :string
@@ -12,11 +15,11 @@ module Lutaml
12
15
  attribute :visibility, :string, default: "public"
13
16
  attribute :is_abstract, :boolean, default: false
14
17
  attribute :attributes, "Lutaml::Lml::TopElementAttribute", collection: true,
15
- default: -> { [] }
18
+ default: -> { [] }
16
19
  attribute :operations, "Lutaml::Lml::Operation", collection: true,
17
- default: -> { [] }
20
+ default: -> { [] }
18
21
  attribute :associations, "Lutaml::Lml::Association", collection: true,
19
- default: -> { [] }
22
+ default: -> { [] }
20
23
 
21
24
  def self.entity_type
22
25
  :primitives
@@ -6,6 +6,8 @@ module Lutaml
6
6
  # its class and linked by target name —
7
7
  # mapping xml Ceramic { element "ceramic" … }
8
8
  class SerializationMapping < Lutaml::Model::Serializable
9
+ include Lutaml::Lml::Positioned
10
+
9
11
  attribute :format, :string
10
12
  attribute :target, :string
11
13
  attribute :element_name, :string
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # An attribute declaration or instance assignment entry.
5
6
  class TopElementAttribute < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # Core attributes
7
10
  attribute :name, :string
8
11
  attribute :visibility, :string, default: "public"
@@ -21,6 +24,8 @@ module Lutaml
21
24
  attribute :default, :string
22
25
  attribute :value_set, :string, collection: true, default: -> { [] }
23
26
  attribute :pattern, :string
27
+ # `as :element` / `as :attribute`: the XML node kind (issue #49)
28
+ attribute :xml_role, :string
24
29
 
25
30
  # LML-specific attributes
26
31
  attribute :properties, "Lutaml::Lml::TopElementAttribute", collection: true, default: -> { [] }
@@ -57,6 +62,7 @@ module Lutaml
57
62
  map "default", to: :default
58
63
  map "value_set", to: :value_set
59
64
  map "pattern", to: :pattern
65
+ map "xml_role", to: :xml_role
60
66
  map "properties", to: :properties
61
67
  map "value", to: :value, with: { to: :value_to, from: :value_from }
62
68
  map "attributes", to: :attributes
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A class declaration with its members.
5
6
  class UmlClass < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  # From TopElement. TextType unescapes \{ \} and normalizes lines
7
10
  # on every assignment (builder, yaml, direct).
8
11
  attribute :name, :string
@@ -29,6 +32,8 @@ module Lutaml
29
32
  attribute :serialization_mappings, "Lutaml::Lml::SerializationMapping", collection: true, default: -> { [] }
30
33
 
31
34
  # LML-specific
35
+ # In-class `namespace "uri"` declaration (issue #49)
36
+ attribute :xml_namespace, "Lutaml::Lml::Namespace"
32
37
  attribute :parent_class, :string
33
38
 
34
39
  yaml do
@@ -39,6 +44,7 @@ module Lutaml
39
44
  map "stereotype", to: :stereotype
40
45
  map "visibility", to: :visibility
41
46
  map "type", to: :type
47
+ map "xml_namespace", to: :xml_namespace
42
48
  map "attributes", to: :attributes
43
49
  map "operations", to: :operations
44
50
  map "constraints", to: :constraints
@@ -2,7 +2,10 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
+ # A value construct member.
5
6
  class Value < Lutaml::Model::Serializable
7
+ include Lutaml::Lml::Positioned
8
+
6
9
  attribute :name, :string
7
10
  attribute :id, :string
8
11
  attribute :type, :string
@@ -29,7 +29,18 @@ module Lutaml
29
29
  def parse_raw(data)
30
30
  Transform.new.apply(Parser.new.parse(data))
31
31
  rescue Parsanol::ParseFailed => e
32
- raise(ParsingError, failure_message(e))
32
+ raise ParsingError.new(failure_message(e), diagnostic: parse_diagnostic(e))
33
+ end
34
+
35
+ def parse_diagnostic(error)
36
+ line, column = failure_location(error)
37
+ Diagnostic.build(
38
+ 'parse/LML-001',
39
+ message: error.message.lines.first.to_s.strip,
40
+ file: @input.path,
41
+ line: line,
42
+ column: column,
43
+ )
33
44
  end
34
45
 
35
46
  # Parsanol releases up to 1.3.57 annotate native failures with a
@@ -38,18 +49,24 @@ module Lutaml
38
49
  # rebuild the location when the marker is present.
39
50
  def failure_message(error)
40
51
  detail = error.message.lines.first.to_s.strip
52
+ line, column = failure_location(error)
53
+ location = (" at line #{line} char #{column}." if line && !detail.match?(/at line \d+ char \d+/))
54
+ "#{detail}#{location}\ncause: #{error.parse_failure_cause&.ascii_tree}"
55
+ end
56
+
57
+ def failure_location(error)
41
58
  cause = error.parse_failure_cause
42
59
  offset = error.message[/@@parsanol_pos:(\d+)/, 1]&.to_i
43
- location =
44
- if offset && cause&.source
45
- line, column = cause.source.line_and_column(offset)
46
- " at line #{line} char #{column}."
47
- end
48
- "#{detail}#{location}\ncause: #{cause&.ascii_tree}"
60
+ return cause.source.line_and_column(offset) if offset && cause&.source
61
+
62
+ match = error.message.match(/at line (\d+) char (\d+)/)
63
+ match ? [match[1].to_i, match[2].to_i] : [nil, nil]
49
64
  end
50
65
 
51
66
  def build_document(hash)
52
- DocumentBuilder.new(DocumentBuilder::DEFAULT_REGISTRY).build(:document, hash)
67
+ document = DocumentBuilder.new(DocumentBuilder::DEFAULT_REGISTRY).build(:document, hash)
68
+ document.source_path = @input.path
69
+ document
53
70
  end
54
71
  end
55
72
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Lml
5
+ # Non-serialized source metadata carried by parsed entities. These
6
+ # accessors are deliberately not lutaml-model attributes: positions
7
+ # and the source path are diagnostics metadata, not document data,
8
+ # and must not appear in to_hash/YAML/JSON output.
9
+ module Positioned
10
+ attr_accessor :source_position, :source_path
11
+ end
12
+ end
13
+ end