lutaml-model 0.8.118 → 0.8.119

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: c5849a4dbcad4fe94ec8f0da69f267dd03eb3d4fdba34515dbc7bad37362e31e
4
- data.tar.gz: 0f3b29692d38dc196c88721d7fbb3d05839f405701e3863f0ced62cc693ca1a6
3
+ metadata.gz: 8df481bfb5303c05153a8bda8eaa4996157a6060d34d70dd8eb835e461aff9f5
4
+ data.tar.gz: a23f93de4f89b1a2b7925833786e9d0337ec5e94ec172723f5b2b70f78347d3c
5
5
  SHA512:
6
- metadata.gz: 4dccd043421013a6d07dfe20eb2082cc08187d1775dfdb8a3480ce8c1733df06c651c133e9114f94f18a3e1747a038cc22e2a5182f7f6212b263e7da15b7a17d
7
- data.tar.gz: c2167269f36cd4d01af8595f4e0a8465614702aa66cb2881bb8cdbf497f0760b4397afee288c93355febdd1f2ffba1f76466ddbc6e3420bec6df35f4169bffae
6
+ metadata.gz: adbc819876bf696f60009c53042dba835e5e557f32057f1aba4f58da56831c15f347d35dcf0160536a877f29f2d17ba59aadb8b1ac38b7fa9572b2d12d6c6dd2
7
+ data.tar.gz: 9af887394f9b2e241f299cb8eabcc3e0202ec51dba5129477a16cb85234105f51c7198fc44b9c20c916fa89a1356918c4752bbd73afa8f83a5a49ffd3032f08d
@@ -27,16 +27,33 @@ module Lutaml
27
27
  private_class_method :require_engine
28
28
 
29
29
  # True where the yeptris engine loads (libyeptris present for
30
- # this platform). Idempotent; a missing or broken install
31
- # reports false instead of raising, so optional-engine callers
32
- # can gate on it.
30
+ # this platform) AND answers a probe parse. A require alone
31
+ # does not prove the engine: a half-initialized Yeptris
32
+ # constant (the poison-process state the gem documents)
33
+ # passes const_defined? and require while every parse fails,
34
+ # and the native parse error then surfaces on valid input.
35
+ # Idempotent; a missing or broken install reports false
36
+ # instead of raising, so optional-engine callers (detection)
37
+ # can gate on it and fall back to the standard lane.
33
38
  def self.available?
34
39
  require_engine
35
- true
40
+ native_ready?
36
41
  rescue LoadError
37
42
  false
38
43
  end
39
44
 
45
+ def self.native_ready?
46
+ return @native_ready unless @native_ready.nil?
47
+
48
+ @native_ready = begin
49
+ ::Yeptris::JSON.load('{"native_probe": 1}')
50
+ true
51
+ rescue ::Yeptris::Error, LoadError, NoMemoryError
52
+ false
53
+ end
54
+ end
55
+ private_class_method :native_ready?
56
+
40
57
  # The parse-error contract is the standard adapter's: invalid
41
58
  # input raises JSON::ParserError regardless of the engine, so
42
59
  # consumers' rescue ladders (and this gem's own
@@ -387,13 +387,21 @@ module Lutaml
387
387
  end
388
388
 
389
389
  # Native key-value engine preference: yeptris (libyeptris C11)
390
- # when its gem is in the bundle, else the format's standard
391
- # adapter. Opt-in by bundle contents — nothing is required of
392
- # consumers that do not add yeptris.
390
+ # when its gem is in the bundle AND the native engine answers a
391
+ # probe, else the format's standard adapter. Opt-in by bundle
392
+ # contents — nothing is required of consumers that do not add
393
+ # yeptris. The probe matters because a half-initialized Yeptris
394
+ # constant (the poison-process state the gem documents) passes
395
+ # const_defined? and require while every parse fails; without
396
+ # it the lane selection flips with process load order and the
397
+ # failure surfaces as a native parse error on valid input
398
+ # instead of a clean standard-lane parse (#978).
393
399
  def detect_yeptris_kv_adapter(format)
394
- return metadata.dig(format, :default) if Lutaml::Model.opal?
400
+ default = metadata.dig(format, :default)
401
+ return default if Lutaml::Model.opal?
402
+ return default unless Utils.safe_load("yeptris", :Yeptris)
395
403
 
396
- Utils.safe_load("yeptris", :Yeptris) ? :yeptris : metadata.dig(format, :default)
404
+ load_adapter(format, :yeptris).available? ? :yeptris : default
397
405
  end
398
406
 
399
407
  # Detect available XML adapter.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.118"
5
+ VERSION = "0.8.119"
6
6
  end
7
7
  end
@@ -41,16 +41,34 @@ module Lutaml
41
41
  private_class_method :require_engine
42
42
 
43
43
  # True where the yeptris engine loads (libyeptris present for
44
- # this platform). Idempotent; a missing or broken install
45
- # reports false instead of raising, so optional-engine callers
46
- # can gate on it.
44
+ # this platform) AND answers a probe parse. A require alone
45
+ # does not prove the engine: a half-initialized Yeptris
46
+ # constant (the poison-process state the gem documents)
47
+ # passes const_defined? and require while every parse fails,
48
+ # and the native parse error then surfaces on valid input.
49
+ # Idempotent; a missing or broken install reports false
50
+ # instead of raising, so optional-engine callers (detection)
51
+ # can gate on it and fall back to the standard lane.
47
52
  def self.available?
