lutaml-model 0.8.94 → 0.8.96

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.
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XML plan fast path: type-namespaced attribute rows" do
6
+ # Attribute identity is (URI, local) (lutaml-model#744). A map_attribute
7
+ # whose TYPE carries a namespace class used to opt the whole model out
8
+ # of the plan path; it now compiles into an exact-URI AttrPlan ns row
9
+ # (libleptris 1.9.289) with the sole-claimant lenient recovery keeping
10
+ # the interpretive exact-first-then-any-qualification precedence.
11
+
12
+ let(:wml_ns) do
13
+ Class.new(Lutaml::Xml::Namespace) do
14
+ uri "http://example.com/type-ns/wml"
15
+ prefix_default "w"
16
+ end
17
+ end
18
+
19
+ let(:val_type) do
20
+ stub_const("PlanTypeNsAttr::WmlNs", wml_ns)
21
+ type = Class.new(Lutaml::Model::Type::String) do
22
+ include Lutaml::Xml::Type::Configurable
23
+
24
+ xml do
25
+ namespace PlanTypeNsAttr::WmlNs
26
+ end
27
+ end
28
+ stub_const("PlanTypeNsAttr::ValType", type)
29
+ type
30
+ end
31
+
32
+ let(:klass) do
33
+ val_type
34
+ model = Class.new(Lutaml::Model::Serializable) do
35
+ attribute :val, PlanTypeNsAttr::ValType
36
+
37
+ xml do
38
+ element "keepNext"
39
+ namespace PlanTypeNsAttr::WmlNs
40
+ map_attribute "val", to: :val
41
+ end
42
+ end
43
+ stub_const("PlanTypeNsAttr::KeepNext", model)
44
+ model
45
+ end
46
+
47
+ around do |example|
48
+ old = Lutaml::Model::Config.instance.xml_plan_fast_path
49
+ Lutaml::Model::Config.instance.xml_plan_fast_path = true
50
+ Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
51
+ ensure
52
+ Lutaml::Model::Config.instance.xml_plan_fast_path = old
53
+ end
54
+
55
+ it "compiles a model whose attribute type carries a namespace" do
56
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
57
+ klass, Lutaml::Model::Config.default_register
58
+ )
59
+
60
+ expect(plan).not_to be_nil
61
+ ns_row = plan[:tree][:attributes].find { |a| a[:name] == "val" }
62
+ expect(ns_row[:ns]).to include(exact: "http://example.com/type-ns/wml")
63
+ expect(ns_row[:ns][:prefix]).to eq("w")
64
+ end
65
+
66
+ it "binds the attribute from the type's namespace" do
67
+ doc = %(<w:keepNext xmlns:w="http://example.com/type-ns/wml" w:val="yes"/>)
68
+
69
+ expect(klass.from_xml(doc).val).to eq("yes")
70
+ end
71
+
72
+ # The interpretive matcher refuses an ambiguous local name outright:
73
+ # a foreign spelling alongside the canonical one binds nothing. The
74
+ # exact-URI row mirrors that refusal (lutaml-model#744).
75
+ it "refuses to bind when a foreign spelling accompanies the canonical one" do
76
+ doc = '<w:keepNext xmlns:w="http://example.com/type-ns/wml" ' \
77
+ 'xmlns:f="http://example.com/foreign" val="foreign" w:val="canonical"/>'
78
+
79
+ expect(klass.from_xml(doc).val).to be_nil
80
+ end
81
+
82
+ # Interpretive parity: the plan path resolves exactly as the
83
+ # interpretive matcher (nokogiri) does on every spelling shape.
84
+ it "matches the interpretive path's resolution on all spelling shapes" do
85
+ canonical = %(<w:keepNext xmlns:w="http://example.com/type-ns/wml" w:val="yes"/>)
86
+ ambiguous = '<w:keepNext xmlns:w="http://example.com/type-ns/wml" ' \
87
+ 'xmlns:f="http://example.com/foreign" val="foreign" w:val="canonical"/>'
88
+ foreign_only = '<w:keepNext xmlns:f="http://example.com/foreign" ' \
89
+ 'xmlns:w="http://example.com/type-ns/wml" val="foreign"/>'
90
+
91
+ [canonical, ambiguous, foreign_only].each do |doc|
92
+ interpretive = Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
93
+ klass.from_xml(doc).val
94
+ end
95
+
96
+ expect(klass.from_xml(doc).val).to eq(interpretive)
97
+ end
98
+ end
99
+
100
+ # A type declaring alias URI families stays interpretive: the plan row
101
+ # holds one exact URI and non-sole-claimant alias families have no
102
+ # plan-side resolution order.
103
+ it "keeps alias-declaring types on the interpretive path" do
104
+ aliased_ns = Class.new(Lutaml::Xml::Namespace) do
105
+ uri "http://example.com/type-ns/canonical"
106
+ uri_aliases "http://example.com/type-ns/alias"
107
+ end
108
+ stub_const("PlanTypeNsAttr::AliasedNs", aliased_ns)
109
+ type = Class.new(Lutaml::Model::Type::String) do
110
+ include Lutaml::Xml::Type::Configurable
111
+
112
+ xml do
113
+ namespace PlanTypeNsAttr::AliasedNs
114
+ end
115
+ end
116
+ stub_const("PlanTypeNsAttr::AliasedValType", type)
117
+ model = Class.new(Lutaml::Model::Serializable) do
118
+ attribute :val, PlanTypeNsAttr::AliasedValType
119
+
120
+ xml do
121
+ element "keepNext"
122
+ map_attribute "val", to: :val
123
+ end
124
+ end
125
+ stub_const("PlanTypeNsAttr::AliasedKeepNext", model)
126
+
127
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(
128
+ model, Lutaml::Model::Config.default_register
129
+ )).to be_nil
130
+ end
131
+
132
+ # Engines older than the AttrPlan ns form (libleptris 1.9.289) keep
133
+ # the opt-out — the existing version-gate protocol.
134
+ it "opts out when the engine predates attribute ns rows" do
135
+ allow(Lutaml::Xml::Leptris).to receive(:attr_ns_rows_compatible?)
136
+ .and_return(false)
137
+
138
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(
139
+ klass, Lutaml::Model::Config.default_register
140
+ )).to be_nil
141
+ end
142
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe "XML plan fast path: map_any_element catch-all" do
6
+ # leptris#1552: the wildcard child row rides a two-pass walk where
7
+ # named rows take precedence; the hydrator bridges the unclaimed
8
+ # remainder in document order and casts it through the attribute —
9
+ # the interpretive pass's contract for map_any_element.
10
+
11
+ let(:host_class) do
12
+ klass = Class.new(Lutaml::Model::Serializable) do
13
+ attribute :title, :string
14
+ attribute :others, :string, collection: true
15
+
16
+ xml do
17
+ element "host"
18
+ map_element "title", to: :title
19
+ map_any_element to: :others
20
+ end
21
+ end
22
+ stub_const("Gate4Any::Host", klass)
23
+ klass
24
+ end
25
+
26
+ let(:typed_class) do
27
+ item = Class.new(Lutaml::Model::Serializable) do
28
+ attribute :content, :string
29
+
30
+ xml do
31
+ element "item"
32
+ map_content to: :content
33
+ end
34
+ end
35
+ stub_const("Gate4Any::Item", item)
36
+ model = Class.new(Lutaml::Model::Serializable) do
37
+ attribute :label, :string
38
+ attribute :items, Gate4Any::Item, collection: true
39
+
40
+ xml do
41
+ element "bag"
42
+ map_element "label", to: :label
43
+ map_any_element to: :items
44
+ end
45
+ end
46
+ stub_const("Gate4Any::Bag", model)
47
+ model
48
+ end
49
+
50
+ around do |example|
51
+ old = Lutaml::Model::Config.instance.xml_plan_fast_path
52
+ Lutaml::Model::Config.instance.xml_plan_fast_path = true
53
+ Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
54
+ ensure
55
+ Lutaml::Model::Config.instance.xml_plan_fast_path = old
56
+ end
57
+
58
+ # Members flow through the attribute's cast exactly as the
59
+ # interpretive pass does: String attributes stringify the wrapper
60
+ # (both paths identically), Serializable attributes build instances.
61
+ def member_shapes(elements)
62
+ elements.map { |e| e.class.name.split("::").last }
63
+ end
64
+
65
+ it "captures unclaimed children in document order" do
66
+ doc = "<host><title>t</title><extra1>a</extra1>" \
67
+ "<extra2>b</extra2></host>"
68
+
69
+ parsed = host_class.from_xml(doc)
70
+ expect(parsed.title).to eq("t")
71
+ expect(parsed.others.size).to eq(2)
72
+ expect(member_shapes(parsed.others)).to eq(%w[String String])
73
+ end
74
+
75
+ it "captures typed members as model instances" do
76
+ doc = "<bag><label>L</label><item>one</item><item>two</item></bag>"
77
+
78
+ parsed = typed_class.from_xml(doc)
79
+ expect(parsed.label).to eq("L")
80
+ expect(parsed.items.map(&:content)).to eq(%w[one two])
81
+ end
82
+
83
+ it "matches the interpretive path's capture" do
84
+ doc = "<host><title>t</title><extra1>a</extra1><extra2>b</extra2></host>"
85
+
86
+ interpretive = Lutaml::Model::Config.with_adapter(xml: :nokogiri) do
87
+ host_class.from_xml(doc)
88
+ end
89
+
90
+ parsed = host_class.from_xml(doc)
91
+ expect(parsed.others.size).to eq(interpretive.others.size)
92
+ expect(member_shapes(parsed.others)).to eq(
93
+ member_shapes(interpretive.others).map { |n| n.sub("Nokogiri", "Leptris") },
94
+ )
95
+ end
96
+
97
+ it "compiles under engines without wildcard rows to nil" do
98
+ allow(Lutaml::Xml::Leptris).to receive(:wildcard_rows_compatible?)
99
+ .and_return(false)
100
+
101
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(
102
+ host_class, Lutaml::Model::Config.default_register
103
+ )).to be_nil
104
+ end
105
+ end
data/spec/person_spec.rb CHANGED
@@ -115,12 +115,13 @@ RSpec.describe Person do
115
115
  expect(model.to_yaml).to eq(attributes_yaml.to_yaml)
