lutaml-model 0.8.23 → 0.8.28
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/performance.yml +1 -1
- data/Gemfile +6 -0
- data/README.adoc +16 -7
- data/docs/_guides/index.adoc +4 -0
- data/docs/_guides/native-engines.adoc +61 -0
- data/docs/_pages/configuration.adoc +11 -7
- data/docs/_pages/serialization_adapters.adoc +14 -5
- data/lib/compat/opal/lutaml_model_boot.rb +5 -0
- data/lib/lutaml/json/adapter/multi_json_adapter.rb +12 -1
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +21 -0
- data/lib/lutaml/json/adapter.rb +1 -0
- data/lib/lutaml/json/format.rb +1 -1
- data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +16 -1
- data/lib/lutaml/key_value/adapter/json/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +42 -0
- data/lib/lutaml/key_value/adapter/toml.rb +1 -0
- data/lib/lutaml/key_value/adapter/yaml/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/transform.rb +82 -0
- data/lib/lutaml/key_value/transformation/value_serializer.rb +25 -0
- data/lib/lutaml/key_value/transformation.rb +25 -1
- data/lib/lutaml/model/adapter_resolver.rb +23 -6
- data/lib/lutaml/model/attribute.rb +31 -6
- data/lib/lutaml/model/compiled_rule.rb +16 -0
- data/lib/lutaml/model/configuration.rb +5 -0
- data/lib/lutaml/model/error/unknown_adapter_type_error.rb +4 -4
- data/lib/lutaml/model/serialize/format_conversion.rb +65 -0
- data/lib/lutaml/model/toml.rb +10 -6
- data/lib/lutaml/model/type/date_time.rb +2 -2
- data/lib/lutaml/model/type/value.rb +24 -2
- data/lib/lutaml/model/utils.rb +6 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/toml/adapter/teptris_adapter.rb +14 -0
- data/lib/lutaml/toml/adapter.rb +1 -0
- data/lib/lutaml/toml/format.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +15 -0
- data/lib/lutaml/xml/adapter_element.rb +111 -3
- data/lib/lutaml/xml/builder/base.rb +3 -1
- data/lib/lutaml/xml/builder/leptris.rb +13 -0
- data/lib/lutaml/xml/builder/nokogiri.rb +3 -0
- data/lib/lutaml/xml/builder.rb +1 -0
- data/lib/lutaml/xml/declaration_planner.rb +8 -2
- data/lib/lutaml/xml/format.rb +7 -1
- data/lib/lutaml/xml/leptris/element.rb +34 -0
- data/lib/lutaml/xml/namespace_collector.rb +4 -3
- data/lib/lutaml/xml/plan_compiler.rb +253 -0
- data/lib/lutaml/xml/plan_hydrator.rb +341 -0
- data/lib/lutaml/xml/plan_order.rb +50 -0
- data/lib/lutaml/xml/plan_serializer.rb +241 -0
- data/lib/lutaml/xml/plan_walk.rb +30 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +8 -0
- data/lib/lutaml/xml/xml_element.rb +6 -2
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +46 -0
- data/lib/lutaml/yaml/adapter.rb +5 -0
- data/lib/lutaml/yaml/format.rb +1 -1
- data/lib/tasks/performance.rake +5 -0
- data/lib/tasks/performance_comparator.rb +19 -3
- data/lib/tasks/performance_probe.rake +33 -0
- data/lib/tasks/performance_probe.rb +106 -0
- data/lib/tasks/performance_probe_plan_walk.rb +58 -0
- data/lib/tasks/performance_probe_xml.rb +54 -0
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/json/yeptris_adapter_spec.rb +63 -0
- data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +71 -0
- data/spec/lutaml/model/json_adapter_spec.rb +6 -0
- data/spec/lutaml/model/toml_adapter_spec.rb +8 -0
- data/spec/lutaml/model/toml_spec.rb +36 -1
- data/spec/lutaml/xml/plan_fast_path_spec.rb +547 -0
- data/spec/lutaml/xml/plan_walk_spec.rb +102 -0
- data/spec/lutaml/xml/xml_spec.rb +13 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +63 -0
- data/spec/spec_helper.rb +7 -1
- metadata +28 -4
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "XML plan fast path" do
|
|
6
|
+
let(:classes) do
|
|
7
|
+
item_class = Class.new(Lutaml::Model::Serializable) do
|
|
8
|
+
attribute :id, :integer
|
|
9
|
+
attribute :name, :string
|
|
10
|
+
attribute :tags, :string, collection: true
|
|
11
|
+
|
|
12
|
+
xml do
|
|
13
|
+
element "item"
|
|
14
|
+
map_attribute "id", to: :id
|
|
15
|
+
map_element "name", to: :name
|
|
16
|
+
map_element "tag", to: :tags
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
root_class = Class.new(Lutaml::Model::Serializable) do
|
|
20
|
+
attribute :item, item_class, collection: true
|
|
21
|
+
|
|
22
|
+
xml do
|
|
23
|
+
element "root"
|
|
24
|
+
map_element "item", to: :item
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
[item_class, root_class]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
let(:item_class) { classes[0] }
|
|
31
|
+
let(:root_class) { classes[1] }
|
|
32
|
+
|
|
33
|
+
around do |example|
|
|
34
|
+
old = Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
35
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
36
|
+
example.run
|
|
37
|
+
ensure
|
|
38
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = old
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "is opt-in and off by default" do
|
|
42
|
+
expect(Lutaml::Model::Configuration.new.xml_plan_fast_path).to be(false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "hydrates equal to the interpretive path" do
|
|
46
|
+
xml = root_class.new(item: [
|
|
47
|
+
item_class.new(id: 1, name: "a", tags: %w[x y]),
|
|
48
|
+
item_class.new(id: 2, name: "b", tags: %w[z]),
|
|
49
|
+
]).to_xml
|
|
50
|
+
|
|
51
|
+
expect(root_class.from_xml(xml).item).to eq(root_class.new(item: [
|
|
52
|
+
item_class.new(id: 1, name: "a", tags: %w[x y]),
|
|
53
|
+
item_class.new(id: 2, name: "b", tags: %w[z]),
|
|
54
|
+
]).item)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "matches the interpretive path on missing elements" do
|
|
58
|
+
xml = <<~XML
|
|
59
|
+
<root>
|
|
60
|
+
<item id="1"><name>A</name><tag>x</tag></item>
|
|
61
|
+
<item id="2"><tag>y</tag><tag>z</tag></item>
|
|
62
|
+
<item id="3"><name>C</name></item>
|
|
63
|
+
</root>
|
|
64
|
+
XML
|
|
65
|
+
|
|
66
|
+
parsed = root_class.from_xml(xml)
|
|
67
|
+
expect(parsed.item.map { |i| [i.id, i.name, i.tags] })
|
|
68
|
+
.to eq([[1, "A", %w[x]], [2, nil, %w[y z]], [3, "C", []]])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "wraps engine parse errors as InvalidFormatError" do
|
|
72
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) do
|
|
73
|
+
expect { root_class.from_xml("<root><item>") }
|
|
74
|
+
.to raise_error(Lutaml::Model::InvalidFormatError)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "defers custom-method rules through fragment interpretation" do
|
|
79
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
80
|
+
attribute :note, :string
|
|
81
|
+
|
|
82
|
+
xml do
|
|
83
|
+
element "doc"
|
|
84
|
+
map_element "note", to: :note, with: { from: :note_from }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def note_from(instance, value)
|
|
88
|
+
instance.note = value.text.upcase
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
expect(Lutaml::Xml::PlanCompiler.compile(klass,
|
|
93
|
+
Lutaml::Model::Config.default_register)).not_to be_nil
|
|
94
|
+
expect(klass.from_xml("<doc><note>hi</note></doc>").note).to eq("HI")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "compiles multiple spellings in interpretive spelling-group order" do
|
|
98
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
99
|
+
attribute :val, :string, collection: true
|
|
100
|
+
|
|
101
|
+
xml do
|
|
102
|
+
element "d"
|
|
103
|
+
map_element %w[a b], to: :val
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
parsed = klass.from_xml("<d><a>1</a><b>2</b><a>3</a></d>")
|
|
108
|
+
expect(parsed.val).to eq(%w[1 3 2])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "compiles multiple collection rows via callback routing" do
|
|
112
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
113
|
+
attribute :xs, :string, collection: true
|
|
114
|
+
attribute :ys, :string, collection: true
|
|
115
|
+
|
|
116
|
+
xml do
|
|
117
|
+
element "d"
|
|
118
|
+
map_element "x", to: :xs
|
|
119
|
+
map_element "y", to: :ys
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
parsed = klass.from_xml("<d><x>1</x><y>a</y><x>2</x><y>b</y></d>")
|
|
124
|
+
expect(parsed.xs).to eq(%w[1 2])
|
|
125
|
+
expect(parsed.ys).to eq(%w[a b])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "splits delimited attributes" do
|
|
129
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
130
|
+
attribute :tags, :string, collection: true
|
|
131
|
+
|
|
132
|
+
xml do
|
|
133
|
+
element "d"
|
|
134
|
+
map_attribute "tags", to: :tags, delimiter: ","
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
expect(klass.from_xml('<d tags="a,b,c"/>').tags).to eq(%w[a b c])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "compiles delegate rules with post-instance routing" do
|
|
142
|
+
target = Class.new(Lutaml::Model::Serializable) do
|
|
143
|
+
attribute :note, :string
|
|
144
|
+
|
|
145
|
+
xml do
|
|
146
|
+
element "tgt"
|
|
147
|
+
map_element "note", to: :note
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
151
|
+
attribute :tgt, target
|
|
152
|
+
|
|
153
|
+
xml do
|
|
154
|
+
element "doc"
|
|
155
|
+
map_element "note", to: :note, delegate: :tgt
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
expect(Lutaml::Xml::PlanCompiler.compile(klass,
|
|
160
|
+
Lutaml::Model::Config.default_register)).not_to be_nil
|
|
161
|
+
parsed = klass.from_xml("<doc><note>hi</note></doc>")
|
|
162
|
+
expect(parsed.tgt.note).to eq("hi")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "routes a single collection row natively (hybrid routing)" do
|
|
166
|
+
plan = Lutaml::Xml::PlanCompiler.compile(
|
|
167
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
168
|
+
attribute :xs, :string, collection: true
|
|
169
|
+
attribute :name, :string
|
|
170
|
+
|
|
171
|
+
xml do
|
|
172
|
+
element "d"
|
|
173
|
+
map_element "x", to: :xs
|
|
174
|
+
map_element "name", to: :name
|
|
175
|
+
end
|
|
176
|
+
end,
|
|
177
|
+
Lutaml::Model::Config.default_register,
|
|
178
|
+
)
|
|
179
|
+
expect(plan[:rows].map { |r| r[2] }).to include(:collection_native)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe "newly compilable shapes" do
|
|
183
|
+
around do |example|
|
|
184
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "compiles namespace-qualified models and matches any bound prefix" do
|
|
188
|
+
ns = Class.new(Lutaml::Xml::W3c::XmlNamespace) do
|
|
189
|
+
uri "urn:probe"
|
|
190
|
+
prefix_default "p"
|
|
191
|
+
element_form_default :qualified
|
|
192
|
+
end
|
|
193
|
+
item = Class.new(Lutaml::Model::Serializable) do
|
|
194
|
+
attribute :name, :string
|
|
195
|
+
xml do
|
|
196
|
+
namespace ns
|
|
197
|
+
element "item"
|
|
198
|
+
map_element "name", to: :name
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
root = Class.new(Lutaml::Model::Serializable) do
|
|
202
|
+
attribute :item, item, collection: true
|
|
203
|
+
xml do
|
|
204
|
+
namespace ns
|
|
205
|
+
element "root"
|
|
206
|
+
map_element "item", to: :item
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
xml = %(<zz:root xmlns:zz="urn:probe"><zz:item><zz:name>A</zz:name></zz:item></zz:root>)
|
|
210
|
+
|
|
211
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
212
|
+
ref = root.from_xml(xml)
|
|
213
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
214
|
+
fast = root.from_xml(xml)
|
|
215
|
+
expect(fast.item.map(&:name)).to eq(ref.item.map(&:name))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it "reads cdata sections as text" do
|
|
219
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
220
|
+
attribute :t, :string
|
|
221
|
+
xml do
|
|
222
|
+
element "d"
|
|
223
|
+
map_element "t", to: :t, cdata: true
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
expect(klass.from_xml("<d><t><![CDATA[a <b> c]]></t></d>").t).to eq("a <b> c")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "captures raw element subtrees verbatim" do
|
|
230
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
231
|
+
attribute :frag, :string
|
|
232
|
+
xml do
|
|
233
|
+
element "d"
|
|
234
|
+
map_element "frag", to: :frag, raw: :element
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
# Verbatim capture: the fast path returns the exact source
|
|
238
|
+
# subtree (the interpretive path re-serializes with indentation).
|
|
239
|
+
expect(klass.from_xml(%(<d><frag><x a="1">inner</x></frag></d>)).frag)
|
|
240
|
+
.to eq(%(<frag><x a="1">inner</x></frag>))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "hydrates content runs on collection attributes" do
|
|
244
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
245
|
+
attribute :text, :string, collection: true
|
|
246
|
+
attribute :b, :string
|
|
247
|
+
xml do
|
|
248
|
+
element "p"
|
|
249
|
+
map_content to: :text
|
|
250
|
+
map_element "b", to: :b
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
parsed = klass.from_xml("<p>Hello <b>bold</b> world!</p>")
|
|
254
|
+
expect(parsed.text).to eq(["Hello ", " world!"])
|
|
255
|
+
expect(parsed.b).to eq("bold")
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
describe "ordered and mixed content" do
|
|
260
|
+
around do |example|
|
|
261
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
let(:emph_class) do
|
|
265
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
266
|
+
attribute :text, :string
|
|
267
|
+
|
|
268
|
+
xml do
|
|
269
|
+
element "emph"
|
|
270
|
+
map_content to: :text
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
let(:mixed_class) do
|
|
276
|
+
emph = emph_class
|
|
277
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
278
|
+
attribute :emph, emph, collection: true
|
|
279
|
+
attribute :text, :string, collection: true
|
|
280
|
+
attribute :kind, :string
|
|
281
|
+
|
|
282
|
+
xml do
|
|
283
|
+
element "para"
|
|
284
|
+
mixed_content
|
|
285
|
+
map_attribute "kind", to: :kind
|
|
286
|
+
map_element "emph", to: :emph
|
|
287
|
+
map_content to: :text
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "compiles mixed-content mappings and reconstructs element_order" do
|
|
293
|
+
xml = %(<para kind="a">Hi <emph>there</emph> bye</para>)
|
|
294
|
+
|
|
295
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
296
|
+
interpretive = mixed_class.from_xml(xml)
|
|
297
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
298
|
+
fast = mixed_class.from_xml(xml)
|
|
299
|
+
|
|
300
|
+
order_sig = ->(m) do
|
|
301
|
+
m.element_order.map { |e| [e.type, e.name, e.text_content] }
|
|
302
|
+
end
|
|
303
|
+
expect(order_sig.call(fast)).to eq(order_sig.call(interpretive))
|
|
304
|
+
expect(fast.kind).to eq("a")
|
|
305
|
+
expect(fast.text).to eq(interpretive.text)
|
|
306
|
+
expect(fast.emph.map(&:text)).to eq(interpretive.emph.map(&:text))
|
|
307
|
+
expect(fast.to_xml).to eq(interpretive.to_xml)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it "yields each_mixed_content equal to the interpretive path" do
|
|
311
|
+
xml = %(<para>Hi <emph>there</emph> bye</para>)
|
|
312
|
+
|
|
313
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
314
|
+
interpretive = mixed_class.from_xml(xml)
|
|
315
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
316
|
+
fast = mixed_class.from_xml(xml)
|
|
317
|
+
|
|
318
|
+
sig = ->(m) do
|
|
319
|
+
m.each_mixed_content.map { |i| i.is_a?(String) ? i : i.text }
|
|
320
|
+
end
|
|
321
|
+
expect(sig.call(fast)).to eq(sig.call(interpretive))
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "defers ordered nested children interpretively while the parent compiles" do
|
|
325
|
+
mixed = mixed_class
|
|
326
|
+
doc_class = Class.new(Lutaml::Model::Serializable) do
|
|
327
|
+
attribute :title, :string
|
|
328
|
+
attribute :para, mixed, collection: true
|
|
329
|
+
|
|
330
|
+
xml do
|
|
331
|
+
element "doc"
|
|
332
|
+
map_element "title", to: :title
|
|
333
|
+
map_element "para", to: :para
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
plan = Lutaml::Xml::PlanCompiler.compile(
|
|
337
|
+
doc_class, Lutaml::Model::Config.default_register
|
|
338
|
+
)
|
|
339
|
+
expect(plan).not_to be_nil
|
|
340
|
+
expect(plan[:rows].map { |r| r[2] }).to include(:ordered_deferred)
|
|
341
|
+
|
|
342
|
+
xml = %(<doc><title>T</title><para>Hi <emph>x</emph> bye</para><para>plain</para></doc>)
|
|
343
|
+
parsed = doc_class.from_xml(xml)
|
|
344
|
+
expect(parsed.title).to eq("T")
|
|
345
|
+
para = parsed.para.first
|
|
346
|
+
expect(para.text).to eq(["Hi ", " bye"])
|
|
347
|
+
expect(para.emph.map(&:text)).to eq(["x"])
|
|
348
|
+
expect(para.element_order.map { |e| [e.type, e.name] })
|
|
349
|
+
.to eq([["Text", "text"], ["Element", "emph"], ["Text", "text"]])
|
|
350
|
+
# absent collections materialize as [] (interpretive parity)
|
|
351
|
+
expect(parsed.para.last.emph).to eq([])
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
it "reconstructs element_order for ordered-only mappings" do
|
|
355
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
356
|
+
attribute :a, :string, collection: true
|
|
357
|
+
attribute :b, :string, collection: true
|
|
358
|
+
|
|
359
|
+
xml do
|
|
360
|
+
element "d"
|
|
361
|
+
ordered
|
|
362
|
+
map_element "a", to: :a
|
|
363
|
+
map_element "b", to: :b
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
xml = %(<d><a>1</a><b>x</b><a>2</a></d>)
|
|
367
|
+
|
|
368
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
369
|
+
interpretive = klass.from_xml(xml)
|
|
370
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
371
|
+
fast = klass.from_xml(xml)
|
|
372
|
+
|
|
373
|
+
expect(fast.a).to eq(interpretive.a)
|
|
374
|
+
expect(fast.b).to eq(interpretive.b)
|
|
375
|
+
expect(fast.element_order.map { |e| [e.type, e.name] })
|
|
376
|
+
.to eq(interpretive.element_order.map { |e| [e.type, e.name] })
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it "joins content runs for non-collection content attributes" do
|
|
380
|
+
emph = emph_class
|
|
381
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
382
|
+
attribute :emph, emph, collection: true
|
|
383
|
+
|
|
384
|
+
xml do
|
|
385
|
+
element "para"
|
|
386
|
+
map_element "emph", to: :emph
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
parsed = klass.from_xml(%(<para><emph>bold</emph></para>))
|
|
391
|
+
expect(parsed.emph.first.text).to eq("bold")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
describe "serialize fast path" do
|
|
396
|
+
around do |example|
|
|
397
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
it "serializes ordered models byte-equal from element_order" do
|
|
401
|
+
emph = Class.new(Lutaml::Model::Serializable) do
|
|
402
|
+
attribute :text, :string
|
|
403
|
+
|
|
404
|
+
xml do
|
|
405
|
+
element "emph"
|
|
406
|
+
map_content to: :text
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
para = Class.new(Lutaml::Model::Serializable) do
|
|
410
|
+
attribute :emph, emph, collection: true
|
|
411
|
+
attribute :text, :string, collection: true
|
|
412
|
+
attribute :kind, :string
|
|
413
|
+
|
|
414
|
+
xml do
|
|
415
|
+
element "para"
|
|
416
|
+
mixed_content
|
|
417
|
+
map_attribute "kind", to: :kind
|
|
418
|
+
map_element "emph", to: :emph
|
|
419
|
+
map_content to: :text
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
doc_class = Class.new(Lutaml::Model::Serializable) do
|
|
423
|
+
attribute :title, :string
|
|
424
|
+
attribute :para, para, collection: true
|
|
425
|
+
|
|
426
|
+
xml do
|
|
427
|
+
element "doc"
|
|
428
|
+
map_element "title", to: :title
|
|
429
|
+
map_element "para", to: :para
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
xml = %(<doc><title>T</title><para kind="a">Hi <emph>there</emph> bye <emph>again</emph>!</para><para>second</para></doc>)
|
|
433
|
+
|
|
434
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
435
|
+
fast = doc_class.from_xml(xml)
|
|
436
|
+
fast_out = fast.to_xml
|
|
437
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
438
|
+
interp_out = doc_class.from_xml(xml).to_xml
|
|
439
|
+
|
|
440
|
+
expect(fast_out).to eq(interp_out)
|
|
441
|
+
expect(fast_out).to include(%(<para kind="a">Hi <emph>there</emph> bye <emph>again</emph>!</para>))
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it "reflects content mutations through the ordered fast serializer" do
|
|
445
|
+
para = Class.new(Lutaml::Model::Serializable) do
|
|
446
|
+
attribute :emph, :string, collection: true
|
|
447
|
+
attribute :text, :string, collection: true
|
|
448
|
+
|
|
449
|
+
xml do
|
|
450
|
+
element "para"
|
|
451
|
+
mixed_content
|
|
452
|
+
map_element "emph", to: :emph
|
|
453
|
+
map_content to: :text
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
xml = %(<para>Hi <emph>x</emph> bye</para>)
|
|
457
|
+
|
|
458
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
459
|
+
fast = para.from_xml(xml)
|
|
460
|
+
fast.text = ["MUTATED ", " tail"]
|
|
461
|
+
fast_out = fast.to_xml
|
|
462
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
463
|
+
interp = para.from_xml(xml)
|
|
464
|
+
interp.text = ["MUTATED ", " tail"]
|
|
465
|
+
expect(fast_out).to eq(interp.to_xml)
|
|
466
|
+
expect(fast_out).to eq("<para>MUTATED <emph>x</emph> tail</para>")
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it "falls back interpretively for ordered instances without element_order" do
|
|
470
|
+
para = Class.new(Lutaml::Model::Serializable) do
|
|
471
|
+
attribute :emph, :string, collection: true
|
|
472
|
+
attribute :text, :string, collection: true
|
|
473
|
+
|
|
474
|
+
xml do
|
|
475
|
+
element "para"
|
|
476
|
+
mixed_content
|
|
477
|
+
map_element "emph", to: :emph
|
|
478
|
+
map_content to: :text
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
model = para.new(emph: ["z"], text: ["hello ", " world"])
|
|
482
|
+
|
|
483
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
484
|
+
out = model.to_xml
|
|
485
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
486
|
+
expect(out).to eq(model.to_xml)
|
|
487
|
+
expect(out).to eq("<para>hello <emph>z</emph> world</para>")
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
it "serializes byte-equal to the interpretive path" do
|
|
491
|
+
item = Class.new(Lutaml::Model::Serializable) do
|
|
492
|
+
attribute :id, :integer
|
|
493
|
+
attribute :name, :string
|
|
494
|
+
attribute :tags, :string, collection: true
|
|
495
|
+
|
|
496
|
+
xml do
|
|
497
|
+
element "item"
|
|
498
|
+
map_attribute "id", to: :id
|
|
499
|
+
map_element "name", to: :name
|
|
500
|
+
map_element "tag", to: :tags
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
root = Class.new(Lutaml::Model::Serializable) do
|
|
504
|
+
attribute :item, item, collection: true
|
|
505
|
+
|
|
506
|
+
xml do
|
|
507
|
+
element "root"
|
|
508
|
+
map_element "item", to: :item
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
model = root.new(item: [item.new(id: 1, name: "a", tags: %w[x y]),
|
|
512
|
+
item.new(id: 2, name: "b", tags: %w[z])])
|
|
513
|
+
|
|
514
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
515
|
+
interpretive = model.to_xml
|
|
516
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
517
|
+
expect(model.to_xml).to eq(interpretive)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
it "round-trips through both fast paths" do
|
|
521
|
+
item = Class.new(Lutaml::Model::Serializable) do
|
|
522
|
+
attribute :id, :integer
|
|
523
|
+
attribute :name, :string
|
|
524
|
+
|
|
525
|
+
xml do
|
|
526
|
+
element "item"
|
|
527
|
+
map_attribute "id", to: :id
|
|
528
|
+
map_element "name", to: :name
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
root = Class.new(Lutaml::Model::Serializable) do
|
|
532
|
+
attribute :item, item, collection: true
|
|
533
|
+
|
|
534
|
+
xml do
|
|
535
|
+
element "root"
|
|
536
|
+
map_element "item", to: :item
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
model = root.new(item: [item.new(id: 7, name: "n")])
|
|
540
|
+
|
|
541
|
+
xml = model.to_xml
|
|
542
|
+
parsed = root.from_xml(xml)
|
|
543
|
+
expect(parsed.item.first.id).to eq(7)
|
|
544
|
+
expect(parsed.to_xml).to eq(xml)
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/xml/plan_walk"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "XML PlanWalk consumer surface" do
|
|
7
|
+
around do |example|
|
|
8
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:item_class) do
|
|
12
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
13
|
+
attribute :id, :integer
|
|
14
|
+
attribute :name, :string
|
|
15
|
+
attribute :tags, :string, collection: true
|
|
16
|
+
|
|
17
|
+
xml do
|
|
18
|
+
element "row"
|
|
19
|
+
map_attribute "id", to: :id
|
|
20
|
+
map_element "name", to: :name
|
|
21
|
+
map_element "tags", to: :tags
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "hydrates equal to the interpretive path on a single-row document" do
|
|
27
|
+
xml = %(<row id="7"><name>Alice</name><tags>x</tags><tags>y</tags></row>)
|
|
28
|
+
walked = Lutaml::Xml::PlanWalk.call(item_class, xml)
|
|
29
|
+
expected = item_class.from_xml(xml)
|
|
30
|
+
|
|
31
|
+
expect(walked).to be_a(item_class)
|
|
32
|
+
expect(walked.id).to eq(expected.id)
|
|
33
|
+
expect(walked.name).to eq(expected.name)
|
|
34
|
+
expect(walked.tags).to eq(expected.tags)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "returns an Array for collection roots" do
|
|
38
|
+
xml = %(<root><row id="1"><name>a</name></row><row id="2"><name>b</name></row></root>)
|
|
39
|
+
item_type = item_class
|
|
40
|
+
root_class = Class.new(Lutaml::Model::Serializable) do
|
|
41
|
+
attribute :item, item_type, collection: true
|
|
42
|
+
|
|
43
|
+
xml do
|
|
44
|
+
element "root"
|
|
45
|
+
map_element "row", to: :item
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
walked = Lutaml::Xml::PlanWalk.call(root_class, xml)
|
|
50
|
+
expected = root_class.from_xml(xml)
|
|
51
|
+
|
|
52
|
+
expect(walked.item).to be_an(Array)
|
|
53
|
+
expect(walked.item.map(&:id)).to eq(expected.item.map(&:id))
|
|
54
|
+
expect(walked.item.map(&:name)).to eq(expected.item.map(&:name))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "walks and hydrates a 5000-row document in under the interpretive cost" do
|
|
58
|
+
xml = +"<root>"
|
|
59
|
+
5000.times do |i|
|
|
60
|
+
xml << %(<row id="#{i}"><name>r-#{i}</name><tags>a</tags><tags>b</tags></row>)
|
|
61
|
+
end
|
|
62
|
+
xml << "</root>"
|
|
63
|
+
|
|
64
|
+
item_type = item_class
|
|
65
|
+
root_class = Class.new(Lutaml::Model::Serializable) do
|
|
66
|
+
attribute :item, item_type, collection: true
|
|
67
|
+
|
|
68
|
+
xml do
|
|
69
|
+
element "root"
|
|
70
|
+
map_element "row", to: :item
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
walked = Lutaml::Xml::PlanWalk.call(root_class, xml)
|
|
75
|
+
interpretive = root_class.from_xml(xml)
|
|
76
|
+
expect(walked.item.size).to eq(interpretive.item.size)
|
|
77
|
+
expect(walked.item.map(&:id)).to eq(interpretive.item.map(&:id))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "compiles delegate mappings for post-instance routing" do
|
|
81
|
+
# delegates compile and route after the target instance exists
|
|
82
|
+
target = Class.new(Lutaml::Model::Serializable) do
|
|
83
|
+
attribute :note, :string
|
|
84
|
+
xml do
|
|
85
|
+
element "tgt"
|
|
86
|
+
map_element "note", to: :note
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
90
|
+
attribute :tgt, target
|
|
91
|
+
xml do
|
|
92
|
+
element "doc"
|
|
93
|
+
map_element "note", to: :note, delegate: :tgt
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
expect(Lutaml::Xml::PlanCompiler.compile(klass,
|
|
98
|
+
Lutaml::Model::Config.default_register)).not_to be_nil
|
|
99
|
+
expect(Lutaml::Xml::PlanWalk.call(klass, "<doc><note>x</note></doc>").tgt.note)
|
|
100
|
+
.to eq("x")
|
|
101
|
+
end
|
|
102
|
+
end
|
data/spec/lutaml/xml/xml_spec.rb
CHANGED
|
@@ -120,6 +120,19 @@ RSpec.describe Lutaml::Xml do
|
|
|
120
120
|
hide_const("Ox") if Object.const_defined?(:Ox)
|
|
121
121
|
hide_const("Oga") if Object.const_defined?(:Oga)
|
|
122
122
|
hide_const("REXML") if Object.const_defined?(:REXML)
|
|
123
|
+
# When the leptris gem is in the bundle, moxml prefers it over every
|
|
124
|
+
# other engine; the legacy-detection contexts below must opt out to
|
|
125
|
+
# exercise the fallback chain.
|
|
126
|
+
allow(Moxml::Config).to receive(:leptris_preferred_available?)
|
|
127
|
+
.and_return(false)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context "when the leptris engine is in the bundle" do
|
|
131
|
+
it "prefers :leptris" do
|
|
132
|
+
allow(Moxml::Config).to receive(:leptris_preferred_available?)
|
|
133
|
+
.and_return(true)
|
|
134
|
+
expect(described_class.detect_xml_adapter).to eq(:leptris)
|
|
135
|
+
end
|
|
123
136
|
end
|
|
124
137
|
|
|
125
138
|
context "when Nokogiri is available" do
|