48
53
  require_engine
49
- true
54
+ native_ready?
50
55
  rescue LoadError
51
56
  false
52
57
  end
53
58
 
59
+ def self.native_ready?
60
+ return @native_ready unless @native_ready.nil?
61
+
62
+ @native_ready = begin
63
+ # rubocop:disable-next Naming/VariableNumber -- upstream schema literal
64
+ ::Yeptris::YAML.load("native_probe: 1", schema: :compat_11)
65
+ true
66
+ rescue ::Yeptris::Error, LoadError, NoMemoryError
67
+ false
68
+ end
69
+ end
70
+ private_class_method :native_ready?
71
+
54
72
  # The parse-error contract is the standard adapter's: invalid
55
73
  # input raises Psych::SyntaxError regardless of the engine, so
56
74
  # consumers' rescue ladders (and this gem's own
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "open3"
5
+ require "lutaml/json/adapter/yeptris_adapter"
6
+
7
+ # Surface 1 of #978: the yeptris lane was selected from a bare
8
+ # constant check, so a process holding a half-initialized Yeptris
9
+ # module (the poison-process state the gem documents) committed to
10
+ # the native lane and failed pre-mapping on valid JSON — while other
11
+ # processes fell back to the standard lane, flipping the adapter
12
+ # choice across runs. Detection now probes the native engine; a
13
+ # broken native deterministically rides the standard lane.
14
+ RSpec.describe Lutaml::Model::AdapterResolver, ".adapter_for yeptris native detection" do
15
+ let(:parse_probe) do
16
+ <<~RUBY
17
+ require "lutaml/model"
18
+
19
+ klass = Class.new(Lutaml::Model::Serializable) do
20
+ attribute :v, :string
21
+ key_value { map "v", to: :v }
22
+ end
23
+
24
+ model = klass.from_json('{"v": "scalar"}')
25
+ adapter = Lutaml::Model::AdapterResolver.adapter_for(:json)
26
+ puts "ADAPTER=\#{adapter.name.split('::').last} PARSED=\#{model.v}"
27
+ RUBY
28
+ end
29
+
30
+ def subprocess_yeptris_capable?
31
+ probe = <<~RUBY
32
+ begin
33
+ require "yeptris"
34
+ exit(0)
35
+ rescue LoadError
36
+ exit(1)
37
+ end
38
+ RUBY
39
+ _out, _err, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", probe)
40
+ status.success?
41
+ end
42
+
43
+ it "resolves the yeptris lane when the native engine answers a probe" do
44
+ skip "optional yeptris engine not resolvable outside the bundle" unless subprocess_yeptris_capable?
45
+
46
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", parse_probe)
47
+ expect(status).to be_success, stderr
48
+ expect(stdout).to include("ADAPTER=YeptrisAdapter")
49
+ expect(stdout).to include("PARSED=scalar")
50
+ end
51
+
52
+ it "falls back to the standard lane when the native engine cannot parse" do
53
+ poison = <<~RUBY
54
+ require "lutaml/model"
55
+ require "yeptris"
56
+ require "lutaml/json/adapter/yeptris_adapter"
57
+
58
+ # The poison state: the module is defined and require succeeds,
59
+ # but every native parse fails. Whatever the cause (a stale
60
+ # libyeptris, a half-initialized load), the lane must not be
61
+ # selected.
62
+ Lutaml::Json::Adapter::YeptrisAdapter
63
+ .define_singleton_method(:native_ready?) { false }
64
+
65
+ #{parse_probe}
66
+ RUBY
67
+
68
+ stdout, stderr, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", poison)
69
+ expect(status).to be_success, stderr
70
+ expect(stdout).to include("ADAPTER=StandardAdapter")
71
+ expect(stdout).to include("PARSED=scalar")
72
+ end
73
+
74
+ it "reports the adapter unavailable when the native probe fails" do
75
+ allow(Lutaml::Json::Adapter::YeptrisAdapter)
76
+ .to receive(:native_ready?).and_return(false)
77
+
78
+ expect(Lutaml::Json::Adapter::YeptrisAdapter.available?).to be(false)
79
+ ensure
80
+ allow(Lutaml::Json::Adapter::YeptrisAdapter)
81
+ .to receive(:native_ready?).and_call_original
82
+ end
83
+ 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.118
4
+ version: 0.8.119
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -1900,6 +1900,7 @@ files:
1900
1900
  - spec/lutaml/model/yamls_sequence_spec.rb
1901
1901
  - spec/lutaml/model/yamls_spec.rb
1902
1902
  - spec/lutaml/model/yeptris_error_parity_spec.rb
1903
+ - spec/lutaml/model/yeptris_native_detection_spec.rb
1903
1904
  - spec/lutaml/model_spec.rb
1904
1905
  - spec/lutaml/rdf/context_spec.rb
1905
1906
  - spec/lutaml/rdf/graph_serialization_spec.rb