lutaml-model 0.8.64 → 0.8.66

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: 8a6392d2dcfd63cc0cb61e585a164c72e09438c25154651cf9ad935e32985429
4
- data.tar.gz: 492ad5dd9799652c1dd6abc9e148edba5937f3a613e143bdf556f625d94851fc
3
+ metadata.gz: f1a4f0f42471c24f887cb31b50be356825d1bb538ca568b30d15a703928c7d0b
4
+ data.tar.gz: b2b5227d2f505a10921d41123caf0f04b2cafec68ab0318db49690d394d1309a
5
5
  SHA512:
6
- metadata.gz: c14bf78b03a2cce38a13d7a7c09cc07a7cca4b1dbed59a46c0faa9a5c4c275c872dcbff23afae3f8b8518b4fa926ad64597f0f7d67a7567a711ea05d60f8754d
7
- data.tar.gz: c8d8b9723545747d826a7c16bb3d11699963299b2a238b5c04e5a724b1de555463fb839689cef9e46e52137c92a1f6831bd50676097653d888a92c57d7255f29
6
+ metadata.gz: 191a88d3dc8eacc1e94c8ea2b033500dbb4ce77345feefb6e7328527c4648c67575d10db7f9cb9e4d9611fada0902210077ae5c6ec0fcc4ff457b8637f35e2ef
7
+ data.tar.gz: fe6e34251e26d5593ec07873e5f0320a0802e0cd3c86ec89b8492c9add49c11f919dedd4aef225f6b8b63ca6564c0d3841f88d16b614fe265c8ebf3fb27fdb00
@@ -106,6 +106,13 @@ Lutaml::Model::Config.toml_adapter_type = :teptris
106
106
  raising `Psych::DisallowedClass`.
107
107
  * **JSON**: `yeptris` parsing targets exact `JSON.parse` semantics;
108
108
  generation stays on the `json` gem.
109
+ * **Error classes**: parse failures keep the standard adapters' contract
110
+ on every engine — the yeptris adapters re-raise the engine's syntax
111
+ errors as `Psych::SyntaxError` (YAML, with the engine's line, column,
112
+ and problem text) and `JSON::ParserError` (JSON), and the model layer
113
+ wraps them as `InvalidFormatError` just as it does for Psych and the
114
+ `json` gem. Code that rescues those classes does not change when a
115
+ native engine is added to the bundle.
109
116
  * **TOML**: the teptris adapter mirrors tomlib value semantics — offset
110
117
  and local datetimes become `Time`, dates become `Date`, local times
111
118
  stay `String`. Parse failures raise `Teptris::ParseError` with line
@@ -12,9 +12,32 @@ module Lutaml
12
12
  # load-only), so everything else inherits from the standard adapter.
13
13
  class YeptrisAdapter < StandardAdapter
14
14
  def self.parse(json, _options = {})
15
- require "yeptris"
15
+ require_engine
16
16
  ::Yeptris::JSON.load(json)
17
+ rescue ::Yeptris::JSON::ParseError => e
18
+ raise parser_error(e)
19
+ end
20
+
21
+ # The engine require sits outside parse's rescue so a broken
22
+ # install still surfaces its LoadError (the detection fallback's
23
+ # contract) instead of a NameError from the rescue clause.
24
+ def self.require_engine
25
+ require "yeptris"
26
+ end
27
+ private_class_method :require_engine
28
+
29
+ # The parse-error contract is the standard adapter's: invalid
30
+ # input raises JSON::ParserError regardless of the engine, so
31
+ # consumers' rescue ladders (and this gem's own
32
+ # format_error_types conversion to InvalidFormatError) hold on
33
+ # every adapter. Without the json gem the engine error
34
+ # propagates unchanged.
35
+ def self.parser_error(error)
36
+ return error unless defined?(::JSON::ParserError)
37
+
38
+ ::JSON::ParserError.new(error.message)
17
39
  end
40
+ private_class_method :parser_error
18
41
  end
19
42
  end
20
43
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.64"
5
+ VERSION = "0.8.66"
6
6
  end
