lutaml-model 0.8.117 → 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: 49af9f0bc6f8dd954e93d9d3ac641e212dbf526486c9c8a2eb3460ccfbf01cbf
4
- data.tar.gz: cf20d264b0827b500c0dde7340b721c5ea89235341a55ef603f3e41dd461a31a
3
+ metadata.gz: 8df481bfb5303c05153a8bda8eaa4996157a6060d34d70dd8eb835e461aff9f5
4
+ data.tar.gz: a23f93de4f89b1a2b7925833786e9d0337ec5e94ec172723f5b2b70f78347d3c
5
5
  SHA512:
6
- metadata.gz: 62359095df53c8835748e08c60c8648f6e07a4e892536da699ded2f255ec176a8aee3e648012d27e62909af5e34426118f465d93adf616ed005e22273e990fbc
7
- data.tar.gz: 7e1d1f54a24252a44a31e2a0d45e878c13b9a5d44115a856aafd383cf24adb731ad585d52b8a8d9d648557ee150e2c0b776a1edd18b405bf16c79e76fbf4c2a8
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.
@@ -5,6 +5,20 @@ module Lutaml
5
5
  class Attribute
6
6
  attr_reader :name, :options
7
7
 
8
+ # Format scalars a mapped-model attribute may be handed in the
9
+ # key-value lanes (#978). Adapter element nodes and other object
10
+ # forms belong to the mapping lane and must not divert.
11
+ SCALAR_CAST_TYPES = [
12
+ ::String,
13
+ ::Numeric,
14
+ ::Symbol,
15
+ ::Time,
16
+ ::Date,
17
+ ::DateTime,
18
+ ::TrueClass,
19
+ ::FalseClass,
20
+ ].freeze
21
+
8
22
  include CollectionHandler
9
23
  include DeepDupable
10
24
  include RestrictionValidation
@@ -982,9 +996,11 @@ instance_object = nil)
982
996
  # String, #978). A type declaring its own .cast owns scalar
983
997
  # construction; base-cast identity types fall through to the
984
998
  # legacy conversion chain, whose parse error names the token.
999
+ # Non-scalar input (adapter elements, hashes, arrays) rides
1000
+ # the mapping lane untouched.
985
1001
  if serializable_type?(register) && !value.nil? &&
986
- !value.is_a?(::Hash) && !value.is_a?(::Array) &&
987
- !value.respond_to?(:key?) && klass.respond_to?(:cast) &&
1002
+ SCALAR_CAST_TYPES.any? { |type| value.is_a?(type) } &&
1003
+ klass.respond_to?(:cast) &&
988
1004
  klass.method(:cast).owner == klass.singleton_class
989
1005
  casted = klass.cast(value)
990
1006
  return casted if casted.is_a?(klass)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.117"
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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # The scalar-cast diversion (#978) must not capture element-form input:
6
+ # a type declaring its own .cast still receives parsed child elements
7
+ # through the mapping lane, which builds the model from its XML
8
+ # mapping. Diverting those into .cast raised IncorrectModelError
9
+ # downstream (uniword shared-schemas round-trips on 0.8.116/0.8.117).
10
+ RSpec.describe "cast routing for element-form input" do
11
+ let(:styled_class) do
12
+ Class.new(Lutaml::Model::Serializable) do
13
+ attribute :value, :string
14
+
15
+ xml do
16
+ element "style"
17
+ map_attribute "value", to: :value
18
+ end
19
+
20
+ key_value do
21
+ map "value", to: :value
22
+ end
23
+
24
+ def self.cast(value)
25
+ return value if value.is_a?(self)
26
+
27
+ new(value: value.to_s)
28
+ end
29
+ end
30
+ end
31
+
32
+ let(:host_class) do
33
+ Class.new(Lutaml::Model::Serializable) do
34
+ attribute :style, "CastStyled"
35
+
36
+ xml do
37
+ element "host"
38
+ map_element "style", to: :style
39
+ end
40
+ end
41
+ end
42
+
43
+ before do
44
+ stub_const("CastStyled", styled_class)
45
+ stub_const("CastHost", host_class)
46
+ end
47
+
48
+ it "builds element-form input through the mapping lane" do
49
+ model = CastHost.from_xml('<host><style value="normal"/></host>')
50
+
51
+ expect(model.style).to be_a(CastStyled)
52
+ expect(model.style.value).to eq("normal")
53
+ end
54
+ end
@@ -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,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.117
4
+ version: 0.8.119
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-19 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -1713,6 +1713,7 @@ files:
1713
1713
  - spec/lutaml/model/boot_manifest_spec.rb
1714
1714
  - spec/lutaml/model/cached_type_resolver_spec.rb
1715
1715
  - spec/lutaml/model/cardinality_strict_spec.rb
1716
+ - spec/lutaml/model/cast_element_form_spec.rb
1716
1717
  - spec/lutaml/model/cdata_spec.rb
1717
1718
  - spec/lutaml/model/choice_restrict_spec.rb
1718
1719
  - spec/lutaml/model/choice_spec.rb
@@ -1899,6 +1900,7 @@ files:
1899
1900
  - spec/lutaml/model/yamls_sequence_spec.rb
1900
1901
  - spec/lutaml/model/yamls_spec.rb
1901
1902
  - spec/lutaml/model/yeptris_error_parity_spec.rb
1903
+ - spec/lutaml/model/yeptris_native_detection_spec.rb
1902
1904
  - spec/lutaml/model_spec.rb
1903
1905
  - spec/lutaml/rdf/context_spec.rb
1904
1906
  - spec/lutaml/rdf/graph_serialization_spec.rb