116
116
  end
117
117
 
118
- # yeptris misparses time-only scalars ("07:00:00" anchors to a fixed
119
- # date at midnight) — pending the engine fix, this example cannot
120
- # pass under the in-suite-detected engine.
121
- # rubocop:disable RSpec/PendingWithoutReason -- yeptris misparses time-only
122
- # scalars (fixed-date midnight anchor); engine fix pending upstream.
123
- xit "deserializes from YAML" do
118
+ # yeptris 0.6.29.x regressed the time-only scalar anchor: "07:00:00"
119
+ # comes back as a fixed 2025-07-19 00:00 instead of today-07:00
120
+ # (psych answers exactly; verified on the psych path). Re-pended
121
+ # until the yeptris typed-materialize ships the fix — evidence on
122
+ # moxml#336. 873a67bd un-pended these on the premise that the
123
+ # 0.6.28.x behavior held; it did not survive the 0.6.29 cut.
124
+ xit "deserializes from YAML" do # rubocop:disable RSpec/PendingWithoutReason -- yeptris time-only misparse; engine fix pending.
124
125
  yaml = attributes_yaml.to_yaml
125
126
  person = described_class.from_yaml(yaml)
126
127
  expect(person.first_name).to eq("John")
@@ -133,7 +134,7 @@ RSpec.describe Person do
133
134
  end