7
7
  end
@@ -36,6 +36,17 @@ module Lutaml
36
36
 
37
37
  class << self
38
38
  def compile(model_class, register)
39
+ # A child model's declared lutaml_default_register takes
40
+ # precedence over the ambient (parent) register — the same
41
+ # contract the interpretive path applies through
42
+ # Register.resolve_for_child. Without this, plan compilation
43
+ # resolves the child's symbol attribute types in the parent
44
+ # context and raises UnknownTypeError for ids registered only
45
+ # in the child's own register (#876).
46
+ register = Lutaml::Model::Register.resolve_for_child(
47
+ model_class, register
48
+ )
49
+
39
50
  key = [model_class, register]
40
51
  return PLAN_CACHE[key] if PLAN_CACHE.key?(key)
41
52
 
@@ -20,11 +20,21 @@ module Lutaml
20
20
  # node: the parsed source element (leptris); ordered/mixed
21
21
  # plans rebuild element_order from it and ordered children
22
22
  # hydrate natively against their own nodes
23
- def call(model_class, plan, value, parent: nil, node: nil)
23
+ def call(model_class, plan, value, parent: nil, node: nil,
24
+ register: nil)
25
+ # The child model's declared lutaml_default_register takes
26
+ # precedence over the ambient register — the same
27
+ # resolve_for_child contract the interpretive path applies.
28
+ # Without it, hydration resolves the child's symbol attribute
29
+ # types in the parent context and raises UnknownTypeError
30
+ # for ids registered only in the child's register (#876).
31
+ register = Lutaml::Model::Register.resolve_for_child(
32
+ model_class, register || Lutaml::Model::Config.default_register
33
+ )
24
34
  buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
25
35
  attr_kwargs = attributes_kwargs(plan, value, node)
26
36
  child_kwargs, children, delegates =
27
- children_kwargs(model_class, plan, value, buckets)
37
+ children_kwargs(model_class, plan, value, buckets, register)
28
38
  plan[:collection_defaults].each do |name|
29
39
  next if attr_kwargs.key?(name) || child_kwargs.key?(name)
30
40
 
@@ -80,7 +90,9 @@ module Lutaml
80
90
  # — child instances come back so the caller can decorate
81
91
  # parent/root links once the parent exists; delegate values
82
92
  # wait for the instance (their target object must exist).
83
- def children_kwargs(_model_class, plan, value, buckets = nil)
93
+ def children_kwargs(_model_class, plan, value, buckets = nil,
94
+ register = nil)
95
+ register ||= Lutaml::Model::Config.default_register
84
96
  grouped, tagged = group_children(value, plan[:row_tags])
85
97
  buckets = nil unless buckets && plan[:needs_nodes]
86
98
  kwargs = {}
@@ -126,7 +138,8 @@ module Lutaml
126
138
  child_plan = PlanCompiler.compile(child_type, register)
127
139
  items = buckets.fetch(rule.name.to_s, []).map do |n|
128
140
  call(child_type, child_plan,
129
- child_plan[:descriptor].walk(n), node: n)
141
+ child_plan[:descriptor].walk(n), node: n,
142
+ register: register)
130
143
  end
131
144
  unless items.empty?
132
145
  children.concat(items)
@@ -162,7 +175,7 @@ module Lutaml
162
175
  items = grouped.fetch(rule.name.to_s, []).each_with_index
163
176
  .map do |v, i|
164
177
  call(child_type, child_plan, v,
165
- node: cursor && cursor[i])
178
+ node: cursor && cursor[i], register: register)
166
179
  end
167
180
  next if items.empty?
168
181
 
@@ -25,16 +25,24 @@ module Lutaml
25
25
 
26
26
  class << self
27
27
  # plan: the compiler's entry for instance's class
