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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3953cc3c0f81db402cab373b0da574ebd8832e2aa42c98de6e88e3fc157e4503
4
- data.tar.gz: d3816ea57a18d40ff1577757367f41fc604e54ad945545223820787dff81a86b
3
+ metadata.gz: 16aa3d91bcea83c3eec5b3ab8ae3d3c9b91386d39abf80da4da5704487d22edf
4
+ data.tar.gz: e1a1db496cd7888458915a831f874b16e72f6894b251740bc873aea17f6bf6b3
5
5
  SHA512:
6
- metadata.gz: cd7dc48c1d8a1d89718e755e93ffc7a0ac0d577aab1d6c0986ddd7d45d6df3f88b1716f1514e445df0e3ab6c10b306904b3219247126bf5e60b8568acb6aafd3
7
- data.tar.gz: 9c79221cf405d9512c094da610abdb7ab4f8675ce64ca7180c0039e337a281872483ef31db2f9344a0e7c853d1d51085e3108b08b1e6fba83bd200134da5aa2d
6
+ metadata.gz: f407f3ce4984f37f9b0a4c5932e99b4062439aceb6287a43e960f9166eeb0b3476a675981afd6777d453a1e895f09e576e0ea4b813cd42921712999bb8f38cca
7
+ data.tar.gz: 04705fa33b170b9966ef004a743100c4490c4dd954b43cb2cfe243badbaf6d3b898aea33b24dd980f14d2074bc4db83db2ddf2e4f64dae79bd4b911a84db8999
@@ -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
- # instances = Executor.run(doc, compiled: compiled)
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 an array of hydrated instance objects.
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
- adapter = FormatAdapter.resolve(imp.format_type)
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
- adapter = FormatAdapter.resolve(exp.format_type)
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
- Array(instance.attributes).each do |attr|
38
- if attr.instances.any?
39
- hashes = attr.instances.map { |i| instance_to_hash(i) }
40
- hash[attr.name] = attr.instances.one? ? hashes.first : hashes
41
- elsif attr.value.is_a?(Array)
42
- hash[attr.name] = attr.value.map { |v| primitive_value(v) }
43
- elsif !attr.value.nil?
44
- hash[attr.name] = primitive_value(attr.value)
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
 
@@ -56,10 +56,7 @@ module Lutaml
56
56
  end
57
57
 
58
58
  def format(node)
59
- result = dispatch_format(node)
60
- return unless result
61
-
62
- result
59
+ dispatch_format(node)
63
60
  end
64
61
 
65
62
  def dispatch_format(node)
@@ -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
- spaces? >>
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(:enum_inner_definitions) do
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(:enum_inner_definition) do
70
- enum_inner_definitions >> whitespace?
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
- spaces? >>
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
- spaces? >>
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
- spaces? >>
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
- spaces? >>
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
- def validate(input_or_instance, compiled: nil)
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
- elsif input_or_instance.is_a?(String) || input_or_instance.is_a?(IO) || input_or_instance.is_a?(StringIO)
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 = input_or_instance.is_a?(Lutaml::Lml::Instance) ? input_or_instance : nil
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
- Array(instance.attributes).each_with_object({}) do |attr, hash|
144
- if attr.instances.any?
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
- Array(instance.attributes).each_with_object({}) do |attr, hash|
157
- key = attr.name.to_sym
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] = coerce_attribute_value(attr, klass.attributes[key])
162
+ hash[key] = resolve_instance_value(value, nested)
161
163
  end
162
164
  end
163
165
 
164
- def coerce_attribute_value(attr, _attr_def)
165
- if attr.instances.any?
166
- attr.instances.map { |i| hydrate_instance(i) }
167
- elsif attr.value.is_a?(Array)
168
- attr.value
169
- elsif !attr.value.nil?
170
- attr.value
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 == "*" || val == "n"
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? || line =~ /^\s\*\*/
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
- $stderr.puts(
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Lml
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
7
7
  end
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-lml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.