134
135
 
135
136
  # Same yeptris time-only scalar misparse as above.
136
- xit "deserializes from YAML array" do
137
+ xit "deserializes from YAML array" do # rubocop:disable RSpec/PendingWithoutReason -- yeptris time-only misparse; engine fix pending.
137
138
  yaml = [attributes_yaml.dup, attributes_yaml.dup].to_yaml
138
139
 
139
140
  persons = described_class.from_yaml(yaml)
@@ -154,5 +155,4 @@ RSpec.describe Person do
154
155
  expect(persons[1].wakeup_time).to eq(Time.parse("07:00:00"))
155
156
  expect(persons[1].active).to be true
156
157
  end
157
- # rubocop:enable RSpec/PendingWithoutReason
158
158
  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.94
4
+ version: 0.8.96
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-05 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -184,6 +184,7 @@ files:
184
184
  - bench/bench_alloc_audit.rb
185
185
  - bench/bench_common.rb
186
186
  - bench/bench_compare.rb
187
+ - bench/bench_merge_attempts.rb
187
188
  - bench/bench_mml.rb
188
189
  - bench/bench_niso.rb
189
190
  - bench/bench_sts.rb
@@ -206,6 +207,7 @@ files:
206
207
  - docs/_guides/creating-xsd.adoc
207
208
  - docs/_guides/custom-methods.adoc
208
209
  - docs/_guides/document-validation.adoc
210
+ - docs/_guides/grammar-backed-formats.adoc
209
211
  - docs/_guides/index.adoc
210
212
  - docs/_guides/jsonld-serialization.adoc
211
213
  - docs/_guides/keyvalue-serialization.adoc
@@ -1810,6 +1812,7 @@ files:
1810
1812
  - spec/lutaml/model/serializable_instantiate_spec.rb
1811
1813
  - spec/lutaml/model/serializable_spec.rb
1812
1814
  - spec/lutaml/model/serializable_validation_spec.rb
1815
+ - spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb
1813
1816
  - spec/lutaml/model/serialize/builder_spec.rb
1814
1817
  - spec/lutaml/model/serialize_perf_guard_spec.rb
1815
1818
  - spec/lutaml/model/services/transformer_spec.rb
@@ -1985,8 +1988,12 @@ files:
1985
1988
  - spec/lutaml/xml/pipeline_integration_spec.rb
1986
1989
  - spec/lutaml/xml/plain_model_accessors_spec.rb
1987
1990
  - spec/lutaml/xml/plan_fast_path_spec.rb
1991
+ - spec/lutaml/xml/plan_namespaced_serialize_spec.rb
1992
+ - spec/lutaml/xml/plan_ns_less_document_spec.rb
1988
1993
  - spec/lutaml/xml/plan_partition_spec.rb
1994
+ - spec/lutaml/xml/plan_type_ns_attribute_spec.rb
1989
1995
  - spec/lutaml/xml/plan_walk_spec.rb
1996
+ - spec/lutaml/xml/plan_wildcard_any_spec.rb
1990
1997
  - spec/lutaml/xml/prefix_control_spec.rb
1991
1998
  - spec/lutaml/xml/register_spec.rb
1992
1999
  - spec/lutaml/xml/reusable_mapping_spec.rb