28
- def call(instance, plan)
28
+ def call(instance, plan, register = nil)
29
+ # The child model's declared lutaml_default_register takes
30
+ # precedence over the ambient register — the resolve_for_child
31
+ # contract (#876); without it, child plans and attribute
32
+ # serialization resolve in the parent context.
33
+ register = Lutaml::Model::Register.resolve_for_child(
34
+ instance.class,
35
+ register || Lutaml::Model::Config.default_register,
36
+ )
29
37
  return nil if plan[:ordered] && instance.element_order.nil?
30
38
 
31
39
  doc = ::Leptris::XML::Document.create
32
40
  root = doc.create_element(plan[:tree][:name])
33
41
  doc.root = root
34
42
  if plan[:ordered]
35
- build_ordered(root, instance, plan, doc)
43
+ build_ordered(root, instance, plan, doc, register)
36
44
  else
37
- build(root, instance, plan, doc)
45
+ build(root, instance, plan, doc, register)
38
46
  end
39
47
  doc.to_xml(indent: 2, no_decl: true)
40
48
  end
@@ -54,8 +62,9 @@ module Lutaml
54
62
  Lutaml::Model::Config.default_register
55
63
  end
56
64
 
57
- def build(element, instance, plan, doc)
58
- write_attributes(element, instance, plan)
65
+ def build(element, instance, plan, doc, reg = nil)
66
+ reg ||= register
67
+ write_attributes(element, instance, plan, reg)
59
68
  plan[:rows].each do |rule, attr, kind, spelling, delegate|
60
69
  value = value_of(instance, attr, delegate)
61
70
  next unless rule.render?(value, instance)
@@ -67,24 +76,25 @@ module Lutaml
67
76
  # as the interpretive writer does.
68
77
  Array(value).each do |item|
69
78
  add_leaf(element, row_name(rule, spelling),
70
- attr.serialize(item, :xml, register), doc,
79
+ attr.serialize(item, :xml, reg), doc,
71
80
  attrs: rule.when_attribute)
72
81
  end
73
82
  when :collection_native, :collection_cb
74
83
  Array(value).each do |item|
75
84
  add_leaf(element, rule.name.to_s,
76
- attr.serialize(item, :xml, register), doc,
85
+ attr.serialize(item, :xml, reg), doc,
77
86
  attrs: rule.when_attribute)
78
87
  end
79
88
  when :nested, :ordered_deferred
80
89
  Array(value).each do |item|
81
- child_plan = PlanCompiler.compile(item.class, register)
90
+ child_plan = PlanCompiler.compile(item.class, reg)
82
91
  child = element.create_child(child_plan[:tree][:name])
83
92
  if child_plan[:ordered]
84
- return nil unless build_ordered(child, item, child_plan, doc)
93
+ return nil unless build_ordered(child, item, child_plan,
94
+ doc, reg)
85
95
 
86
96
  else
87
- build(child, item, child_plan, doc)
97
+ build(child, item, child_plan, doc, reg)
88
98
  end
89
99
  end
90
100
  when :raw
@@ -103,8 +113,9 @@ module Lutaml
103
113
  # the model are reflected; element_order text is the fallback.
104
114
  # PIs drop (interpretive parity). Returns nil when a nested
105
115
  # ordered child lacks element_order (caller falls back).
106
- def build_ordered(element, instance, plan, doc)
107
- write_attributes(element, instance, plan)
116
+ def build_ordered(element, instance, plan, doc, reg = nil)
117
+ reg ||= register
118
+ write_attributes(element, instance, plan, reg)
108
119
  rows_by_name = {}
109
120
  plan[:rows].each do |rule, attr, kind, spelling, delegate|
110
121
  next unless rule.name
@@ -162,7 +173,7 @@ module Lutaml
162
173
 
163
174
  element_indices[object.name] += 1
164
175
  add_leaf(element, object.name,
165
- attr.serialize(items[index], :xml, register), doc)
176
+ attr.serialize(items[index], :xml, reg), doc)
166
177
  when :nested, :ordered_deferred
167
178
  item = if attr.collection?
168
179
  index = element_indices[object.name]
@@ -173,18 +184,19 @@ module Lutaml
173
184
  end
174
185
  next unless item
175
186
 
