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
|
@@ -4,76 +4,337 @@ module Lutaml
|
|
|
4
4
|
module Xml
|
|
5
5
|
# Hydrates model instances from a Descriptor#walk PlanValue tree,
|
|
6
6
|
# keyed by each value's producing row name (never position — rows
|
|
7
|
-
# for missing elements are simply absent).
|
|
8
|
-
# kwargs
|
|
9
|
-
#
|
|
7
|
+
# for missing elements are simply absent). Native rows build
|
|
8
|
+
# constructor kwargs; deferred rows (custom methods, polymorphism,
|
|
9
|
+
# unions) capture their subtree verbatim and interpret it
|
|
10
|
+
# post-walk — the fragment parse runs the existing interpretive
|
|
11
|
+
# machinery on just that island. Delegate rules hydrate
|
|
12
|
+
# post-instance onto their target object. Collection rows route
|
|
13
|
+
# natively when a single one exists, else through callback rows
|
|
14
|
+
# (their values echo name and type_tag; native collection values
|
|
15
|
+
# echo neither — leptris-ruby#220).
|
|
10
16
|
module PlanHydrator
|
|
11
17
|
class << self
|
|
12
18
|
# plan: the compiler's entry for model_class
|
|
13
19
|
# value: the walk root PlanValue (element)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
# node: the parsed source element (leptris); ordered/mixed
|
|
21
|
+
# plans rebuild element_order from it and ordered children
|
|
22
|
+
# hydrate natively against their own nodes
|
|
23
|
+
def call(model_class, plan, value, parent: nil, node: nil)
|
|
24
|
+
buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
|
|
25
|
+
attr_kwargs = attributes_kwargs(plan, value)
|
|
26
|
+
child_kwargs, children, delegates =
|
|
27
|
+
children_kwargs(model_class, plan, value, buckets)
|
|
28
|
+
plan[:collection_defaults].each do |name|
|
|
29
|
+
next if attr_kwargs.key?(name) || child_kwargs.key?(name)
|
|
30
|
+
|
|
31
|
+
child_kwargs[name] = []
|
|
32
|
+
end
|
|
33
|
+
instance = model_class.new(**attr_kwargs, **child_kwargs)
|
|
34
|
+
instance.lutaml_parent = parent if parent
|
|
35
|
+
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
36
|
+
instance.element_order = PlanOrder.build(node) if node && plan[:ordered]
|
|
37
|
+
children.each do |child|
|
|
38
|
+
child.lutaml_parent = instance
|
|
39
|
+
child.lutaml_root ||= instance.lutaml_root || instance
|
|
40
|
+
end
|
|
41
|
+
interpret_deferred(model_class, plan, value, instance, node, buckets)
|
|
42
|
+
route_delegates(delegates, instance)
|
|
43
|
+
instance
|
|
18
44
|
end
|
|
19
45
|
|
|
20
46
|
private
|
|
21
47
|
|
|
48
|
+
def register
|
|
49
|
+
Lutaml::Model::Config.default_register
|
|
50
|
+
end
|
|
51
|
+
|
|
22
52
|
def attributes_kwargs(plan, value)
|
|
23
53
|
kwargs = {}
|
|
24
54
|
plan[:attr_rows].each do |rule, attr|
|
|
25
55
|
v = value.attribute(rule.name.to_s)
|
|
26
|
-
|
|
56
|
+
next if v.nil?
|
|
57
|
+
|
|
58
|
+
v = v.split(rule.delimiter) if rule.delimiter
|
|
59
|
+
if rule.as_list && rule.as_list[:import]
|
|
60
|
+
v = rule.as_list[:import].call(v)
|
|
61
|
+
end
|
|
62
|
+
kwargs[attr.name.to_sym] = v
|
|
27
63
|
end
|
|
28
64
|
kwargs
|
|
29
65
|
end
|
|
30
66
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
67
|
+
# Returns [kwargs, hydrated_child_instances, delegate_values]
|
|
68
|
+
# — child instances come back so the caller can decorate
|
|
69
|
+
# parent/root links once the parent exists; delegate values
|
|
70
|
+
# wait for the instance (their target object must exist).
|
|
71
|
+
def children_kwargs(_model_class, plan, value, buckets = nil)
|
|
72
|
+
grouped = group_children(value)
|
|
73
|
+
buckets = nil unless buckets && plan[:needs_nodes]
|
|
35
74
|
kwargs = {}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
children = []
|
|
76
|
+
delegates = []
|
|
77
|
+
spellings = Hash.new { |h, k| h[k] = [] }
|
|
78
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
79
|
+
case kind
|
|
80
|
+
when :scalar
|
|
81
|
+
# Raw passthrough: the model constructor is the single
|
|
82
|
+
# cast authority — pre-casting here doubled every cast.
|
|
83
|
+
# Class transforms still apply before assignment.
|
|
84
|
+
v = grouped.dig(rule.name.to_s, 0)&.string_value
|
|
85
|
+
unless v.nil?
|
|
86
|
+
v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
|
|
87
|
+
assign(kwargs, delegates, delegate, rule, attr, v)
|
|
88
|
+
end
|
|
89
|
+
when :raw, :custom_method, :polymorphic, :content_deferred
|
|
90
|
+
# interpreted post-instance (interpret_deferred)
|
|
91
|
+
when :content
|
|
92
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
93
|
+
content_runs(value))
|
|
94
|
+
when :ordered_deferred
|
|
95
|
+
# With a source node the child hydrates natively: one
|
|
96
|
+
# walk against its own node + element_order from the
|
|
97
|
+
# node's children. Without one (direct PlanHydrator
|
|
98
|
+
# use), the subtree fragment-parses interpretively.
|
|
99
|
+
if buckets
|
|
100
|
+
child_type = attr.type(register)
|
|
101
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
102
|
+
items = buckets.fetch(rule.name.to_s, []).map do |n|
|
|
103
|
+
call(child_type, child_plan,
|
|
104
|
+
child_plan[:descriptor].walk(n), node: n)
|
|
49
105
|
end
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
call(attr.type(register), child_plan, v)
|
|
106
|
+
unless items.empty?
|
|
107
|
+
children.concat(items)
|
|
108
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
109
|
+
attr.collection? ? items : items.first)
|
|
55
110
|
end
|
|
56
|
-
attr.collection? ? items : items.first
|
|
57
111
|
end
|
|
112
|
+
when :collection_cb
|
|
113
|
+
values = grouped[rule.name.to_s].to_a.map(&:string_value)
|
|
114
|
+
if rule.transform.is_a?(Class)
|
|
115
|
+
values = values.map { |v| rule.transform_value(attr, v, :from, :xml) }
|
|
116
|
+
end
|
|
117
|
+
unless values.empty?
|
|
118
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
119
|
+
end
|
|
120
|
+
when :collection_native
|
|
121
|
+
values = native_collection(value, rule.name.to_s)
|
|
122
|
+
unless values.nil?
|
|
123
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
124
|
+
end
|
|
125
|
+
when :spelling
|
|
126
|
+
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
127
|
+
when :nested
|
|
128
|
+
child_type = attr.type(register)
|
|
129
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
130
|
+
cursor = if child_plan[:needs_nodes] && buckets
|
|
131
|
+
buckets.fetch(rule.name.to_s, [])
|
|
132
|
+
end
|
|
133
|
+
items = grouped.fetch(rule.name.to_s, []).each_with_index
|
|
134
|
+
.map do |v, i|
|
|
135
|
+
call(child_type, child_plan, v,
|
|
136
|
+
node: cursor && cursor[i])
|
|
137
|
+
end
|
|
138
|
+
next if items.empty?
|
|
139
|
+
|
|
140
|
+
children.concat(items)
|
|
141
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
142
|
+
attr.collection? ? items : items.first)
|
|
143
|
+
end
|
|
58
144
|
end
|
|
59
|
-
|
|
145
|
+
unless spellings.empty?
|
|
146
|
+
spellings.each do |(rule, attr), groups|
|
|
147
|
+
# Interpretive order for shared-attribute groups is
|
|
148
|
+
# spelling-group order (first spelling's matches, then
|
|
149
|
+
# the next's), not interleaved document order.
|
|
150
|
+
values = groups.compact.flatten.map(&:string_value)
|
|
151
|
+
next if values.empty?
|
|
152
|
+
|
|
153
|
+
delegate = delegate_of(plan, rule)
|
|
154
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
155
|
+
attr.collection? ? values : values.first)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
[kwargs, children, delegates]
|
|
60
159
|
end
|
|
61
160
|
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
|
|
161
|
+
# Deferred islands: raw subtrees become wrapper elements via a
|
|
162
|
+
# fragment parse, then the interpretive machinery runs on just
|
|
163
|
+
# that island — custom method invocation with an
|
|
164
|
+
# element-shaped argument, or the polymorphic/union cast.
|
|
165
|
+
# With the source node available the island wraps the ORIGINAL
|
|
166
|
+
# node (no fragment parse — the reparse dominated deferred
|
|
167
|
+
# hydration). Ordered children only land here without a node.
|
|
168
|
+
def interpret_deferred(model_class, plan, value, instance, node = nil,
|
|
169
|
+
buckets = nil)
|
|
170
|
+
plan[:rows].each do |rule, attr, kind, _spelling, _delegate|
|
|
171
|
+
case kind
|
|
172
|
+
when :content_deferred
|
|
173
|
+
runs = content_runs(value)
|
|
174
|
+
next if runs.empty?
|
|
175
|
+
|
|
176
|
+
# Non-collection content attrs hold the joined text
|
|
177
|
+
# (the interpretive path assigns element text, the runs
|
|
178
|
+
# concatenated).
|
|
179
|
+
instance.public_send(:"#{attr.name}=", runs.join)
|
|
180
|
+
when :raw
|
|
181
|
+
raws = raw_strings(value, rule)
|
|
182
|
+
next if raws.empty?
|
|
183
|
+
|
|
184
|
+
instance.public_send(:"#{attr.name}=",
|
|
185
|
+
attr.collection? ? raws : raws.first)
|
|
186
|
+
when :custom_method
|
|
187
|
+
elements = deferred_elements(value, rule, buckets)
|
|
188
|
+
next if elements.empty?
|
|
189
|
+
|
|
190
|
+
args = attr.collection? ? elements : elements.first
|
|
191
|
+
rule.deserialize(instance, args,
|
|
192
|
+
model_class.attributes(register),
|
|
193
|
+
model_class)
|
|
194
|
+
when :ordered_deferred
|
|
195
|
+
next if node # hydrated natively in children_kwargs
|
|
196
|
+
|
|
197
|
+
results = deferred_elements(value, rule, buckets).map do |element|
|
|
198
|
+
attr.cast(element, :xml, register,
|
|
199
|
+
lutaml_parent: instance,
|
|
200
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
201
|
+
end
|
|
202
|
+
next if results.empty?
|
|
203
|
+
|
|
204
|
+
instance.public_send(:"#{attr.name}=",
|
|
205
|
+
attr.collection? ? results : results.first)
|
|
206
|
+
when :polymorphic
|
|
207
|
+
results = deferred_elements(value, rule, buckets).map do |element|
|
|
208
|
+
attr.cast(element, :xml, register,
|
|
209
|
+
polymorphic: rule.polymorphic,
|
|
210
|
+
lutaml_parent: instance,
|
|
211
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
212
|
+
end
|
|
213
|
+
next if results.empty?
|
|
214
|
+
|
|
215
|
+
instance.public_send(:"#{attr.name}=",
|
|
216
|
+
attr.collection? ? results : results.first)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Wrapper elements for deferred rows: the ORIGINAL source
|
|
222
|
+
# nodes bridged through moxml's wrapper cache (wrapper
|
|
223
|
+
# construction only, no parse), falling back to a fragment
|
|
224
|
+
# parse of the captured subtree when no node is available.
|
|
225
|
+
# Both lists are document-ordered for the row's name.
|
|
226
|
+
def deferred_elements(value, rule, buckets)
|
|
227
|
+
nodes = buckets && buckets[rule.name.to_s]
|
|
228
|
+
return nodes.map { |n| bridge_element(n) } if nodes
|
|
229
|
+
|
|
230
|
+
raw_strings(value, rule).map { |raw| fragment_element(raw) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def bridge_element(node)
|
|
234
|
+
Lutaml::Xml::LeptrisElement.new(
|
|
235
|
+
Moxml::Node.wrap(node, bridge_context),
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def bridge_context
|
|
240
|
+
@bridge_context ||= Moxml::Context.new(:leptris)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def route_delegates(delegates, instance)
|
|
244
|
+
delegates.each do |rule, delegate_attr, attr, v|
|
|
245
|
+
target = instance.public_send(rule.delegate)
|
|
246
|
+
unless target
|
|
247
|
+
# the interpretive path instantiates absent delegate
|
|
248
|
+
# targets; mirror it
|
|
249
|
+
target = delegate_attr.type(register).new
|
|
250
|
+
instance.public_send(:"#{rule.delegate}=", target)
|
|
251
|
+
end
|
|
252
|
+
target.public_send(:"#{attr.name}=", v)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def delegate_of(plan, rule)
|
|
257
|
+
entry = plan[:rows].find { |r, _, _k, _| r.equal?(rule) }
|
|
258
|
+
entry && entry[4]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Delegate rules hold their values for post-instance routing
|
|
262
|
+
# (the target object must exist first); plain rules land in
|
|
263
|
+
# kwargs directly.
|
|
264
|
+
def assign(kwargs, delegates, delegate, rule, attr, value)
|
|
265
|
+
if delegate
|
|
266
|
+
delegates << [rule, delegate, attr, value]
|
|
267
|
+
else
|
|
268
|
+
kwargs[attr.name.to_sym] = value
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def content_runs(value)
|
|
273
|
+
out = []
|
|
274
|
+
value.count.times do |i|
|
|
275
|
+
c = value.at(i)
|
|
276
|
+
next unless c.name.nil? && c.kind == :collection
|
|
277
|
+
|
|
278
|
+
out.concat(Array.new(c.count) { |j| c.at(j).string_value })
|
|
279
|
+
end
|
|
280
|
+
out
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Native collection rows echo their producing row's name
|
|
284
|
+
# (leptris 1.9.178).
|
|
285
|
+
def native_collection(value, name)
|
|
286
|
+
value.count.times do |i|
|
|
287
|
+
c = value.at(i)
|
|
288
|
+
next unless c.kind == :collection && c.name == name
|
|
289
|
+
|
|
290
|
+
return Array.new(c.count) { |j| c.at(j).string_value }
|
|
291
|
+
end
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def raw_strings(value, rule)
|
|
296
|
+
out = []
|
|
297
|
+
value.count.times do |idx|
|
|
298
|
+
child = value.at(idx)
|
|
299
|
+
if child.name == rule.name.to_s && child.kind == :raw
|
|
300
|
+
out << child.string_value
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
out
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Fragment deferral: parse the captured subtree back into a
|
|
307
|
+
# wrapper element. Ancestor namespace context is absent (the
|
|
308
|
+
# compiler only defers on namespace-free model chains).
|
|
309
|
+
def fragment_element(raw)
|
|
310
|
+
Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def group_children(value)
|
|
67
314
|
grouped = {}
|
|
68
315
|
value.count.times do |i|
|
|
69
316
|
child = value.at(i)
|
|
70
|
-
|
|
71
|
-
next if key.nil?
|
|
317
|
+
next if child.name.nil? # content runs, read separately
|
|
72
318
|
|
|
73
|
-
(grouped[
|
|
319
|
+
(grouped[child.name] ||= []) << child
|
|
74
320
|
end
|
|
75
321
|
grouped
|
|
76
322
|
end
|
|
323
|
+
|
|
324
|
+
# Element children bucketed by local name, document order
|
|
325
|
+
# preserved — the node-side mirror of group_children for
|
|
326
|
+
# ordered-child hydration. Only built when the plan's subtree
|
|
327
|
+
# needs source nodes; namespace-qualified models never get
|
|
328
|
+
# here (the compiler keeps them interpretive).
|
|
329
|
+
def element_buckets(node)
|
|
330
|
+
buckets = {}
|
|
331
|
+
node.children.each do |child|
|
|
332
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
333
|
+
|
|
334
|
+
(buckets[child.name] ||= []) << child
|
|
335
|
+
end
|
|
336
|
+
buckets
|
|
337
|
+
end
|
|
77
338
|
end
|
|
78
339
|
end
|
|
79
340
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# Reconstructs element_order for ordered/mixed models from the
|
|
6
|
+
# leptris node surface in one native pass, mirroring the
|
|
7
|
+
# interpretive AdapterElement#order contract: text runs, CDATA,
|
|
8
|
+
# comments, PIs, and element entries (local name + namespace
|
|
9
|
+
# uri/prefix) in document order, frozen like the interpretive
|
|
10
|
+
# product.
|
|
11
|
+
module PlanOrder
|
|
12
|
+
TEXT_MARKER = "text"
|
|
13
|
+
CDATA_MARKER = "#cdata-section"
|
|
14
|
+
COMMENT_MARKER = "comment"
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def build(node)
|
|
18
|
+
node.children.filter_map { |child| entry_for(child) }
|
|
19
|
+
.each(&:freeze).freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def entry_for(child)
|
|
25
|
+
case child
|
|
26
|
+
when ::Leptris::XML::CDATA
|
|
27
|
+
Element.new("Text", CDATA_MARKER,
|
|
28
|
+
text_content: child.content, node_type: :cdata)
|
|
29
|
+
when ::Leptris::XML::Text
|
|
30
|
+
Element.new("Text", TEXT_MARKER,
|
|
31
|
+
text_content: child.text, node_type: :text)
|
|
32
|
+
when ::Leptris::XML::Comment
|
|
33
|
+
Element.new("Comment", COMMENT_MARKER,
|
|
34
|
+
text_content: child.content, node_type: :comment)
|
|
35
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
36
|
+
Element.new("ProcessingInstruction", child.name,
|
|
37
|
+
text_content: child.content.to_s.sub(/\A\s+/, ""),
|
|
38
|
+
node_type: :processing_instruction)
|
|
39
|
+
when ::Leptris::XML::Element
|
|
40
|
+
ns = child.namespace
|
|
41
|
+
Element.new("Element", child.name,
|
|
42
|
+
node_type: :element,
|
|
43
|
+
namespace_uri: ns&.values&.first,
|
|
44
|
+
namespace_prefix: ns&.keys&.first)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# Serialize-side mirror of the plan fast path: builds the output
|
|
6
|
+
# tree directly through leptris's fused create_child chain — no
|
|
7
|
+
# XmlElement tree, no DeclarationPlanner pass, no builder context
|
|
8
|
+
# stack, no moxml wrapper minting. Serialized shapes must be the
|
|
9
|
+
# plain ones (attributes, scalars, collections, nested models,
|
|
10
|
+
# raw fragments, content runs); custom-method, polymorphic,
|
|
11
|
+
# union, and multi-spelling rows keep the interpretive serializer
|
|
12
|
+
# (their output flows through hash-shaped custom APIs).
|
|
13
|
+
#
|
|
14
|
+
# Ordered/mixed models serialize from element_order when the
|
|
15
|
+
# instance carries one (parsed models): text runs, comments, and
|
|
16
|
+
# element entries in document order, mirroring the interpretive
|
|
17
|
+
# order applier — including its content-attr-first text reads
|
|
18
|
+
# (mutations to the model show up) and CDATA flattening. Models
|
|
19
|
+
# without element_order fall back interpretively (return nil).
|
|
20
|
+
module PlanSerializer
|
|
21
|
+
SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
|
|
22
|
+
nested ordered_deferred raw content
|
|
23
|
+
content_deferred].freeze
|
|
24
|
+
CONTENT_KINDS = %i[content content_deferred].freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# plan: the compiler's entry for instance's class
|
|
28
|
+
def call(instance, plan)
|
|
29
|
+
return nil if plan[:ordered] && instance.element_order.nil?
|
|
30
|
+
|
|
31
|
+
doc = ::Leptris::XML::Document.create
|
|
32
|
+
root = doc.create_element(plan[:tree][:name])
|
|
33
|
+
doc.root = root
|
|
34
|
+
if plan[:ordered]
|
|
35
|
+
build_ordered(root, instance, plan, doc)
|
|
36
|
+
else
|
|
37
|
+
build(root, instance, plan, doc)
|
|
38
|
+
end
|
|
39
|
+
doc.to_xml(indent: 2, no_decl: true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Whether the compiled plan is serialize-shaped. Ordered/mixed
|
|
43
|
+
# models serialize through build_ordered from element_order;
|
|
44
|
+
# plain models through plan rows.
|
|
45
|
+
def serializable?(plan)
|
|
46
|
+
plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
|
|
47
|
+
SERIALIZABLE_KINDS.include?(kind)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def register
|
|
54
|
+
Lutaml::Model::Config.default_register
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build(element, instance, plan, doc)
|
|
58
|
+
write_attributes(element, instance, plan)
|
|
59
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
60
|
+
value = value_of(instance, attr, delegate)
|
|
61
|
+
next unless rule.render?(value, instance)
|
|
62
|
+
|
|
63
|
+
case kind
|
|
64
|
+
when :scalar
|
|
65
|
+
add_leaf(element, row_name(rule, spelling),
|
|
66
|
+
attr.serialize(value, :xml, register), doc)
|
|
67
|
+
when :collection_native, :collection_cb
|
|
68
|
+
Array(value).each do |item|
|
|
69
|
+
add_leaf(element, rule.name.to_s,
|
|
70
|
+
attr.serialize(item, :xml, register), doc)
|
|
71
|
+
end
|
|
72
|
+
when :nested, :ordered_deferred
|
|
73
|
+
Array(value).each do |item|
|
|
74
|
+
child_plan = PlanCompiler.compile(item.class, register)
|
|
75
|
+
child = element.create_child(child_plan[:tree][:name])
|
|
76
|
+
if child_plan[:ordered]
|
|
77
|
+
return nil unless build_ordered(child, item, child_plan, doc)
|
|
78
|
+
|
|
79
|
+
else
|
|
80
|
+
build(child, item, child_plan, doc)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
when :raw
|
|
84
|
+
Array(value).each { |raw| element.add_child(raw.to_s) }
|
|
85
|
+
when :content, :content_deferred
|
|
86
|
+
Array(value).each { |run| element.add_child(doc.create_text_node(run.to_s)) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Element_order-driven emission (the interpretive order
|
|
92
|
+
# applier's contract): text runs and comments verbatim, each
|
|
93
|
+
# element entry consuming the next item of its collection
|
|
94
|
+
# (document order). Text reads prefer the content attribute
|
|
95
|
+
# when it lines up with the text-node count, so mutations to
|
|
96
|
+
# the model are reflected; element_order text is the fallback.
|
|
97
|
+
# PIs drop (interpretive parity). Returns nil when a nested
|
|
98
|
+
# ordered child lacks element_order (caller falls back).
|
|
99
|
+
def build_ordered(element, instance, plan, doc)
|
|
100
|
+
write_attributes(element, instance, plan)
|
|
101
|
+
rows_by_name = {}
|
|
102
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
103
|
+
next unless rule.name
|
|
104
|
+
|
|
105
|
+
rows_by_name[rule.name.to_s] ||= [rule, attr, kind, spelling,
|
|
106
|
+
delegate]
|
|
107
|
+
end
|
|
108
|
+
content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
|
|
109
|
+
CONTENT_KINDS.include?(kind) && rule.name.nil?
|
|
110
|
+
end
|
|
111
|
+
order = instance.element_order
|
|
112
|
+
element_indices = ::Hash.new(0)
|
|
113
|
+
content_attr = content_entry && content_entry[1]
|
|
114
|
+
content_value = content_attr &&
|
|
115
|
+
value_of(instance, content_attr, content_entry[4])
|
|
116
|
+
text_node_count = order.count { |o| o.type == "Text" }
|
|
117
|
+
use_content_index = content_value.is_a?(Array) &&
|
|
118
|
+
content_value.length == text_node_count
|
|
119
|
+
content_cdata = content_entry && content_entry[0].cdata
|
|
120
|
+
text_node_index = 0
|
|
121
|
+
|
|
122
|
+
order.each do |object|
|
|
123
|
+
case object.type
|
|
124
|
+
when "Text"
|
|
125
|
+
text = if content_attr && !content_value.nil?
|
|
126
|
+
if use_content_index
|
|
127
|
+
content_value[text_node_index].to_s
|
|
128
|
+
elsif !content_value.is_a?(Array) && text_node_count <= 1
|
|
129
|
+
content_value.to_s
|
|
130
|
+
else
|
|
131
|
+
object.text_content || object.name
|
|
132
|
+
end
|
|
133
|
+
else
|
|
134
|
+
object.text_content || object.name
|
|
135
|
+
end
|
|
136
|
+
text_node_index += 1
|
|
137
|
+
next if text.nil?
|
|
138
|
+
# Mixed content keeps whitespace; ordered-only skips it
|
|
139
|
+
next if content_attr.nil? && text.strip.empty?
|
|
140
|
+
|
|
141
|
+
element.add_child(content_cdata ? doc.create_cdata(text) : doc.create_text_node(text))
|
|
142
|
+
when "Comment"
|
|
143
|
+
element.add_child(doc.create_comment(object.text_content))
|
|
144
|
+
when "Element"
|
|
145
|
+
entry = rows_by_name[object.name]
|
|
146
|
+
next unless entry
|
|
147
|
+
|
|
148
|
+
_, attr, kind, _spelling, delegate = entry
|
|
149
|
+
value = value_of(instance, attr, delegate)
|
|
150
|
+
case kind
|
|
151
|
+
when :collection_native, :collection_cb
|
|
152
|
+
index = element_indices[object.name]
|
|
153
|
+
items = Array(value)
|
|
154
|
+
next unless index < items.length
|
|
155
|
+
|
|
156
|
+
element_indices[object.name] += 1
|
|
157
|
+
add_leaf(element, object.name,
|
|
158
|
+
attr.serialize(items[index], :xml, register), doc)
|
|
159
|
+
when :nested, :ordered_deferred
|
|
160
|
+
item = if attr.collection?
|
|
161
|
+
index = element_indices[object.name]
|
|
162
|
+
element_indices[object.name] += 1
|
|
163
|
+
Array(value)[index]
|
|
164
|
+
else
|
|
165
|
+
value
|
|
166
|
+
end
|
|
167
|
+
next unless item
|
|
168
|
+
|
|
169
|
+
child_plan = PlanCompiler.compile(item.class, register)
|
|
170
|
+
child = element.create_child(child_plan[:tree][:name])
|
|
171
|
+
if child_plan[:ordered]
|
|
172
|
+
return nil unless build_ordered(child, item, child_plan, doc)
|
|
173
|
+
else
|
|
174
|
+
build(child, item, child_plan, doc)
|
|
175
|
+
end
|
|
176
|
+
when :raw
|
|
177
|
+
element.add_child(value.to_s) unless value.nil?
|
|
178
|
+
when :scalar
|
|
179
|
+
add_leaf(element, object.name,
|
|
180
|
+
attr.serialize(value, :xml, register), doc)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Attributes in document order when the instance recorded it
|
|
188
|
+
# (parsed models), else plan row order.
|
|
189
|
+
def write_attributes(element, instance, plan)
|
|
190
|
+
recorded = instance.attribute_order
|
|
191
|
+
if recorded && !recorded.empty?
|
|
192
|
+
by_name = {}
|
|
193
|
+
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
194
|
+
by_name[rule.name.to_s] = [rule, attr, delegate]
|
|
195
|
+
end
|
|
196
|
+
names = recorded |
|
|
197
|
+
plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
|
|
198
|
+
names.each do |name|
|
|
199
|
+
write_attribute(element, instance, name, by_name[name])
|
|
200
|
+
end
|
|
201
|
+
else
|
|
202
|
+
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
203
|
+
write_attribute(element, instance, rule.name.to_s,
|
|
204
|
+
[rule, attr, delegate])
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def write_attribute(element, instance, name, entry)
|
|
210
|
+
return unless entry
|
|
211
|
+
|
|
212
|
+
_rule, attr, delegate = entry
|
|
213
|
+
value = value_of(instance, attr, delegate)
|
|
214
|
+
return if value.nil?
|
|
215
|
+
|
|
216
|
+
element.set_attribute(name,
|
|
217
|
+
attr.serialize(value, :xml, register).to_s)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def add_leaf(element, name, value, doc)
|
|
221
|
+
child = element.create_child(name)
|
|
222
|
+
child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
|
|
223
|
+
child
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def value_of(instance, attr, delegate)
|
|
227
|
+
if delegate
|
|
228
|
+
target = instance.public_send(delegate)
|
|
229
|
+
target&.public_send(attr.name)
|
|
230
|
+
else
|
|
231
|
+
instance.public_send(attr.name)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def row_name(rule, spelling)
|
|
236
|
+
(spelling || rule.name).to_s
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|