lutaml-model 0.8.84 → 0.8.85
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/lib/compat/opal/lutaml_model_boot.rb +6 -5
- data/lib/lutaml/key_value/transform.rb +3 -1
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +5 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/format.rb +3 -5
- data/lib/lutaml/xml/leptris/plan_compiler.rb +372 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +441 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +273 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +32 -0
- data/lib/lutaml/xml/leptris.rb +33 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +1 -1
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/xml/plan_fast_path_spec.rb +8 -8
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- metadata +7 -6
- data/lib/lutaml/xml/plan_compiler.rb +0 -362
- data/lib/lutaml/xml/plan_hydrator.rb +0 -464
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -271
- data/lib/lutaml/xml/plan_walk.rb +0 -30
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# Hydrates model instances from a Descriptor#walk PlanValue tree,
|
|
7
|
+
# keyed by each value's producing row name (never position — rows
|
|
8
|
+
# for missing elements are simply absent). Native rows build
|
|
9
|
+
# constructor kwargs; deferred rows (custom methods, polymorphism,
|
|
10
|
+
# unions) capture their subtree verbatim and interpret it
|
|
11
|
+
# post-walk — the fragment parse runs the existing interpretive
|
|
12
|
+
# machinery on just that island. Delegate rules hydrate
|
|
13
|
+
# post-instance onto their target object. Collection rows route
|
|
14
|
+
# natively when a single one exists, else through callback rows
|
|
15
|
+
# (their values echo name and type_tag; native collection values
|
|
16
|
+
# echo neither — leptris-ruby#220).
|
|
17
|
+
module PlanHydrator
|
|
18
|
+
class << self
|
|
19
|
+
# plan: the compiler's entry for model_class
|
|
20
|
+
# value: the walk root PlanValue (element)
|
|
21
|
+
# node: the parsed source element (leptris); ordered/mixed
|
|
22
|
+
# plans rebuild element_order from it and ordered children
|
|
23
|
+
# hydrate natively against their own nodes
|
|
24
|
+
def call(model_class, plan, value, parent: nil, node: nil,
|
|
25
|
+
register: nil)
|
|
26
|
+
# The child model's declared lutaml_default_register takes
|
|
27
|
+
# precedence over the ambient register — the same
|
|
28
|
+
# resolve_for_child contract the interpretive path applies.
|
|
29
|
+
# Without it, hydration resolves the child's symbol attribute
|
|
30
|
+
# types in the parent context and raises UnknownTypeError
|
|
31
|
+
# for ids registered only in the child's register (#876).
|
|
32
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
33
|
+
model_class, register || Lutaml::Model::Config.default_register
|
|
34
|
+
)
|
|
35
|
+
buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
|
|
36
|
+
attr_kwargs = attributes_kwargs(plan, value, node)
|
|
37
|
+
child_kwargs, children, delegates =
|
|
38
|
+
children_kwargs(model_class, plan, value, buckets, register)
|
|
39
|
+
plan[:collection_defaults].each do |name|
|
|
40
|
+
next if attr_kwargs.key?(name) || child_kwargs.key?(name)
|
|
41
|
+
|
|
42
|
+
child_kwargs[name] = []
|
|
43
|
+
end
|
|
44
|
+
# TODO.max-perf/33: the bulk constructor — allocate +
|
|
45
|
+
# compiled writers, from_hash semantics — measured ~30x on
|
|
46
|
+
# hydration-heavy corpora versus the generic constructor.
|
|
47
|
+
instance = model_class.instantiate(attr_kwargs.merge(child_kwargs),
|
|
48
|
+
register)
|
|
49
|
+
instance.lutaml_parent = parent if parent
|
|
50
|
+
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
51
|
+
instance.element_order = PlanOrder.build(node) if node && plan[:ordered]
|
|
52
|
+
children.each do |child|
|
|
53
|
+
child.lutaml_parent = instance
|
|
54
|
+
child.lutaml_root ||= instance.lutaml_root || instance
|
|
55
|
+
end
|
|
56
|
+
interpret_deferred(model_class, plan, value, instance, node, buckets)
|
|
57
|
+
route_delegates(delegates, instance)
|
|
58
|
+
instance
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def register
|
|
64
|
+
Lutaml::Model::Config.default_register
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def attributes_kwargs(plan, value, node = nil)
|
|
68
|
+
kwargs = {}
|
|
69
|
+
sole_claimants = sole_claimant_names(plan)
|
|
70
|
+
plan_attr_rows(plan).each do |rule, attr, name|
|
|
71
|
+
v = value.attribute(name)
|
|
72
|
+
if v.nil? && node && sole_claimants.include?(name)
|
|
73
|
+
# #754/#790 parity: a sole-claimant attribute rule binds
|
|
74
|
+
# any qualification (the interpretive matcher's lenient
|
|
75
|
+
# recovery). The walk captures exact (URI, local) matches
|
|
76
|
+
# only, so the qualified spelling is read off the node.
|
|
77
|
+
v = lenient_node_attribute(node, name)
|
|
78
|
+
end
|
|
79
|
+
next if v.nil?
|
|
80
|
+
|
|
81
|
+
v = v.split(rule.delimiter) if rule.delimiter
|
|
82
|
+
if rule.as_list && rule.as_list[:import]
|
|
83
|
+
v = rule.as_list[:import].call(v)
|
|
84
|
+
end
|
|
85
|
+
kwargs[attr.name.to_sym] = v
|
|
86
|
+
end
|
|
87
|
+
kwargs
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Wire-name strings are plan-frozen; materializing them per
|
|
91
|
+
# hydration allocated two or three strings per attribute row
|
|
92
|
+
# per instance.
|
|
93
|
+
def plan_attr_rows(plan)
|
|
94
|
+
plan[:attr_row_names] ||= plan[:attr_rows].map do |rule, attr|
|
|
95
|
+
[rule, attr, rule.name.to_s]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns [kwargs, hydrated_child_instances, delegate_values]
|
|
100
|
+
# — child instances come back so the caller can decorate
|
|
101
|
+
# parent/root links once the parent exists; delegate values
|
|
102
|
+
# wait for the instance (their target object must exist).
|
|
103
|
+
def children_kwargs(_model_class, plan, value, buckets = nil,
|
|
104
|
+
register = nil)
|
|
105
|
+
register ||= Lutaml::Model::Config.default_register
|
|
106
|
+
grouped, tagged = group_children(value, plan[:row_tags])
|
|
107
|
+
buckets = nil unless buckets && plan[:needs_nodes]
|
|
108
|
+
kwargs = {}
|
|
109
|
+
children = []
|
|
110
|
+
delegates = []
|
|
111
|
+
# Lazy: models without multi-spelling rows never touch it, and
|
|
112
|
+
# the block-backed Hash is a fresh object per hydrated row.
|
|
113
|
+
spellings = nil
|
|
114
|
+
row_tags = plan[:row_tags]
|
|
115
|
+
plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate), idx|
|
|
116
|
+
tag = row_tags&.[](idx)
|
|
117
|
+
case kind
|
|
118
|
+
when :scalar
|
|
119
|
+
# Raw passthrough: the model constructor is the single
|
|
120
|
+
# cast authority — pre-casting here doubled every cast.
|
|
121
|
+
# Class transforms still apply before assignment.
|
|
122
|
+
if tag && (vals = tagged[tag]) && vals.size > 1
|
|
123
|
+
# Interpretive parity: several captures into a
|
|
124
|
+
# non-collection attribute arrive as the full array.
|
|
125
|
+
v = vals.map(&:string_value)
|
|
126
|
+
else
|
|
127
|
+
first = if tag
|
|
128
|
+
tagged[tag]&.first
|
|
129
|
+
else
|
|
130
|
+
grouped.dig(rule.name.to_s, 0)
|
|
131
|
+
end
|
|
132
|
+
v = first&.string_value
|
|
133
|
+
end
|
|
134
|
+
unless v.nil?
|
|
135
|
+
v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
|
|
136
|
+
assign(kwargs, delegates, delegate, rule, attr, v)
|
|
137
|
+
end
|
|
138
|
+
when :raw, :custom_method, :polymorphic, :content_deferred
|
|
139
|
+
# interpreted post-instance (interpret_deferred)
|
|
140
|
+
when :content
|
|
141
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
142
|
+
content_runs(value))
|
|
143
|
+
when :ordered_deferred
|
|
144
|
+
# With a source node the child hydrates natively: one
|
|
145
|
+
# walk against its own node + element_order from the
|
|
146
|
+
# node's children. Without one (direct PlanHydrator
|
|
147
|
+
# use), the subtree fragment-parses interpretively.
|
|
148
|
+
if buckets
|
|
149
|
+
child_type = attr.type(register)
|
|
150
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
151
|
+
items = buckets.fetch(rule.name.to_s, []).map do |n|
|
|
152
|
+
call(child_type, child_plan,
|
|
153
|
+
child_plan[:descriptor].walk(n), node: n,
|
|
154
|
+
register: register)
|
|
155
|
+
end
|
|
156
|
+
unless items.empty?
|
|
157
|
+
children.concat(items)
|
|
158
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
159
|
+
attr.collection? ? items : items.first)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
when :collection_cb
|
|
163
|
+
values = grouped[rule.name.to_s].to_a.map(&:string_value)
|
|
164
|
+
if rule.transform.is_a?(Class)
|
|
165
|
+
values = values.map { |v| rule.transform_value(attr, v, :from, :xml) }
|
|
166
|
+
end
|
|
167
|
+
unless values.empty?
|
|
168
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
169
|
+
end
|
|
170
|
+
when :collection_native
|
|
171
|
+
values = if tag
|
|
172
|
+
tagged[tag].to_a.map(&:string_value)
|
|
173
|
+
else
|
|
174
|
+
native_collection(value, rule.name.to_s)
|
|
175
|
+
end
|
|
176
|
+
unless values.nil?
|
|
177
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
178
|
+
end
|
|
179
|
+
when :spelling
|
|
180
|
+
spellings ||= Hash.new { |h, k| h[k] = [] }
|
|
181
|
+
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
182
|
+
when :nested
|
|
183
|
+
child_type = attr.type(register)
|
|
184
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
185
|
+
cursor = if child_plan[:needs_nodes] && buckets
|
|
186
|
+
buckets.fetch(rule.name.to_s, [])
|
|
187
|
+
end
|
|
188
|
+
items = grouped.fetch(rule.name.to_s, []).each_with_index
|
|
189
|
+
.map do |v, i|
|
|
190
|
+
call(child_type, child_plan, v,
|
|
191
|
+
node: cursor && cursor[i], register: register)
|
|
192
|
+
end
|
|
193
|
+
next if items.empty?
|
|
194
|
+
|
|
195
|
+
children.concat(items)
|
|
196
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
197
|
+
attr.collection? ? items : items.first)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
unless !spellings || spellings.empty?
|
|
201
|
+
spellings.each do |(rule, attr), groups|
|
|
202
|
+
# Interpretive order for shared-attribute groups is
|
|
203
|
+
# spelling-group order (first spelling's matches, then
|
|
204
|
+
# the next's), not interleaved document order.
|
|
205
|
+
values = groups.compact.flatten.map(&:string_value)
|
|
206
|
+
next if values.empty?
|
|
207
|
+
|
|
208
|
+
delegate = delegate_of(plan, rule)
|
|
209
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
210
|
+
attr.collection? ? values : values.first)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
[kwargs, children, delegates]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Deferred islands: raw subtrees become wrapper elements via a
|
|
217
|
+
# fragment parse, then the interpretive machinery runs on just
|
|
218
|
+
# that island — custom method invocation with an
|
|
219
|
+
# element-shaped argument, or the polymorphic/union cast.
|
|
220
|
+
# With the source node available the island wraps the ORIGINAL
|
|
221
|
+
# node (no fragment parse — the reparse dominated deferred
|
|
222
|
+
# hydration). Ordered children only land here without a node.
|
|
223
|
+
def interpret_deferred(model_class, plan, value, instance, node = nil,
|
|
224
|
+
buckets = nil)
|
|
225
|
+
plan[:rows].each do |rule, attr, kind, _spelling, _delegate|
|
|
226
|
+
case kind
|
|
227
|
+
when :content_deferred
|
|
228
|
+
runs = content_runs(value)
|
|
229
|
+
if runs.empty?
|
|
230
|
+
# The interpretive pipeline marks every applied rule's
|
|
231
|
+
# attribute set (model_transform apply). Skipping the
|
|
232
|
+
# mark leaves using_default? true, so the serializer's
|
|
233
|
+
# render gate suppresses a mapped reader that derives
|
|
234
|
+
# content from other attributes (#856).
|
|
235
|
+
instance.value_set_for(attr.name)
|
|
236
|
+
next
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Non-collection content attrs hold the joined text
|
|
240
|
+
# (the interpretive path assigns element text, the runs
|
|
241
|
+
# concatenated).
|
|
242
|
+
instance.public_send(:"#{attr.name}=", runs.join)
|
|
243
|
+
when :raw
|
|
244
|
+
raws = raw_strings(value, rule)
|
|
245
|
+
next if raws.empty?
|
|
246
|
+
|
|
247
|
+
instance.public_send(:"#{attr.name}=",
|
|
248
|
+
attr.collection? ? raws : raws.first)
|
|
249
|
+
when :custom_method
|
|
250
|
+
elements = deferred_elements(value, rule, buckets)
|
|
251
|
+
next if elements.empty?
|
|
252
|
+
|
|
253
|
+
args = attr.collection? ? elements : elements.first
|
|
254
|
+
rule.deserialize(instance, args,
|
|
255
|
+
model_class.attributes(register),
|
|
256
|
+
model_class)
|
|
257
|
+
when :ordered_deferred
|
|
258
|
+
next if node # hydrated natively in children_kwargs
|
|
259
|
+
|
|
260
|
+
results = deferred_elements(value, rule, buckets).map do |element|
|
|
261
|
+
attr.cast(element, :xml, register,
|
|
262
|
+
lutaml_parent: instance,
|
|
263
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
264
|
+
end
|
|
265
|
+
next if results.empty?
|
|
266
|
+
|
|
267
|
+
instance.public_send(:"#{attr.name}=",
|
|
268
|
+
attr.collection? ? results : results.first)
|
|
269
|
+
when :polymorphic
|
|
270
|
+
results = deferred_elements(value, rule, buckets).map do |element|
|
|
271
|
+
attr.cast(element, :xml, register,
|
|
272
|
+
polymorphic: rule.polymorphic,
|
|
273
|
+
lutaml_parent: instance,
|
|
274
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
275
|
+
end
|
|
276
|
+
next if results.empty?
|
|
277
|
+
|
|
278
|
+
instance.public_send(:"#{attr.name}=",
|
|
279
|
+
attr.collection? ? results : results.first)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Wrapper elements for deferred rows: the ORIGINAL source
|
|
285
|
+
# nodes bridged through moxml's wrapper cache (wrapper
|
|
286
|
+
# construction only, no parse), falling back to a fragment
|
|
287
|
+
# parse of the captured subtree when no node is available.
|
|
288
|
+
# Both lists are document-ordered for the row's name.
|
|
289
|
+
def deferred_elements(value, rule, buckets)
|
|
290
|
+
nodes = buckets && buckets[rule.name.to_s]
|
|
291
|
+
return nodes.map { |n| bridge_element(n) } if nodes
|
|
292
|
+
|
|
293
|
+
raw_strings(value, rule).map { |raw| fragment_element(raw) }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def bridge_element(node)
|
|
297
|
+
Lutaml::Xml::LeptrisElement.new(
|
|
298
|
+
Moxml::Node.wrap(node, bridge_context),
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def bridge_context
|
|
303
|
+
@bridge_context ||= Moxml::Context.new(:leptris)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def route_delegates(delegates, instance)
|
|
307
|
+
delegates.each do |rule, delegate_attr, attr, v|
|
|
308
|
+
target = instance.public_send(rule.delegate)
|
|
309
|
+
unless target
|
|
310
|
+
# the interpretive path instantiates absent delegate
|
|
311
|
+
# targets; mirror it
|
|
312
|
+
target = delegate_attr.type(register).new
|
|
313
|
+
instance.public_send(:"#{rule.delegate}=", target)
|
|
314
|
+
end
|
|
315
|
+
target.public_send(:"#{attr.name}=", v)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def delegate_of(plan, rule)
|
|
320
|
+
entry = plan[:rows].find { |r, _, _k, _| r.equal?(rule) }
|
|
321
|
+
entry && entry[4]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Delegate rules hold their values for post-instance routing
|
|
325
|
+
# (the target object must exist first); plain rules land in
|
|
326
|
+
# kwargs directly.
|
|
327
|
+
def assign(kwargs, delegates, delegate, rule, attr, value)
|
|
328
|
+
if delegate
|
|
329
|
+
delegates << [rule, delegate, attr, value]
|
|
330
|
+
else
|
|
331
|
+
kwargs[attr.name.to_sym] = value
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def content_runs(value)
|
|
336
|
+
out = []
|
|
337
|
+
value.count.times do |i|
|
|
338
|
+
c = value.at(i)
|
|
339
|
+
next unless c.name.nil? && c.kind == :collection
|
|
340
|
+
|
|
341
|
+
out.concat(Array.new(c.count) { |j| c.at(j).string_value })
|
|
342
|
+
end
|
|
343
|
+
out
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Native collection rows echo their producing row's name
|
|
347
|
+
# (leptris 1.9.178).
|
|
348
|
+
def native_collection(value, name)
|
|
349
|
+
value.count.times do |i|
|
|
350
|
+
c = value.at(i)
|
|
351
|
+
next unless c.kind == :collection && c.name == name
|
|
352
|
+
|
|
353
|
+
return Array.new(c.count) { |j| c.at(j).string_value }
|
|
354
|
+
end
|
|
355
|
+
nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def raw_strings(value, rule)
|
|
359
|
+
out = []
|
|
360
|
+
value.count.times do |idx|
|
|
361
|
+
child = value.at(idx)
|
|
362
|
+
if child.name == rule.name.to_s && child.kind == :raw
|
|
363
|
+
out << child.string_value
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
out
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Fragment deferral: parse the captured subtree back into a
|
|
370
|
+
# wrapper element. Ancestor namespace context is absent (the
|
|
371
|
+
# compiler only defers on namespace-free model chains).
|
|
372
|
+
def fragment_element(raw)
|
|
373
|
+
Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Attribute names claimed by exactly one attr row — the only
|
|
377
|
+
# names eligible for #754/#790 lenient binding (multi-claimant
|
|
378
|
+
# names stay exact; per #841 leniency is sole-claimant-only).
|
|
379
|
+
def sole_claimant_names(plan)
|
|
380
|
+
# Frozen with the plan: recomputing the tally per hydration
|
|
381
|
+
# showed in the #876-era profile as Enumerable#find/tally churn.
|
|
382
|
+
plan[:sole_claimants] ||= plan[:attr_rows]
|
|
383
|
+
.map { |rule, _attr| rule.name.to_s }.tally
|
|
384
|
+
.select { |_n, c| c == 1 }.keys
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Local-name attribute lookup on the source node, qualified
|
|
388
|
+
# spellings included (w:val matches rule "val").
|
|
389
|
+
def lenient_node_attribute(node, local_name)
|
|
390
|
+
return nil unless node.respond_to?(:attributes)
|
|
391
|
+
|
|
392
|
+
node.attributes.each_value do |attr|
|
|
393
|
+
# Leptris::XML::Attr carries the raw (possibly prefixed)
|
|
394
|
+
# name; strip the prefix for the local-name comparison.
|
|
395
|
+
name = attr.name.to_s
|
|
396
|
+
name = name.split(":").last if name.include?(":")
|
|
397
|
+
return attr.value if name == local_name
|
|
398
|
+
end
|
|
399
|
+
nil
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def group_children(value, row_tags = nil)
|
|
403
|
+
# One crossing per subtree: names/tags/children come back in
|
|
404
|
+
# parallel arrays from leptris_plan_value_children_snapshot.
|
|
405
|
+
# The plan-path version gate (PlanCompiler.compile) already
|
|
406
|
+
# guarantees leptris >= 1.9.273.0.
|
|
407
|
+
names, tags, children = value.children_snapshot
|
|
408
|
+
grouped = {}
|
|
409
|
+
want_tags = !row_tags.nil?
|
|
410
|
+
tagged = want_tags ? {} : nil
|
|
411
|
+
names.each_with_index do |name, i|
|
|
412
|
+
next if name.nil? # content runs, read separately
|
|
413
|
+
|
|
414
|
+
child = children[i]
|
|
415
|
+
(grouped[name] ||= []) << child
|
|
416
|
+
if want_tags && tags[i] != 0
|
|
417
|
+
(tagged[tags[i]] ||= []) << child
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
[grouped, tagged]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Element children bucketed by local name, document order
|
|
424
|
+
# preserved — the node-side mirror of group_children for
|
|
425
|
+
# ordered-child hydration. Only built when the plan's subtree
|
|
426
|
+
# needs source nodes; namespace-qualified models never get
|
|
427
|
+
# here (the compiler keeps them interpretive).
|
|
428
|
+
def element_buckets(node)
|
|
429
|
+
buckets = {}
|
|
430
|
+
node.children.each do |child|
|
|
431
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
432
|
+
|
|
433
|
+
(buckets[child.name] ||= []) << child
|
|
434
|
+
end
|
|
435
|
+
buckets
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# Reconstructs element_order for ordered/mixed models from the
|
|
7
|
+
# leptris node surface in one native pass, mirroring the
|
|
8
|
+
# interpretive AdapterElement#order contract: text runs, CDATA,
|
|
9
|
+
# comments, PIs, and element entries (local name + namespace
|
|
10
|
+
# uri/prefix) in document order, frozen like the interpretive
|
|
11
|
+
# product.
|
|
12
|
+
module PlanOrder
|
|
13
|
+
TEXT_MARKER = "text"
|
|
14
|
+
CDATA_MARKER = "#cdata-section"
|
|
15
|
+
COMMENT_MARKER = "comment"
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def build(node)
|
|
19
|
+
node.children.filter_map { |child| entry_for(child) }
|
|
20
|
+
.each(&:freeze).freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def entry_for(child)
|
|
26
|
+
case child
|
|
27
|
+
when ::Leptris::XML::CDATA
|
|
28
|
+
Element.new("Text", CDATA_MARKER,
|
|
29
|
+
text_content: child.content, node_type: :cdata)
|
|
30
|
+
when ::Leptris::XML::Text
|
|
31
|
+
Element.new("Text", TEXT_MARKER,
|
|
32
|
+
text_content: child.text, node_type: :text)
|
|
33
|
+
when ::Leptris::XML::Comment
|
|
34
|
+
Element.new("Comment", COMMENT_MARKER,
|
|
35
|
+
text_content: child.content, node_type: :comment)
|
|
36
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
37
|
+
Element.new("ProcessingInstruction", child.name,
|
|
38
|
+
text_content: child.content.to_s.sub(/\A\s+/, ""),
|
|
39
|
+
node_type: :processing_instruction)
|
|
40
|
+
when ::Leptris::XML::Element
|
|
41
|
+
ns = child.namespace
|
|
42
|
+
# Leptris::XML::Namespace is a single object (href/prefix),
|
|
43
|
+
# not the Nokogiri-style prefix hash — hash accessors blow
|
|
44
|
+
# up ordered+namespaced plan parses.
|
|
45
|
+
Element.new("Element", child.name,
|
|
46
|
+
node_type: :element,
|
|
47
|
+
namespace_uri: ns&.href,
|
|
48
|
+
namespace_prefix: ns&.prefix)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|