176
- child_plan = PlanCompiler.compile(item.class, register)
187
+ child_plan = PlanCompiler.compile(item.class, reg)
177
188
  child = element.create_child(child_plan[:tree][:name])
178
189
  if child_plan[:ordered]
179
- return nil unless build_ordered(child, item, child_plan, doc)
190
+ return nil unless build_ordered(child, item, child_plan,
191
+ doc, reg)
180
192
  else
181
- build(child, item, child_plan, doc)
193
+ build(child, item, child_plan, doc, reg)
182
194
  end
183
195
  when :raw
184
196
  element.add_child(value.to_s) unless value.nil?
185
197
  when :scalar
186
198
  add_leaf(element, object.name,
187
- attr.serialize(value, :xml, register), doc)
199
+ attr.serialize(value, :xml, reg), doc)
188
200
  end
189
201
  end
190
202
  end
@@ -193,7 +205,7 @@ module Lutaml
193
205
 
194
206
  # Attributes in document order when the instance recorded it
195
207
  # (parsed models), else plan row order.
196
- def write_attributes(element, instance, plan)
208
+ def write_attributes(element, instance, plan, _reg = nil)
197
209
  recorded = instance.attribute_order
198
210
  if recorded && !recorded.empty?
199
211
  by_name = {}
@@ -213,7 +225,7 @@ module Lutaml
213
225
  end
214
226
  end
215
227
 
216
- def write_attribute(element, instance, name, entry)
228
+ def write_attribute(element, instance, name, entry, reg = nil)
217
229
  return unless entry
218
230
 
219
231
  _rule, attr, delegate = entry
@@ -221,7 +233,7 @@ module Lutaml
221
233
  return if value.nil?
222
234
 
223
235
  element.set_attribute(name,
224
- attr.serialize(value, :xml, register).to_s)
236
+ attr.serialize(value, :xml, reg || register).to_s)
225
237
  end
226
238
 
227
239
  # Partition rows (#88) re-emit their discriminator: the wire
@@ -14,18 +14,50 @@ module Lutaml
14
14
  # Semantic notes vs the standard adapter's Psych.safe_load:
15
15
  # implicit typing is parity-verified (compat_11 schema); anchors
16
16
  # and aliases always resolve; a tagged value without a core type
17
- # materializes as plain data instead of raising DisallowedClass.
17
+ # materializes as plain data instead of raising DisallowedClass;
18
+ # parse failures surface as Psych::SyntaxError like the standard
19
+ # adapter's (the engine's own ParseError is re-raised under the
20
+ # standard class).
18
21
  class YeptrisAdapter < StandardAdapter
19
22
  # yeptris is required lazily: the ruby-platform variant installs
20
23
  # everywhere but only loads where libyeptris exists (no Windows
21
24
  # prebuilts) — a broken install must degrade to detection
22
25
  # fallback, never crash loads.
23
26
  def self.parse(yaml, _options = {})
24
- require "yeptris"
27
+ require_engine
25
28
  require "yeptris/yaml"
26
29
  # rubocop:disable-next Naming/VariableNumber -- upstream schema literal
27
30
  Yeptris::YAML.load(yaml, schema: :compat_11)
31
+ rescue Yeptris::ParseError => e
32
+ raise syntax_error(e)
33
+ end
34
+
35
+ # The engine require sits outside parse's rescue so a broken
36
+ # install still surfaces its LoadError (the detection fallback's
37
+ # contract) instead of a NameError from the rescue clause.
38
+ def self.require_engine
39
+ require "yeptris"
40
+ end
41
+ private_class_method :require_engine
42
+
43
+ # The parse-error contract is the standard adapter's: invalid
44
+ # input raises Psych::SyntaxError regardless of the engine, so
45
+ # consumers' rescue ladders (and this gem's own
46
+ # format_error_types conversion to InvalidFormatError) hold on
47
+ # every adapter. The C parser reports the position inside the
48
+ # error message ("... at line L, column C"); lift it into the
49
+ # structured fields. Without psych (Opal) the engine error
50
+ # propagates unchanged.
51
+ def self.syntax_error(error)
52
+ return error unless defined?(::Psych::SyntaxError)
53
+
54
+ match = /\bline (\d+),? column (\d+)/.match(error.message)
55
+ line = match ? match[1].to_i : 0
56
+ column = match ? match[2].to_i : 0
57
+ problem = error.message.sub(/\s+at\s+line\s+\d+,?\s+column\s+\d+\z/, "")
58
+ ::Psych::SyntaxError.new(nil, line, column, 0, problem, nil)
28
59
  end
