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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d2253a295aed1a2d3218c34818aa1fddf38ca68be9830695175a52ba777719b
|
|
4
|
+
data.tar.gz: 6a070753dfcf0acf67b5196c8f8fca550f8b7ae41f01a15ad969f8d3e423de4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1190e583a4ed9f86af817b45863ab26a40668dc38f4f1d17458f5df869e47e409c2454a7282d60ba2a601896e53697a852a263ab6148390f972c29519a8af61
|
|
7
|
+
data.tar.gz: ec32fdc80d72afa8a7173655c3f47115acbab0c1b14535126954e0edd0bd2c809b218884c8e98287beb53bd740f225ec552f741171f5fded67dadeaced305ca5
|
data/exe/lutaml-lml-lsp
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "lutaml/lml"
|
|
5
|
+
|
|
6
|
+
# JSON-RPC framing is byte-oriented; text-mode newline translation would
|
|
7
|
+
# turn \r\n into \r\r\n and break Content-Length.
|
|
8
|
+
$stdin.binmode
|
|
9
|
+
$stdout.binmode
|
|
10
|
+
|
|
11
|
+
exit Lutaml::Lml::Lsp::Server.new.run
|
data/lib/lutaml/lml/cli.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
4
|
require 'pathname'
|
|
5
|
+
require 'json'
|
|
5
6
|
require 'lutaml/lml'
|
|
6
7
|
|
|
7
8
|
module Lutaml
|
|
@@ -103,11 +104,38 @@ module Lutaml
|
|
|
103
104
|
DESC
|
|
104
105
|
method_option :input_format, type: :string, aliases: '-i',
|
|
105
106
|
desc: 'Input format (lutaml, yaml, exp)'
|
|
107
|
+
method_option :format, type: :string, enum: %w[text json], default: 'text',
|
|
108
|
+
desc: 'Report format: text (default) or json'
|
|
106
109
|
def validate(*paths)
|
|
107
110
|
assert_input_paths(paths)
|
|
108
111
|
@input_format = options[:input_format] || DEFAULT_INPUT_FORMAT
|
|
109
|
-
|
|
110
|
-
report_validation(
|
|
112
|
+
diagnostics = paths.flat_map { |path| validate_single_file(path) }
|
|
113
|
+
report_validation(diagnostics)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
desc 'format [PATHS]', 'Render LML files in canonical form'
|
|
117
|
+
long_desc <<-DESC
|
|
118
|
+
Rewrite LML files into the RS 3001 canonical form (deterministic
|
|
119
|
+
member order, two-space layout, canonical attribute forms and
|
|
120
|
+
quoting).
|
|
121
|
+
|
|
122
|
+
With --check, files are not modified: each non-canonical file is
|
|
123
|
+
reported with a diff and the command exits 1.
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
lutaml lml format model.lml
|
|
127
|
+
|
|
128
|
+
lutaml lml format model.lml --check
|
|
129
|
+
DESC
|
|
130
|
+
method_option :check, type: :boolean, default: false,
|
|
131
|
+
desc: 'Report non-canonical files instead of rewriting them'
|
|
132
|
+
def format(*paths)
|
|
133
|
+
assert_input_paths(paths)
|
|
134
|
+
|
|
135
|
+
noncanonical = paths.reject { |path| file_canonical?(path) }
|
|
136
|
+
return if noncanonical.empty?
|
|
137
|
+
|
|
138
|
+
exit 1
|
|
111
139
|
end
|
|
112
140
|
|
|
113
141
|
desc 'compile PATH', 'Compile LML model definitions into Ruby classes'
|
|
@@ -206,6 +234,84 @@ module Lutaml
|
|
|
206
234
|
name.split('::').last.gsub(/[^a-zA-Z0-9]+/, '_').downcase
|
|
207
235
|
end
|
|
208
236
|
|
|
237
|
+
def file_canonical?(path)
|
|
238
|
+
input = Pathname.new(path)
|
|
239
|
+
raise Thor::Error, "File does not exist: #{path}" unless input.exist?
|
|
240
|
+
|
|
241
|
+
source = input.read
|
|
242
|
+
canonical = canonical_source(source)
|
|
243
|
+
return true if source == canonical
|
|
244
|
+
|
|
245
|
+
if options[:check]
|
|
246
|
+
say diff_source(source, canonical, path)
|
|
247
|
+
say "#{path} is not canonical", :red
|
|
248
|
+
false
|
|
249
|
+
else
|
|
250
|
+
input.write(canonical)
|
|
251
|
+
say "Formatted: #{path}", :green
|
|
252
|
+
true
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def canonical_source(source)
|
|
257
|
+
document = Lutaml::Lml.parse_document(source)
|
|
258
|
+
::Lutaml::Formatter::Canonical.new(document).render
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def diff_source(source, canonical, path)
|
|
262
|
+
old_lines = source.lines.map(&:chomp)
|
|
263
|
+
new_lines = canonical.lines.map(&:chomp)
|
|
264
|
+
out = ["--- #{path}", "+++ #{path} (canonical)"]
|
|
265
|
+
out.concat(diff_lines(old_lines, new_lines))
|
|
266
|
+
out.join("\n")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def diff_lines(old_lines, new_lines)
|
|
270
|
+
return ['(diff omitted: file too large)'] if old_lines.length * new_lines.length > 4_000_000
|
|
271
|
+
|
|
272
|
+
table = Array.new(old_lines.length + 1) do
|
|
273
|
+
Array.new(new_lines.length + 1, 0)
|
|
274
|
+
end
|
|
275
|
+
(old_lines.length - 1).downto(0) do |i|
|
|
276
|
+
(new_lines.length - 1).downto(0) do |j|
|
|
277
|
+
table[i][j] = if old_lines[i] == new_lines[j]
|
|
278
|
+
table[i + 1][j + 1] + 1
|
|
279
|
+
else
|
|
280
|
+
[table[i + 1][j], table[i][j + 1]].max
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
walk_diff(old_lines, new_lines, table)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def walk_diff(old_lines, new_lines, table)
|
|
288
|
+
result = []
|
|
289
|
+
i = 0
|
|
290
|
+
j = 0
|
|
291
|
+
while i < old_lines.length && j < new_lines.length
|
|
292
|
+
if old_lines[i] == new_lines[j]
|
|
293
|
+
result << " #{old_lines[i]}"
|
|
294
|
+
i += 1
|
|
295
|
+
j += 1
|
|
296
|
+
elsif table[i + 1][j] >= table[i][j + 1]
|
|
297
|
+
result << "- #{old_lines[i]}"
|
|
298
|
+
i += 1
|
|
299
|
+
else
|
|
300
|
+
result << "+ #{new_lines[j]}"
|
|
301
|
+
j += 1
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
while i < old_lines.length
|
|
305
|
+
result << "- #{old_lines[i]}"
|
|
306
|
+
i += 1
|
|
307
|
+
end
|
|
308
|
+
while j < new_lines.length
|
|
309
|
+
result << "+ #{new_lines[j]}"
|
|
310
|
+
j += 1
|
|
311
|
+
end
|
|
312
|
+
result
|
|
313
|
+
end
|
|
314
|
+
|
|
209
315
|
def assert_input_paths(paths)
|
|
210
316
|
return unless paths.empty?
|
|
211
317
|
|
|
@@ -250,33 +356,75 @@ module Lutaml
|
|
|
250
356
|
|
|
251
357
|
def validate_single_file(path_string)
|
|
252
358
|
input_path = Pathname.new(path_string)
|
|
253
|
-
return
|
|
359
|
+
return [missing_file_diagnostic(input_path)] unless input_path.exist?
|
|
254
360
|
|
|
255
361
|
document = parse_document(input_path)
|
|
256
|
-
|
|
257
|
-
if
|
|
258
|
-
|
|
259
|
-
|
|
362
|
+
diagnostics = Lutaml::Lml::Validator.diagnostics(document)
|
|
363
|
+
if diagnostics.any?
|
|
364
|
+
if text_output?
|
|
365
|
+
diagnostics.each do |diagnostic|
|
|
366
|
+
say diagnostic_line(diagnostic),
|
|
367
|
+
(diagnostic.severity == 'warning' ? :yellow : :red)
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
elsif text_output?
|
|
260
371
|
say "✓ #{input_path}", :green
|
|
261
|
-
nil
|
|
262
372
|
end
|
|
373
|
+
diagnostics
|
|
374
|
+
rescue Thor::Error => e
|
|
375
|
+
[contract_diagnostic('contract/LML-C02', e.message, input_path)]
|
|
376
|
+
rescue Lutaml::Lml::Error => e
|
|
377
|
+
[error_diagnostic(e, input_path)]
|
|
263
378
|
rescue StandardError => e
|
|
264
|
-
|
|
379
|
+
[contract_diagnostic('contract/LML-C03', e.message, input_path)]
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def missing_file_diagnostic(input_path)
|
|
383
|
+
contract_diagnostic('contract/LML-C01', 'File does not exist', input_path)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def contract_diagnostic(code, message, input_path)
|
|
387
|
+
diagnostic = Diagnostic.build(code, message: message, file: input_path.to_s)
|
|
388
|
+
say "✗ #{diagnostic}", :red if text_output?
|
|
389
|
+
diagnostic
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def error_diagnostic(error, input_path)
|
|
393
|
+
diagnostic = error.diagnostic
|
|
394
|
+
return diagnostic if diagnostic&.file
|
|
395
|
+
|
|
396
|
+
Diagnostic.build(
|
|
397
|
+
diagnostic&.code || 'parse/LML-001',
|
|
398
|
+
message: diagnostic&.message || error.message.lines.first.to_s.strip,
|
|
399
|
+
file: input_path.to_s,
|
|
400
|
+
line: diagnostic&.line,
|
|
401
|
+
column: diagnostic&.column,
|
|
402
|
+
length: diagnostic&.length,
|
|
403
|
+
)
|
|
265
404
|
end
|
|
266
405
|
|
|
267
|
-
def
|
|
268
|
-
|
|
406
|
+
def text_output?
|
|
407
|
+
options[:format] != 'json'
|
|
269
408
|
end
|
|
270
409
|
|
|
271
|
-
def
|
|
272
|
-
|
|
273
|
-
"#{
|
|
410
|
+
def diagnostic_line(diagnostic)
|
|
411
|
+
marker = diagnostic.severity == 'warning' ? '⚠' : '✗'
|
|
412
|
+
"#{marker} #{diagnostic}"
|
|
274
413
|
end
|
|
275
414
|
|
|
276
|
-
def report_validation(
|
|
277
|
-
|
|
278
|
-
|
|
415
|
+
def report_validation(diagnostics)
|
|
416
|
+
errors = diagnostics.count { |diagnostic| diagnostic.severity != 'warning' }
|
|
417
|
+
if options[:format] == 'json'
|
|
418
|
+
puts JSON.pretty_generate(diagnostics.map(&:to_hash))
|
|
419
|
+
exit 1 if errors.positive?
|
|
420
|
+
return
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
if errors.positive?
|
|
424
|
+
say "\nValidation failed with #{errors} error(s)", :red
|
|
279
425
|
exit 1
|
|
426
|
+
elsif diagnostics.any?
|
|
427
|
+
say "\nAll files valid (#{diagnostics.size} warning(s))", :yellow
|
|
280
428
|
else
|
|
281
429
|
say "\nAll files valid!", :green
|
|
282
430
|
end
|
|
@@ -120,9 +120,11 @@ module Lutaml
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
# `default: "Clear"` uses the colon separator (RS 3001 §Attribute);
|
|
123
|
-
# the colon lands in the captured name and must not.
|
|
123
|
+
# the colon lands in the captured name and must not. A quoted key
|
|
124
|
+
# is verbatim, so a trailing colon there is content.
|
|
124
125
|
def strip_argument_colon(name)
|
|
125
|
-
|
|
126
|
+
return name[:string] if name.is_a?(Hash) && name.key?(:string)
|
|
127
|
+
|
|
126
128
|
name.to_s.sub(/:\z/, "")
|
|
127
129
|
end
|
|
128
130
|
end
|
|
@@ -13,6 +13,7 @@ module Lutaml
|
|
|
13
13
|
# member payload properties.
|
|
14
14
|
def process_value_declaration(decl)
|
|
15
15
|
inner = { name: unwrap_string(decl[:value_name]) }
|
|
16
|
+
inner[:position] = decl[:position] if decl[:position]
|
|
16
17
|
Array(decl[:members]).each do |member|
|
|
17
18
|
normalize_value_member(inner, member)
|
|
18
19
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# Post-transform normalization: turns parse captures into the flat
|
|
6
|
+
# member hashes the document builder consumes.
|
|
5
7
|
class DataProcessor
|
|
6
8
|
autoload :ValueProcessing, "lutaml/lml/data_processor/value_processing"
|
|
7
9
|
autoload :AttributeProcessing, "lutaml/lml/data_processor/attribute_processing"
|
|
@@ -18,13 +20,14 @@ module Lutaml
|
|
|
18
20
|
include ViewProcessing
|
|
19
21
|
|
|
20
22
|
KEY_HANDLERS = {
|
|
23
|
+
lml_version: :process_lml_version,
|
|
21
24
|
requires: :process_requires,
|
|
22
25
|
instances: :process_instances,
|
|
23
26
|
instance: :process_instance,
|
|
24
27
|
attributes: :process_attributes,
|
|
25
28
|
value_decl: :process_value_declaration,
|
|
26
29
|
show_list: :process_show_list,
|
|
27
|
-
hide_list: :process_hide_list
|
|
30
|
+
hide_list: :process_hide_list,
|
|
28
31
|
}.freeze
|
|
29
32
|
|
|
30
33
|
def self.process(obj)
|
|
@@ -47,6 +50,10 @@ module Lutaml
|
|
|
47
50
|
def process_requires(obj)
|
|
48
51
|
obj.map { |req| process_value(req).last }
|
|
49
52
|
end
|
|
53
|
+
|
|
54
|
+
def process_lml_version(value)
|
|
55
|
+
value.is_a?(Hash) ? value[:string] : value
|
|
56
|
+
end
|
|
50
57
|
end
|
|
51
58
|
end
|
|
52
59
|
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Lml
|
|
7
|
+
# One diagnostic record: a code, severity, source location, message,
|
|
8
|
+
# and a remedy. Codes are namespaced by stage (parse/validation/
|
|
9
|
+
# hydration/contract) and listed in CODES, the single source of
|
|
10
|
+
# truth also documented in the README.
|
|
11
|
+
class Diagnostic < Lutaml::Model::Serializable
|
|
12
|
+
CODES = {
|
|
13
|
+
'parse/LML-001' => {
|
|
14
|
+
severity: 'error',
|
|
15
|
+
remedy: 'Fix the syntax at the reported position.',
|
|
16
|
+
},
|
|
17
|
+
'parse/LML-002' => {
|
|
18
|
+
severity: 'error',
|
|
19
|
+
remedy: 'Terminate the block comment with */.',
|
|
20
|
+
},
|
|
21
|
+
'parse/LML-003' => {
|
|
22
|
+
severity: 'error',
|
|
23
|
+
remedy: 'Check the include path and the included file.',
|
|
24
|
+
},
|
|
25
|
+
'validation/LML-V01' => {
|
|
26
|
+
severity: 'error',
|
|
27
|
+
remedy: 'Rename or remove the duplicate definition.',
|
|
28
|
+
},
|
|
29
|
+
'validation/LML-V02' => {
|
|
30
|
+
severity: 'error',
|
|
31
|
+
remedy: 'Rename or remove the duplicate attribute.',
|
|
32
|
+
},
|
|
33
|
+
'validation/LML-V03' => {
|
|
34
|
+
severity: 'error',
|
|
35
|
+
remedy: 'Populate the mandatory attribute on the instance.',
|
|
36
|
+
},
|
|
37
|
+
'validation/LML-V04' => {
|
|
38
|
+
severity: 'error',
|
|
39
|
+
remedy: 'Use a value matching the declared primitive type.',
|
|
40
|
+
},
|
|
41
|
+
'validation/LML-V05' => {
|
|
42
|
+
severity: 'error',
|
|
43
|
+
remedy: 'Use one of the values declared for the attribute.',
|
|
44
|
+
},
|
|
45
|
+
'validation/LML-V06' => {
|
|
46
|
+
severity: 'error',
|
|
47
|
+
remedy: 'Declare the attribute or remove it from the instance.',
|
|
48
|
+
},
|
|
49
|
+
'validation/LML-V07' => {
|
|
50
|
+
severity: 'error',
|
|
51
|
+
remedy: 'Point the reference at an existing instance or attribute.',
|
|
52
|
+
},
|
|
53
|
+
'validation/LML-V08' => {
|
|
54
|
+
severity: 'error',
|
|
55
|
+
remedy: 'Break the reference cycle.',
|
|
56
|
+
},
|
|
57
|
+
'validation/LML-V09' => {
|
|
58
|
+
severity: 'error',
|
|
59
|
+
remedy: 'Use a value matching the attribute pattern.',
|
|
60
|
+
},
|
|
61
|
+
'validation/LML-V10' => {
|
|
62
|
+
severity: 'error',
|
|
63
|
+
remedy: 'Fix the attribute pattern so that it compiles.',
|
|
64
|
+
},
|
|
65
|
+
'hydration/LML-H01' => {
|
|
66
|
+
severity: 'error',
|
|
67
|
+
remedy: 'Check the instance data against the compiled model.',
|
|
68
|
+
},
|
|
69
|
+
'contract/LML-C01' => {
|
|
70
|
+
severity: 'error',
|
|
71
|
+
remedy: 'Check the path of the input file.',
|
|
72
|
+
},
|
|
73
|
+
'contract/LML-C02' => {
|
|
74
|
+
severity: 'error',
|
|
75
|
+
remedy: 'Use one of the supported input formats.',
|
|
76
|
+
},
|
|
77
|
+
'contract/LML-C03' => {
|
|
78
|
+
severity: 'error',
|
|
79
|
+
remedy: 'Check the input file and its format.',
|
|
80
|
+
},
|
|
81
|
+
'contract/LML-C04' => {
|
|
82
|
+
severity: 'error',
|
|
83
|
+
remedy: 'Use a cardinality token such as 1, 0..1, or 0..n.',
|
|
84
|
+
},
|
|
85
|
+
'contract/LML-C05' => {
|
|
86
|
+
severity: 'error',
|
|
87
|
+
remedy: 'Check the imported view path and the imported files.',
|
|
88
|
+
},
|
|
89
|
+
'contract/LML-C06' => {
|
|
90
|
+
severity: 'warning',
|
|
91
|
+
remedy: 'Use a supported language version or upgrade lutaml-lml.',
|
|
92
|
+
},
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
attribute :code, :string
|
|
96
|
+
attribute :severity, :string, default: 'error'
|
|
97
|
+
attribute :file, :string
|
|
98
|
+
attribute :line, :integer
|
|
99
|
+
attribute :column, :integer
|
|
100
|
+
attribute :length, :integer
|
|
101
|
+
attribute :message, :string
|
|
102
|
+
attribute :remedy, :string
|
|
103
|
+
|
|
104
|
+
key_value do
|
|
105
|
+
map 'code', to: :code
|
|
106
|
+
map 'severity', to: :severity
|
|
107
|
+
map 'file', to: :file
|
|
108
|
+
map 'line', to: :line
|
|
109
|
+
map 'column', to: :column
|
|
110
|
+
map 'length', to: :length
|
|
111
|
+
map 'message', to: :message
|
|
112
|
+
map 'remedy', to: :remedy
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.build(code, message:, file: nil, line: nil, column: nil,
|
|
116
|
+
length: nil, severity: nil, remedy: nil)
|
|
117
|
+
entry = CODES.fetch(code)
|
|
118
|
+
new(
|
|
119
|
+
code: code,
|
|
120
|
+
severity: severity || entry[:severity],
|
|
121
|
+
file: file,
|
|
122
|
+
line: line,
|
|
123
|
+
column: column,
|
|
124
|
+
length: length,
|
|
125
|
+
message: message,
|
|
126
|
+
remedy: remedy || entry[:remedy],
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Human rendering: `path:line:col: [CODE] message`, then the
|
|
131
|
+
# remedy. Position segments are omitted when the parser could not
|
|
132
|
+
# attach them.
|
|
133
|
+
def to_s
|
|
134
|
+
"#{location}[#{code}] #{message}#{remedy_suffix}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def location
|
|
138
|
+
parts = [file, line, column].compact
|
|
139
|
+
parts.empty? ? '' : "#{parts.join(':')}: "
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def remedy_suffix
|
|
145
|
+
remedy.to_s.empty? ? '' : "\n remedy: #{remedy}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Lml
|
|
5
|
+
# Builds domain model objects from processed parse hashes, applying
|
|
6
|
+
# source positions as non-serialized metadata.
|
|
5
7
|
class DocumentBuilder
|
|
6
8
|
# Default registry: builder keys → LML model classes.
|
|
7
9
|
# Callers needing a different mapping can pass their own registry
|
|
@@ -19,6 +21,8 @@ module Lutaml
|
|
|
19
21
|
operation: ::Lutaml::Lml::Operation,
|
|
20
22
|
constraint: ::Lutaml::Lml::Constraint,
|
|
21
23
|
value: ::Lutaml::Lml::Value,
|
|
24
|
+
instance: ::Lutaml::Lml::Instance,
|
|
25
|
+
instance_collection: ::Lutaml::Lml::InstanceCollection,
|
|
22
26
|
view_import: ::Lutaml::Lml::ViewImport,
|
|
23
27
|
view_filter: ::Lutaml::Lml::ViewFilter,
|
|
24
28
|
mapping: ::Lutaml::Lml::Mapping,
|
|
@@ -26,7 +30,7 @@ module Lutaml
|
|
|
26
30
|
derived_field: ::Lutaml::Lml::DerivedField,
|
|
27
31
|
member_field: ::Lutaml::Lml::MemberField,
|
|
28
32
|
namespace: ::Lutaml::Lml::Namespace,
|
|
29
|
-
serialization_mapping: ::Lutaml::Lml::SerializationMapping
|
|
33
|
+
serialization_mapping: ::Lutaml::Lml::SerializationMapping,
|
|
30
34
|
}.freeze
|
|
31
35
|
|
|
32
36
|
attr_reader :registry
|
|
@@ -39,6 +43,7 @@ module Lutaml
|
|
|
39
43
|
document package class enum data_type diagram view_import view_filter
|
|
40
44
|
attribute association operation constraint value cardinality mapping
|
|
41
45
|
string_format derived_field namespace serialization_mapping
|
|
46
|
+
instance instance_collection
|
|
42
47
|
].freeze
|
|
43
48
|
|
|
44
49
|
MEMBER_KEY_MAP = {
|
|
@@ -56,7 +61,8 @@ module Lutaml
|
|
|
56
61
|
mappings: :mapping,
|
|
57
62
|
member_fields: :member_field,
|
|
58
63
|
namespaces: :namespace,
|
|
59
|
-
serialization_mappings: :serialization_mapping
|
|
64
|
+
serialization_mappings: :serialization_mapping,
|
|
65
|
+
instances: :instance,
|
|
60
66
|
}.freeze
|
|
61
67
|
|
|
62
68
|
FACTORY_KEYS.each do |key|
|
|
@@ -71,6 +77,7 @@ module Lutaml
|
|
|
71
77
|
|
|
72
78
|
def set_model(model, hash)
|
|
73
79
|
hash = build_members(model, hash)
|
|
80
|
+
apply_collection_members(model, hash)
|
|
74
81
|
set_model_attributes(model, hash)
|
|
75
82
|
expand_class_specializer(model, hash)
|
|
76
83
|
end
|
|
@@ -86,10 +93,21 @@ module Lutaml
|
|
|
86
93
|
|
|
87
94
|
def set_model_attributes(model, hash)
|
|
88
95
|
hash.each do |key, value|
|
|
96
|
+
if key == :position
|
|
97
|
+
attach_position(model, value)
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
89
101
|
apply_attribute(model, key, value)
|
|
90
102
|
end
|
|
91
103
|
end
|
|
92
104
|
|
|
105
|
+
def attach_position(model, value)
|
|
106
|
+
return unless model.is_a?(Positioned) && value.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
model.source_position = SourcePosition.new(**value)
|
|
109
|
+
end
|
|
110
|
+
|
|
93
111
|
def build_members(model, hash)
|
|
94
112
|
members = hash.delete(:members)
|
|
95
113
|
members.to_a.each do |member_hash|
|
|
@@ -133,15 +151,26 @@ module Lutaml
|
|
|
133
151
|
normalize_serialization_mapping(hash)
|
|
134
152
|
expand_instances_collection_name(hash)
|
|
135
153
|
expand_nested_members(model, hash)
|
|
154
|
+
apply_collection_members(model, hash)
|
|
155
|
+
|
|
156
|
+
remap_filter_keys(hash, model)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Builds collection-shaped keys through the registry rather than
|
|
160
|
+
# letting lutaml-model cast raw hashes, so nested entities keep
|
|
161
|
+
# their non-serialized source metadata (positions).
|
|
162
|
+
def apply_collection_members(model, hash)
|
|
163
|
+
model.instances = build(:instance_collection, hash.delete(:instances)) if model.is_a?(Document) && hash.key?(:instances)
|
|
164
|
+
|
|
136
165
|
MEMBER_KEY_MAP.each do |plural_key, singular_key|
|
|
137
166
|
data = hash.delete(plural_key)
|
|
138
167
|
next if data.nil?
|
|
139
168
|
|
|
140
|
-
|
|
141
|
-
|
|
169
|
+
entries = data.is_a?(Array) ? data : [data]
|
|
170
|
+
entries.each do |entry|
|
|
171
|
+
ensure_collection(model, plural_key) << build(singular_key, entry)
|
|
172
|
+
end
|
|
142
173
|
end
|
|
143
|
-
|
|
144
|
-
remap_filter_keys(hash, model)
|
|
145
174
|
end
|
|
146
175
|
|
|
147
176
|
# RS 3001 §Enums: an enum-level `values { v1, v2 }` shorthand
|
|
@@ -165,12 +194,15 @@ module Lutaml
|
|
|
165
194
|
# become the entry's instances (RS 3001 §Enums values carrying
|
|
166
195
|
# instances).
|
|
167
196
|
entry = { name: inner[:name] }
|
|
197
|
+
entry[:position] = inner[:position] if inner[:position]
|
|
168
198
|
entry[:attributes] = inner[:attributes] if inner[:attributes]
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
199
|
+
if inner[:properties]
|
|
200
|
+
entry[:properties] = Array(inner[:properties]).flat_map do |pair|
|
|
201
|
+
pair.is_a?(Hash) ? [pair] : Array(pair)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
172
204
|
entry[:instances] = inner[:instances] if inner[:instances]
|
|
173
|
-
|
|
205
|
+
append_collection(model, :attributes, build(:attribute, entry))
|
|
174
206
|
end
|
|
175
207
|
|
|
176
208
|
# Namespace declaration bodies arrive as keyword-line members
|
|
@@ -293,22 +325,22 @@ module Lutaml
|
|
|
293
325
|
end
|
|
294
326
|
end
|
|
295
327
|
|
|
296
|
-
def normalize_operation_hash(
|
|
297
|
-
|
|
298
|
-
if (vis =
|
|
299
|
-
|
|
328
|
+
def normalize_operation_hash(operation)
|
|
329
|
+
operation = operation.dup
|
|
330
|
+
if (vis = operation.delete(:visibility_modifier))
|
|
331
|
+
operation[:visibility] = Transform::VISIBILITY_MAP.fetch(vis.to_s, 'public')
|
|
300
332
|
end
|
|
301
|
-
stereo =
|
|
333
|
+
stereo = operation[:stereotype]
|
|
302
334
|
if stereo
|
|
303
335
|
items = stereo.is_a?(Array) ? stereo : [stereo]
|
|
304
|
-
|
|
336
|
+
operation[:stereotype] = items.map { |x| x.is_a?(Hash) ? x[:stereotype].to_s : x.to_s }
|
|
305
337
|
end
|
|
306
|
-
params =
|
|
338
|
+
params = operation[:owned_parameter]
|
|
307
339
|
if params
|
|
308
340
|
list = params.is_a?(Array) ? params : [params]
|
|
309
|
-
|
|
341
|
+
operation[:owned_parameter] = list.map { |param| normalize_operation_param(param) }
|
|
310
342
|
end
|
|
311
|
-
|
|
343
|
+
operation
|
|
312
344
|
end
|
|
313
345
|
|
|
314
346
|
def normalize_operation_param(param)
|
|
@@ -406,8 +438,8 @@ module Lutaml
|
|
|
406
438
|
end
|
|
407
439
|
|
|
408
440
|
def build_string_format(section)
|
|
409
|
-
items = section[:items].to_a.
|
|
410
|
-
|
|
441
|
+
items = section[:items].to_a.to_h do |item|
|
|
442
|
+
[item[:key].to_sym, unquote(item[:value])]
|
|
411
443
|
end
|
|
412
444
|
build(:string_format,
|
|
413
445
|
format: unquote(section[:format]),
|