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.
- checksums.yaml +4 -4
- data/.github/workflows/downstream-performance.yml +17 -6
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +2 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +145 -16
- data/lib/lutaml/model/serialize/format_conversion.rb +6 -1
- data/lib/lutaml/model/serialize/initialization.rb +13 -1
- data/lib/lutaml/model/serialize/model_import.rb +30 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/format.rb +8 -10
- data/lib/lutaml/xml/leptris/plan_compiler.rb +110 -21
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +124 -10
- data/lib/lutaml/xml/leptris/plan_serializer.rb +114 -18
- data/lib/lutaml/xml/leptris.rb +38 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +2 -2
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +6 -4
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +142 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +105 -0
- data/spec/person_spec.rb +8 -8
- metadata +9 -2
|
@@ -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
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
|
|
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.
|
|
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-
|
|
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
|