60
+ private_class_method :syntax_error
29
61
 
30
62
  def to_yaml(_options = {})
31
63
  attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
@@ -25,9 +25,9 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Objec
25
25
  .to eq("a" => 1.5, "b" => nil, "c" => true, "d" => "1e3")
26
26
  end
27
27
 
28
- it "raises on invalid JSON" do
28
+ it "raises JSON::ParserError on invalid JSON, like the standard adapter" do
29
29
  expect { described_class.parse("{nope") }
30
- .to raise_error(Yeptris::JSON::ParseError)
30
+ .to raise_error(JSON::ParserError)
31
31
  end
32
32
  end
33
33
 
@@ -59,5 +59,10 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Json::Adapter::YeptrisAdapter : Objec
59
59
  expect(model.roles).to eq(%w[admin dev])
60
60
  expect(model_class.from_json(model.to_json).roles).to eq(%w[admin dev])
61
61
  end
62
+
63
+ it "surfaces invalid JSON as InvalidFormatError" do
64
+ expect { model_class.from_json("{nope") }
65
+ .to raise_error(Lutaml::Model::InvalidFormatError)
66
+ end
62
67
  end
63
68
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "spec_helper"
5
+
6
+ # The suite deliberately never loads the optional yeptris engine
7
+ # in-process (adapter_resolver_configured_spec asserts the resolver's
8
+ # lazy-detection guarantee), so the error-class contract is exercised
9
+ # in a subprocess: wherever the engine is installed it is the
10
+ # preferred adapter, and its parse failures must surface under the
11
+ # standard adapters' error classes — Psych::SyntaxError and
12
+ # JSON::ParserError from the adapters, Lutaml::Model::InvalidFormatError
13
+ # through a model — so downstream rescue ladders hold on every engine.
14
+ RSpec.describe "yeptris adapter error parity" do
15
+ # A direct ruby invocation with explicit load paths and NO shell:
16
+ # a nested `bundle exec` produced empty output on some CI legs, and
17
+ # backticks route through cmd.exe on Windows where POSIX quoting
18
+ # mangles the command. Open3 with array arguments is shell-free on
19
+ # every platform.
20
+ subject(:outcomes) do
21
+ libs = %w[lutaml-model yeptris yeptris-ruby moxml leptris].filter_map do |g|
22
+ "#{Gem::Specification.find_by_name(g).gem_dir}/lib"
23
+ rescue Gem::LoadError
24
+ nil
25
+ end
26
+ args = libs.flat_map { |l| ["-I", l] } + ["-e", probe]
27
+ Open3.capture2e(Gem.ruby, *args).first
28
+ end
29
+
30
+ def yeptris_installed?
31
+ Gem::Specification.find_by_name("yeptris")
32
+ true
33
+ rescue Gem::LoadError
34
+ false
35
+ end
36
+
37
+ # The probe requires the engine explicitly (the resolved adapters
38
+ # prove the preference), then reports one outcome line per surface.
39
+ let(:probe) do
40
+ <<~RUBY
41
+ require "lutaml/model"
42
+
43
+ def outcome
44
+ yield
45
+ "NO-ERROR"
46
+ rescue StandardError => e
47
+ "\#{e.class.name}: \#{e.message}"
48
+ end
49
+
50
+ puts "yaml-adapter: \#{Lutaml::Model::AdapterResolver.adapter_for(:yaml).name}"
51
+ puts "json-adapter: \#{Lutaml::Model::AdapterResolver.adapter_for(:json).name}"
52
+
53
+ puts "adapter-yaml: \#{outcome do
54
+ Lutaml::Yaml::Adapter::YeptrisAdapter.parse("name: test\\n invalid: [unclosed\\n")
55
+ end }"
56
+ puts "adapter-json: \#{outcome do
57
+ Lutaml::Json::Adapter::YeptrisAdapter.parse("{nope")
58
+ end }"
59
+
60
+ model_class = Class.new(Lutaml::Model::Serializable) do
61
+ attribute :name, :string
62
+ key_value { map "name", to: :name }
63
+ end
64
+ puts "model-yaml: \#{outcome do
65
+ model_class.from_yaml("name: test\\n invalid: [unclosed\\n")
66
+ end }"
67
+ puts "model-json: \#{outcome do
68
+ model_class.from_json("{nope")
69
+ end }"
70
+ RUBY
71
+ end
72
+
73
+ before { skip "yeptris is not installed" unless yeptris_installed? }
74
+
75
+ it "prefers the yeptris adapters when the engine is installed" do
76
+ expect(outcomes).to include("yaml-adapter: Lutaml::Yaml::Adapter::YeptrisAdapter")
77
+ expect(outcomes).to include("json-adapter: Lutaml::Json::Adapter::YeptrisAdapter")
78
+ end
79
+
80
+ it "raises Psych::SyntaxError from the yaml adapter for invalid YAML" do
81
+ expect(outcomes).to match(/^adapter-yaml: Psych::SyntaxError: .+ at line \d+/)
82
+ end
83
+
84
+ it "raises JSON::ParserError from the json adapter for invalid JSON" do
85
+ expect(outcomes).to match(/^adapter-json: JSON::ParserError: /)
86
+ end
87
+
88
+ it "raises InvalidFormatError through a model for invalid YAML" do
89
+ expect(outcomes).to match(/^model-yaml: Lutaml::Model::InvalidFormatError: /)
90
+ end
91
+
92
+ it "raises InvalidFormatError through a model for invalid JSON" do
93
+ expect(outcomes).to match(/^model-json: Lutaml::Model::InvalidFormatError: /)
94
+ end
95
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "Cross-register embedding on the plan paths (#876)" do
6
+ before do
7
+ stub_const("CrossRegister876", Module.new)
8
+ stub_const("CrossRegister876::Part", part_class)
9
+ stub_const("CrossRegister876::Widget", widget_class)
10
+ stub_const("Host876", host_class)
11
+
12
+ ctx = Lutaml::Model::GlobalContext.create_context(
13
+ id: :crossregister876, registry: Lutaml::Model::TypeRegistry.new,
14
+ )
15
+ ctx.registry.register(:part, CrossRegister876::Part)
16
+ ctx.registry.register(:widget, CrossRegister876::Widget)
17
+ end
18
+
19
+ let(:part_class) do
20
+ Class.new(Lutaml::Model::Serializable) do
21
+ def self.lutaml_default_register
22
+ :crossregister876
23
+ end
24
+
25
+ xml do
26
+ element "part"
27
+ end
28
+ end
29
+ end
30
+
31
+ let(:widget_class) do
32
+ klass = Class.new(Lutaml::Model::Serializable) do
33
+ def self.lutaml_default_register
34
+ :crossregister876
35
+ end
36
+
37
+ attribute :part_value, :part, collection: true
38
+
39
+ xml do
40
+ element "widget"
41
+ ordered
42
+ map_element "part", to: :part_value
43
+ end
44
+ end
45
+ klass
46
+ end
47
+
48
+ let(:host_class) do
49
+ Class.new(Lutaml::Model::Serializable) do
50
+ attribute :widget, CrossRegister876::Widget
51
+
52
+ xml do
53
+ element "host"
54
+ map_element "widget", to: :widget
55
+ end
56
+ end
57
+ end
58
+
59
+ let(:xml) { "<host><widget><part/></widget></host>" }
60
+
61
+ it "resolves the child's symbol types through the child register" do
62
+ model = Host876.from_xml(xml)
63
+ expect(model.widget.part_value.first).to be_a(CrossRegister876::Part)
64
+ end
65
+
66
+ it "round-trips through both the plan serializer and back" do
67
+ model = Host876.from_xml(xml)
68
+ xml_out = model.to_xml
69
+ expect(xml_out).to include("<widget>")
70
+ expect(xml_out).to include("<part/>")
71
+
72
+ reparsed = Host876.from_xml(xml_out)
73
+ expect(reparsed.widget.part_value.first).to be_a(CrossRegister876::Part)
74
+ end
75
+ end
@@ -30,6 +30,30 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Yaml::Adapter::YeptrisAdapter : Objec
30
30
  expect(described_class.parse("d: 2020-01-01\n"))
