lutaml-model 0.8.24 → 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/Gemfile +5 -5
- data/lib/compat/opal/lutaml_model_boot.rb +3 -0
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +1 -1
- data/lib/lutaml/model/compiled_rule.rb +16 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +29 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/declaration_planner.rb +8 -2
- data/lib/lutaml/xml/format.rb +3 -0
- data/lib/lutaml/xml/namespace_collector.rb +4 -3
- data/lib/lutaml/xml/plan_compiler.rb +152 -38
- data/lib/lutaml/xml/plan_hydrator.rb +301 -40
- 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/yaml/adapter/yeptris_adapter.rb +8 -2
- data/lib/tasks/performance_probe_plan_walk.rb +58 -0
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/xml/plan_fast_path_spec.rb +455 -4
- data/spec/lutaml/xml/plan_walk_spec.rb +102 -0
- metadata +9 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# Public consumer surface for the plan fast path: one engine
|
|
6
|
+
# parse + descriptor walk and direct model hydration, bypassing
|
|
7
|
+
# the wrapper-tree construction that dominates large-document
|
|
8
|
+
# consumer paths. The returned object is the mapped model root;
|
|
9
|
+
# collection attributes remain collections on that model.
|
|
10
|
+
module PlanWalk
|
|
11
|
+
class << self
|
|
12
|
+
# Build the plan, walk the document, and hydrate the mapped
|
|
13
|
+
# root. Returns nil when the model is not plan-compilable.
|
|
14
|
+
def call(model_class, xml, _options = {})
|
|
15
|
+
register = Lutaml::Model::Config.default_register
|
|
16
|
+
plan = PlanCompiler.compile(model_class, register)
|
|
17
|
+
return nil unless plan
|
|
18
|
+
|
|
19
|
+
document = ::Leptris::XML.parse(xml)
|
|
20
|
+
root = document.root
|
|
21
|
+
return nil unless root && root.name == plan[:tree][:name]
|
|
22
|
+
|
|
23
|
+
PlanHydrator.call(model_class, plan,
|
|
24
|
+
plan[:descriptor].walk(root),
|
|
25
|
+
node: root)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "yeptris"
|
|
4
|
-
require "yeptris/yaml"
|
|
5
3
|
require_relative "standard_adapter"
|
|
6
4
|
|
|
7
5
|
module Lutaml
|
|
@@ -18,7 +16,13 @@ module Lutaml
|
|
|
18
16
|
# and aliases always resolve; a tagged value without a core type
|
|
19
17
|
# materializes as plain data instead of raising DisallowedClass.
|
|
20
18
|
class YeptrisAdapter < StandardAdapter
|
|
19
|
+
# yeptris is required lazily: the ruby-platform variant installs
|
|
20
|
+
# everywhere but only loads where libyeptris exists (no Windows
|
|
21
|
+
# prebuilts) — a broken install must degrade to detection
|
|
22
|
+
# fallback, never crash loads.
|
|
21
23
|
def self.parse(yaml, _options = {})
|
|
24
|
+
require "yeptris"
|
|
25
|
+
require "yeptris/yaml"
|
|
22
26
|
# rubocop:disable-next Naming/VariableNumber -- upstream schema literal
|
|
23
27
|
Yeptris::YAML.load(yaml, schema: :compat_11)
|
|
24
28
|
end
|
|
@@ -32,6 +36,8 @@ module Lutaml
|
|
|
32
36
|
@attributes
|
|
33
37
|
end
|
|
34
38
|
|
|
39
|
+
require "yeptris"
|
|
40
|
+
require "yeptris/yaml"
|
|
35
41
|
Yeptris::YAML.dump(attributes_to_serialize)
|
|
36
42
|
end
|
|
37
43
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require "leptris"
|
|
5
|
+
require "leptris/xml/descriptor"
|
|
6
|
+
require "benchmark"
|
|
7
|
+
|
|
8
|
+
module PlanWalkProbe
|
|
9
|
+
class BulkItem < Lutaml::Model::Serializable
|
|
10
|
+
attribute :id, :integer
|
|
11
|
+
attribute :name, :string
|
|
12
|
+
attribute :tags, :string, collection: true
|
|
13
|
+
|
|
14
|
+
xml do
|
|
15
|
+
element "row"
|
|
16
|
+
map_attribute "id", to: :id
|
|
17
|
+
map_element "name", to: :name
|
|
18
|
+
map_element "tags", to: :tags
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class BulkRoot < Lutaml::Model::Serializable
|
|
23
|
+
attribute :item, BulkItem, collection: true
|
|
24
|
+
|
|
25
|
+
xml do
|
|
26
|
+
element "iso"
|
|
27
|
+
map_element "row", to: :item
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
xml = File.read("/tmp/iso.xml")
|
|
33
|
+
Lutaml::Model::Config.adapter_for(:xml) # resolve adapter before benchmarking
|
|
34
|
+
|
|
35
|
+
measure = lambda do |label, &block|
|
|
36
|
+
elapsed = Benchmark.measure { 3.times(&block) }.real * 1000
|
|
37
|
+
puts "#{label}: %.2f ms total / %.2f ms each" % [elapsed, elapsed / 3]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
measure.call("raw parse + descriptor walk") do
|
|
41
|
+
descriptor = Leptris::XML::Descriptor.build(
|
|
42
|
+
name: "iso",
|
|
43
|
+
children: [{ name: "row", kind: :collection }],
|
|
44
|
+
)
|
|
45
|
+
descriptor.walk(Leptris::XML.parse(xml).root)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
49
|
+
measure.call("PlanWalk parse + 5000 model.new") do
|
|
50
|
+
result = Lutaml::Xml::PlanWalk.call(PlanWalkProbe::BulkRoot, xml)
|
|
51
|
+
raise "PlanWalk returned nil" unless result&.item&.size == 5000
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = false
|
|
55
|
+
measure.call("interpretive from_xml") do
|
|
56
|
+
result = PlanWalkProbe::BulkRoot.from_xml(xml)
|
|
57
|
+
raise "interpretive returned wrong size" unless result.item.size == 5000
|
|
58
|
+
end
|
data/lutaml-model.gemspec
CHANGED
|
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
spec.add_dependency "canon"
|
|
39
39
|
spec.add_dependency "concurrent-ruby"
|
|
40
40
|
spec.add_dependency "liquid", ">= 4.0", "< 6.0"
|
|
41
|
-
spec.add_dependency "moxml", "~> 0.5.
|
|
41
|
+
spec.add_dependency "moxml", "~> 0.5.56"
|
|
42
42
|
spec.add_dependency "ostruct"
|
|
43
43
|
spec.add_dependency "rubyzip", "~> 2.3"
|
|
44
44
|
spec.add_dependency "thor"
|
|
@@ -75,7 +75,7 @@ RSpec.describe "XML plan fast path" do
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
it "
|
|
78
|
+
it "defers custom-method rules through fragment interpretation" do
|
|
79
79
|
klass = Class.new(Lutaml::Model::Serializable) do
|
|
80
80
|
attribute :note, :string
|
|
81
81
|
|
|
@@ -85,12 +85,463 @@ RSpec.describe "XML plan fast path" do
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def note_from(instance, value)
|
|
88
|
-
instance.note = value.text
|
|
88
|
+
instance.note = value.text.upcase
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
expect(Lutaml::Xml::PlanCompiler.compile(klass,
|
|
93
|
-
Lutaml::Model::Config.default_register)).
|
|
94
|
-
expect(klass.from_xml("<doc><note>hi</note></doc>").note).to eq("
|
|
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
|
|
95
546
|
end
|
|
96
547
|
end
|