lutaml-model 0.8.65 → 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: a8bbc0a4da8114a582ee834a333a8614ca1051747296a1e967eb36f47ef68b48
4
- data.tar.gz: ec01b4058ae9ce9d400f229ae4f116bcdde25cdd015e3b96ff3e2b39af62ff10
3
+ metadata.gz: f1a4f0f42471c24f887cb31b50be356825d1bb538ca568b30d15a703928c7d0b
4
+ data.tar.gz: b2b5227d2f505a10921d41123caf0f04b2cafec68ab0318db49690d394d1309a
5
5
  SHA512:
6
- metadata.gz: 494b45969f4e8f67e5f79fe09eb6e1630e5b04109fe46b13210cc8dfe455a11e7b7d7955a91ae2132c037c7e53d21fb09c0462a55899cccaafc4d4e5b609bccd
7
- data.tar.gz: 6694a345fda4b371988fab2af07b9311b7eed87f2e61a696eab7c2a247ba04950bbb93439b1019a27903dbfcf8c5815067f2d3449a41affe2cdb21a6a3deb583
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.65"
5
+ VERSION = "0.8.66"
6
6
  end
7
7
  end
@@ -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
@@ -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.65
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