31
31
  .to eq("d" => Date.new(2020, 1, 1))
32
32
  end
33
+
34
+ it "raises Psych::SyntaxError for invalid YAML, like the standard adapter" do
35
+ expect { described_class.parse("name: test\n invalid: [unclosed\n") }
36
+ .to raise_error(Psych::SyntaxError)
37
+ end
38
+
39
+ it "raises Psych::SyntaxError for a mapping value inside a plain scalar" do
40
+ # The dependent-CI shape: one line, a second "key:" colon after a
41
+ # plain scalar run. Psych rejects it with "mapping values are not
42
+ # allowed in this context"; the engine must surface under the
43
+ # same class so downstream rescue ladders hold.
44
+ expect { described_class.parse('name: test\n invalid: [unclosed') }
45
+ .to raise_error(Psych::SyntaxError)
46
+ end
47
+
48
+ it "carries the engine's position on the syntax error" do
49
+ expect { described_class.parse("name: test\n invalid: [unclosed\n") }
50
+ .to raise_error(Psych::SyntaxError) do |error|
51
+ expect(error.line).to be >= 1
52
+ expect(error.problem).to be_a(String)
53
+ expect(error.problem).not_to be_empty
54
+ expect(error.message).to include("at line #{error.line}")
55
+ end
56
+ end
33
57
  end
