lutaml-lml 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/lutaml/lml/executor.rb +22 -14
- data/lib/lutaml/lml/format/adapter/standard_adapter.rb +8 -12
- data/lib/lutaml/lml/formatter/base.rb +1 -4
- data/lib/lutaml/lml/grammar/concerns/definitions.rb +20 -38
- data/lib/lutaml/lml/model_compiler.rb +34 -30
- data/lib/lutaml/lml/models/instance.rb +8 -0
- data/lib/lutaml/lml/preprocessor.rb +3 -6
- data/lib/lutaml/lml/version.rb +1 -1
- data/lib/lutaml/lml.rb +6 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 16aa3d91bcea83c3eec5b3ab8ae3d3c9b91386d39abf80da4da5704487d22edf
|
|
4
|
+
data.tar.gz: e1a1db496cd7888458915a831f874b16e72f6894b251740bc873aea17f6bf6b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f407f3ce4984f37f9b0a4c5932e99b4062439aceb6287a43e960f9166eeb0b3476a675981afd6777d453a1e895f09e576e0ea4b813cd42921712999bb8f38cca
|
|
7
|
+
data.tar.gz: 04705fa33b170b9966ef004a743100c4490c4dd954b43cb2cfe243badbaf6d3b898aea33b24dd980f14d2074bc4db83db2ddf2e4f64dae79bd4b911a84db8999
|
data/lib/lutaml/lml/executor.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
3
5
|
module Lutaml
|
|
4
6
|
module Lml
|
|
5
7
|
# Orchestrates instance data I/O: import external data, validate
|
|
@@ -12,7 +14,9 @@ module Lutaml
|
|
|
12
14
|
# Usage:
|
|
13
15
|
# compiled = ModelCompiler.new.compile(models_file)
|
|
14
16
|
# doc = Pipeline.call(instances_file, resolve: false)
|
|
15
|
-
#
|
|
17
|
+
# result = Executor.run(doc, compiled: compiled)
|
|
18
|
+
# result.instances # array of hydrated instance objects
|
|
19
|
+
# result.errors # array of validation error strings
|
|
16
20
|
#
|
|
17
21
|
class Executor
|
|
18
22
|
autoload :FormatAdapter, "lutaml/lml/executor/format_adapter"
|
|
@@ -21,6 +25,16 @@ module Lutaml
|
|
|
21
25
|
autoload :XmlAdapter, "lutaml/lml/executor/xml_adapter"
|
|
22
26
|
autoload :ConditionEvaluator, "lutaml/lml/executor/condition_evaluator"
|
|
23
27
|
|
|
28
|
+
# Result of running the executor: hydrated instances plus any
|
|
29
|
+
# validation errors collected along the way. Delegates array-like
|
|
30
|
+
# methods to instances so existing callers that treated the run
|
|
31
|
+
# return value as an array continue to work.
|
|
32
|
+
Result = Struct.new(:instances, :errors) do
|
|
33
|
+
extend Forwardable
|
|
34
|
+
|
|
35
|
+
def_delegators :instances, :length, :each, :map, :[], :empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
24
38
|
attr_reader :compiled
|
|
25
39
|
|
|
26
40
|
def initialize(compiled:)
|
|
@@ -28,16 +42,16 @@ module Lutaml
|
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
# Run the full import/validate/export cycle on a parsed document.
|
|
31
|
-
# Returns
|
|
45
|
+
# Returns a Result with hydrated instances and validation errors.
|
|
32
46
|
def self.run(doc, compiled:)
|
|
33
47
|
new(compiled: compiled).run(doc)
|
|
34
48
|
end
|
|
35
49
|
|
|
36
50
|
def run(doc)
|
|
37
51
|
instances = run_imports(doc)
|
|
38
|
-
validate_collections(doc, instances)
|
|
52
|
+
errors = validate_collections(doc, instances)
|
|
39
53
|
run_exports(doc, instances)
|
|
40
|
-
instances
|
|
54
|
+
Result.new(instances, errors)
|
|
41
55
|
end
|
|
42
56
|
|
|
43
57
|
private
|
|
@@ -51,19 +65,16 @@ module Lutaml
|
|
|
51
65
|
end
|
|
52
66
|
|
|
53
67
|
def import_one(imp)
|
|
54
|
-
|
|
55
|
-
adapter.import(imp, compiled: @compiled)
|
|
56
|
-
rescue FormatAdapter::AdapterNotFoundError
|
|
57
|
-
[]
|
|
68
|
+
FormatAdapter.resolve(imp.format_type).import(imp, compiled: @compiled)
|
|
58
69
|
end
|
|
59
70
|
|
|
60
71
|
# --- Collection validation ---
|
|
61
72
|
|
|
62
73
|
def validate_collections(doc, instances)
|
|
63
|
-
return unless doc.instances&.collections
|
|
74
|
+
return [] unless doc.instances&.collections
|
|
64
75
|
|
|
65
76
|
collection = doc.instances.collections
|
|
66
|
-
return unless collection.is_a?(Collection)
|
|
77
|
+
return [] unless collection.is_a?(Collection)
|
|
67
78
|
|
|
68
79
|
ConditionEvaluator.evaluate(collection, instances)
|
|
69
80
|
end
|
|
@@ -79,10 +90,7 @@ module Lutaml
|
|
|
79
90
|
end
|
|
80
91
|
|
|
81
92
|
def export_one(exp, instances)
|
|
82
|
-
|
|
83
|
-
adapter.export(exp, instances, compiled: @compiled)
|
|
84
|
-
rescue FormatAdapter::AdapterNotFoundError
|
|
85
|
-
nil
|
|
93
|
+
FormatAdapter.resolve(exp.format_type).export(exp, instances, compiled: @compiled)
|
|
86
94
|
end
|
|
87
95
|
end
|
|
88
96
|
end
|
|
@@ -15,10 +15,6 @@ module Lutaml
|
|
|
15
15
|
instance_to_hash(doc.instance)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def initialize(attributes = {}, **options)
|
|
19
|
-
super
|
|
20
|
-
end
|
|
21
|
-
|
|
22
18
|
def to_lml(_options = {})
|
|
23
19
|
attrs = @attributes.dup
|
|
24
20
|
type_name = attrs.delete(TYPE_KEY) || "Data"
|
|
@@ -34,14 +30,14 @@ module Lutaml
|
|
|
34
30
|
hash = {}
|
|
35
31
|
hash[TYPE_KEY] = instance.type if instance.type
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
if
|
|
39
|
-
hashes =
|
|
40
|
-
hash[
|
|
41
|
-
elsif
|
|
42
|
-
hash[
|
|
43
|
-
elsif !
|
|
44
|
-
hash[
|
|
33
|
+
instance.each_attribute do |name, value, nested|
|
|
34
|
+
if nested.any?
|
|
35
|
+
hashes = nested.map { |i| instance_to_hash(i) }
|
|
36
|
+
hash[name] = nested.one? ? hashes.first : hashes
|
|
37
|
+
elsif value.is_a?(Array)
|
|
38
|
+
hash[name] = value.map { |v| primitive_value(v) }
|
|
39
|
+
elsif !value.nil?
|
|
40
|
+
hash[name] = primitive_value(value)
|
|
45
41
|
end
|
|
46
42
|
end
|
|
47
43
|
|
|
@@ -7,6 +7,17 @@ module Lutaml
|
|
|
7
7
|
module Definitions
|
|
8
8
|
include Parslet
|
|
9
9
|
|
|
10
|
+
# Compose a brace-delimited body parser around an inner rule.
|
|
11
|
+
# All *_body rules share this scaffold; only the inner rule
|
|
12
|
+
# differs.
|
|
13
|
+
def braced_body(inner)
|
|
14
|
+
spaces? >>
|
|
15
|
+
str("{") >>
|
|
16
|
+
whitespace? >>
|
|
17
|
+
inner.repeat.as(:members) >>
|
|
18
|
+
str("}")
|
|
19
|
+
end
|
|
20
|
+
|
|
10
21
|
# -- Class
|
|
11
22
|
rule(:kw_class_modifier) { kw_abstract | kw_interface }
|
|
12
23
|
|
|
@@ -25,11 +36,7 @@ module Lutaml
|
|
|
25
36
|
class_inner_definitions >> whitespace?
|
|
26
37
|
end
|
|
27
38
|
rule(:class_body) do
|
|
28
|
-
|
|
29
|
-
str("{") >>
|
|
30
|
-
whitespace? >>
|
|
31
|
-
class_inner_definition.repeat.as(:members) >>
|
|
32
|
-
str("}")
|
|
39
|
+
braced_body(class_inner_definition)
|
|
33
40
|
end
|
|
34
41
|
rule(:class_body?) { class_body.maybe }
|
|
35
42
|
|
|
@@ -58,23 +65,19 @@ module Lutaml
|
|
|
58
65
|
str("}")
|
|
59
66
|
end
|
|
60
67
|
|
|
61
|
-
# -- Enum
|
|
68
|
+
# -- Enum and data_type share the same body content
|
|
62
69
|
rule(:enum_keyword) { kw_enum >> spaces }
|
|
63
|
-
rule(:
|
|
70
|
+
rule(:enum_or_data_type_inner_definitions) do
|
|
64
71
|
definition_body |
|
|
65
72
|
attribute_definition |
|
|
66
73
|
comment_definition |
|
|
67
74
|
comment_multiline_definition
|
|
68
75
|
end
|
|
69
|
-
rule(:
|
|
70
|
-
|
|
76
|
+
rule(:enum_or_data_type_inner_definition) do
|
|
77
|
+
enum_or_data_type_inner_definitions >> whitespace?
|
|
71
78
|
end
|
|
72
79
|
rule(:enum_body) do
|
|
73
|
-
|
|
74
|
-
str("{") >>
|
|
75
|
-
whitespace? >>
|
|
76
|
-
enum_inner_definition.repeat.as(:members) >>
|
|
77
|
-
str("}")
|
|
80
|
+
braced_body(enum_or_data_type_inner_definition)
|
|
78
81
|
end
|
|
79
82
|
rule(:enum_body?) { enum_body.maybe }
|
|
80
83
|
rule(:enum_definition) do
|
|
@@ -88,21 +91,8 @@ module Lutaml
|
|
|
88
91
|
|
|
89
92
|
# -- data_type
|
|
90
93
|
rule(:data_type_keyword) { kw_data_type >> spaces }
|
|
91
|
-
rule(:data_type_inner_definitions) do
|
|
92
|
-
definition_body |
|
|
93
|
-
attribute_definition |
|
|
94
|
-
comment_definition |
|
|
95
|
-
comment_multiline_definition
|
|
96
|
-
end
|
|
97
|
-
rule(:data_type_inner_definition) do
|
|
98
|
-
data_type_inner_definitions >> whitespace?
|
|
99
|
-
end
|
|
100
94
|
rule(:data_type_body) do
|
|
101
|
-
|
|
102
|
-
str("{") >>
|
|
103
|
-
whitespace? >>
|
|
104
|
-
data_type_inner_definition.repeat.as(:members) >>
|
|
105
|
-
str("}")
|
|
95
|
+
braced_body(enum_or_data_type_inner_definition)
|
|
106
96
|
end
|
|
107
97
|
rule(:data_type_body?) { data_type_body.maybe }
|
|
108
98
|
rule(:data_type_definition) do
|
|
@@ -141,11 +131,7 @@ module Lutaml
|
|
|
141
131
|
diagram_inner_definitions >> whitespace?
|
|
142
132
|
end
|
|
143
133
|
rule(:diagram_body) do
|
|
144
|
-
|
|
145
|
-
str("{") >>
|
|
146
|
-
whitespace? >>
|
|
147
|
-
diagram_inner_definition.repeat.as(:members) >>
|
|
148
|
-
str("}")
|
|
134
|
+
braced_body(diagram_inner_definition)
|
|
149
135
|
end
|
|
150
136
|
rule(:diagram_definition) do
|
|
151
137
|
diagram_keyword >>
|
|
@@ -177,11 +163,7 @@ module Lutaml
|
|
|
177
163
|
view_inner_definitions >> whitespace?
|
|
178
164
|
end
|
|
179
165
|
rule(:view_body) do
|
|
180
|
-
|
|
181
|
-
str("{") >>
|
|
182
|
-
whitespace? >>
|
|
183
|
-
view_inner_definition.repeat.as(:members) >>
|
|
184
|
-
str("}")
|
|
166
|
+
braced_body(view_inner_definition)
|
|
185
167
|
end
|
|
186
168
|
rule(:view_definition) do
|
|
187
169
|
view_keyword >>
|
|
@@ -18,6 +18,10 @@ module Lutaml
|
|
|
18
18
|
"Hash" => :hash,
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
|
+
# Tokens that denote unbounded cardinality in LML attribute
|
|
22
|
+
# declarations. Anything else must parse as an Integer.
|
|
23
|
+
UNBOUNDED_TOKENS = %w[* n N unbounded].freeze
|
|
24
|
+
|
|
21
25
|
def initialize(namespace: nil)
|
|
22
26
|
@namespace = namespace
|
|
23
27
|
@compiled = {}
|
|
@@ -43,20 +47,24 @@ module Lutaml
|
|
|
43
47
|
@compiled
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
# Validate an instances document against compiled models.
|
|
51
|
+
#
|
|
52
|
+
# Accepts the instances input as a String, IO, StringIO, or a
|
|
53
|
+
# pre-parsed Document. If +compiled:+ is supplied, that registry is
|
|
54
|
+
# used; otherwise models are compiled from the same input (which
|
|
55
|
+
# must therefore contain a models section).
|
|
56
|
+
#
|
|
57
|
+
# Returns an array of validation error strings (empty if all pass).
|
|
58
|
+
def validate(input, compiled: nil)
|
|
59
|
+
doc = input.is_a?(Document) ? input : Pipeline.call(input, resolve: false)
|
|
47
60
|
if compiled
|
|
48
61
|
@compiled = compiled
|
|
49
|
-
|
|
50
|
-
doc = Pipeline.call(input_or_instance, resolve: false)
|
|
62
|
+
else
|
|
51
63
|
compile_document(doc)
|
|
52
64
|
end
|
|
65
|
+
|
|
53
66
|
errors = []
|
|
54
|
-
instance
|
|
55
|
-
unless instance
|
|
56
|
-
doc ||= Pipeline.call(input_or_instance, resolve: false)
|
|
57
|
-
instance = doc.instance
|
|
58
|
-
end
|
|
59
|
-
validate_instance(instance, errors) if instance
|
|
67
|
+
validate_instance(doc.instance, errors) if doc.instance
|
|
60
68
|
errors
|
|
61
69
|
end
|
|
62
70
|
|
|
@@ -140,34 +148,28 @@ module Lutaml
|
|
|
140
148
|
end
|
|
141
149
|
|
|
142
150
|
def extract_raw_attributes(instance)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
hash[attr.name.to_sym] = attr.instances.map { |i| hydrate_instance(i) }
|
|
146
|
-
elsif attr.value.is_a?(Array)
|
|
147
|
-
hash[attr.name.to_sym] = attr.value
|
|
148
|
-
elsif !attr.value.nil?
|
|
149
|
-
hash[attr.name.to_sym] = attr.value
|
|
150
|
-
end
|
|
151
|
+
instance.each_attribute.each_with_object({}) do |(name, value, nested), hash|
|
|
152
|
+
hash[name.to_sym] = resolve_instance_value(value, nested)
|
|
151
153
|
end
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
def extract_instance_attributes(instance, klass)
|
|
155
|
-
schema_keys = klass.attributes.keys
|
|
156
|
-
|
|
157
|
-
key =
|
|
157
|
+
schema_keys = klass.attributes.keys.to_set
|
|
158
|
+
instance.each_attribute.each_with_object({}) do |(name, value, nested), hash|
|
|
159
|
+
key = name.to_sym
|
|
158
160
|
next unless schema_keys.include?(key)
|
|
159
161
|
|
|
160
|
-
hash[key] =
|
|
162
|
+
hash[key] = resolve_instance_value(value, nested)
|
|
161
163
|
end
|
|
162
164
|
end
|
|
163
165
|
|
|
164
|
-
def
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
elsif
|
|
168
|
-
|
|
169
|
-
elsif !
|
|
170
|
-
|
|
166
|
+
def resolve_instance_value(value, nested)
|
|
167
|
+
if nested.any?
|
|
168
|
+
nested.map { |i| hydrate_instance(i) }
|
|
169
|
+
elsif value.is_a?(Array)
|
|
170
|
+
value
|
|
171
|
+
elsif !value.nil?
|
|
172
|
+
value
|
|
171
173
|
end
|
|
172
174
|
end
|
|
173
175
|
|
|
@@ -304,8 +306,10 @@ module Lutaml
|
|
|
304
306
|
|
|
305
307
|
def parse_cardinality_value(val)
|
|
306
308
|
return nil if val.nil?
|
|
307
|
-
return Float::INFINITY if val
|
|
308
|
-
val.to_i
|
|
309
|
+
return Float::INFINITY if UNBOUNDED_TOKENS.include?(val.to_s)
|
|
310
|
+
return val.to_i if val.to_s.match?(/\A\d+\z/)
|
|
311
|
+
|
|
312
|
+
raise ValidationError, "Unrecognized cardinality token: #{val.inspect}"
|
|
309
313
|
end
|
|
310
314
|
|
|
311
315
|
def extract_enum_values(enum_def)
|
|
@@ -8,6 +8,14 @@ module Lutaml
|
|
|
8
8
|
attribute :instance, "Lutaml::Lml::Instance"
|
|
9
9
|
attribute :template, "Lutaml::Lml::TopElementAttribute", collection: true
|
|
10
10
|
attribute :parent, :string
|
|
11
|
+
|
|
12
|
+
def each_attribute
|
|
13
|
+
return enum_for(:each_attribute) unless block_given?
|
|
14
|
+
|
|
15
|
+
Array(attributes).each do |attr|
|
|
16
|
+
yield attr.name, attr.value, Array(attr.instances)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
end
|
|
@@ -38,17 +38,14 @@ module Lutaml
|
|
|
38
38
|
|
|
39
39
|
def process_include_line(include_root, line)
|
|
40
40
|
include_path_match = line.match(/^\s*include\s+(.+)/)
|
|
41
|
-
return line if include_path_match.nil?
|
|
41
|
+
return line if include_path_match.nil?
|
|
42
42
|
|
|
43
43
|
path_to_file = File.expand_path(include_path_match[1].strip, include_root)
|
|
44
44
|
File.read(path_to_file).split("\n").map do |l|
|
|
45
45
|
process_comment_line(l)
|
|
46
46
|
end
|
|
47
|
-
rescue Errno::ENOENT
|
|
48
|
-
|
|
49
|
-
"No such file or directory @ rb_sysopen - #{path_to_file}, " \
|
|
50
|
-
"include file paths need to be supplied relative to the main document"
|
|
51
|
-
)
|
|
47
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
48
|
+
warn "Skipping #{path_to_file}: #{e.message}"
|
|
52
49
|
[]
|
|
53
50
|
end
|
|
54
51
|
end
|
data/lib/lutaml/lml/version.rb
CHANGED
data/lib/lutaml/lml.rb
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
4
|
|
|
5
|
-
# Lutaml::Formatter and Lutaml::Layout are in the Lutaml top namespace,
|
|
6
|
-
# not Lutaml::Lml. Require their namespace files to set up autoloads.
|
|
7
|
-
require "lutaml/lml/formatter"
|
|
8
|
-
require "lutaml/lml/layout"
|
|
9
|
-
|
|
10
5
|
module Lutaml
|
|
11
6
|
class Error < StandardError; end
|
|
12
7
|
|
|
8
|
+
# Lutaml::Formatter and Lutaml::Layout live in the Lutaml top namespace
|
|
9
|
+
# (not Lutaml::Lml) but their files are part of this gem. Declare the
|
|
10
|
+
# autoloads here so first reference loads them lazily.
|
|
11
|
+
autoload :Formatter, "lutaml/lml/formatter"
|
|
12
|
+
autoload :Layout, "lutaml/lml/layout"
|
|
13
|
+
|
|
13
14
|
module Lml
|
|
14
15
|
class Error < Lutaml::Error; end
|
|
15
16
|
class ParsingError < Error; end
|