34
58
 
35
59
  describe "#to_yaml" do
@@ -59,5 +83,10 @@ RSpec.describe(YEPTRIS_AVAILABLE ? Lutaml::Yaml::Adapter::YeptrisAdapter : Objec
59
83
  expect(model.roles).to eq(%w[admin dev])
60
84
  expect(model_class.from_yaml(model.to_yaml).roles).to eq(%w[admin dev])
61
85
  end
86
+
87
+ it "surfaces invalid YAML as InvalidFormatError" do
88
+ expect { model_class.from_yaml("name: test\n invalid: [unclosed\n") }
89
+ .to raise_error(Lutaml::Model::InvalidFormatError)
90
+ end
62
91
  end
63
92
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.64
4
+ version: 0.8.66
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -1880,6 +1880,7 @@ files:
1880
1880
  - spec/lutaml/model/yamls_range_spec.rb
1881
1881
  - spec/lutaml/model/yamls_sequence_spec.rb
1882
1882
  - spec/lutaml/model/yamls_spec.rb
1883
+ - spec/lutaml/model/yeptris_error_parity_spec.rb
1883
1884
  - spec/lutaml/model_spec.rb
1884
1885
  - spec/lutaml/rdf/context_spec.rb
1885
1886
  - spec/lutaml/rdf/graph_serialization_spec.rb
@@ -1916,6 +1917,7 @@ files:
1916
1917
  - spec/lutaml/xml/conformance/xml_schema_instance_spec.rb
1917
1918
  - spec/lutaml/xml/content_model_validation_spec.rb
1918
1919
  - spec/lutaml/xml/cross_class_dispatch_spec.rb
1920
+ - spec/lutaml/xml/cross_register_plan_spec.rb
1919
1921
  - spec/lutaml/xml/data_model_spec.rb
1920
1922
  - spec/lutaml/xml/decisions/decision_engine_spec.rb
1921
1923
  - spec/lutaml/xml/decisions/decision_spec.rb