lutaml-model 0.8.100 → 0.8.114
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 +6 -5
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +11 -0
- data/lib/lutaml/key_value/transform.rb +4 -0
- data/lib/lutaml/key_value/transformation.rb +10 -0
- data/lib/lutaml/model/attribute.rb +43 -3
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +21 -3
- data/lib/lutaml/model/serialize/initialization.rb +14 -1
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +1 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -45
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +161 -102
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +276 -58
- data/lib/lutaml/xml/leptris/plan_serializer.rb +298 -45
- data/lib/lutaml/xml/leptris/plan_walk.rb +6 -3
- data/lib/lutaml/xml/leptris.rb +25 -84
- data/lib/lutaml/xml/model_transform.rb +5 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +15 -6
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +11 -0
- data/lutaml-model.gemspec +3 -3
- data/spec/lutaml/json/yeptris_adapter_spec.rb +3 -10
- data/spec/lutaml/model/adapter_resolver_configured_spec.rb +32 -8
- data/spec/lutaml/model/collection_strict_assignment_spec.rb +55 -0
- data/spec/lutaml/model/singular_tolerance_spec.rb +159 -0
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +12 -6
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +0 -11
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +0 -9
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +3 -10
- metadata +16 -10
|
@@ -15,14 +15,23 @@ module Lutaml
|
|
|
15
15
|
# (their values echo name and type_tag; native collection values
|
|
16
16
|
# echo neither — leptris-ruby#220).
|
|
17
17
|
module PlanHydrator
|
|
18
|
+
# Row kinds whose child type/plan resolve once per plan (see
|
|
19
|
+
# child_row_plans).
|
|
20
|
+
CHILD_ROW_KINDS = %i[nested ordered_deferred].freeze
|
|
21
|
+
|
|
18
22
|
class << self
|
|
19
|
-
# plan: the
|
|
23
|
+
# plan: the compiled plan entry for model_class
|
|
20
24
|
# value: the walk root PlanValue (element)
|
|
21
25
|
# node: the parsed source element (leptris); ordered/mixed
|
|
22
26
|
# plans rebuild element_order from it and ordered children
|
|
23
27
|
# hydrate natively against their own nodes
|
|
28
|
+
# declaration_plan_mode: mirrors from_xml's
|
|
29
|
+
# :import_declaration_plan option (:skip records nothing;
|
|
30
|
+
# :lazy is honored as :eager — the lazy variant pins the
|
|
31
|
+
# raw engine DOM, the retention the eager default exists
|
|
32
|
+
# to avoid)
|
|
24
33
|
def call(model_class, plan, value, parent: nil, node: nil,
|
|
25
|
-
register: nil)
|
|
34
|
+
register: nil, declaration_plan_mode: :eager)
|
|
26
35
|
# The child model's declared lutaml_default_register takes
|
|
27
36
|
# precedence over the ambient register — the same
|
|
28
37
|
# resolve_for_child contract the interpretive path applies.
|
|
@@ -32,9 +41,11 @@ module Lutaml
|
|
|
32
41
|
register = Lutaml::Model::Register.resolve_for_child(
|
|
33
42
|
model_class, register || Lutaml::Model::Config.default_register
|
|
34
43
|
)
|
|
35
|
-
buckets = node && plan[:needs_nodes]
|
|
44
|
+
buckets = if node && plan[:needs_nodes]
|
|
45
|
+
element_buckets(node, plan[:contested_names])
|
|
46
|
+
end
|
|
36
47
|
attr_kwargs = attributes_kwargs(plan, value, node)
|
|
37
|
-
child_kwargs, children, delegates =
|
|
48
|
+
child_kwargs, children, delegates, run_children =
|
|
38
49
|
children_kwargs(model_class, plan, value, buckets, register)
|
|
39
50
|
plan[:collection_defaults].each do |name|
|
|
40
51
|
next if attr_kwargs.key?(name) || child_kwargs.key?(name)
|
|
@@ -44,27 +55,92 @@ module Lutaml
|
|
|
44
55
|
# TODO.max-perf/33: the bulk constructor — allocate +
|
|
45
56
|
# compiled writers, from_hash semantics — measured ~30x on
|
|
46
57
|
# hydration-heavy corpora versus the generic constructor.
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
attr_kwargs.update(child_kwargs)
|
|
59
|
+
instance = model_class.instantiate(attr_kwargs, register)
|
|
49
60
|
instance.lutaml_parent = parent if parent
|
|
50
61
|
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
62
|
+
# Root prefix fidelity (the interpretive transform records
|
|
63
|
+
# it from the source element): prefixed sources keep their
|
|
64
|
+
# prefixes through the serialize lane's format
|
|
65
|
+
# preservation.
|
|
66
|
+
if parent.nil? && node.respond_to?(:prefix) && node.prefix &&
|
|
67
|
+
!node.prefix.empty?
|
|
68
|
+
instance.xml_namespace_prefix = node.prefix
|
|
69
|
+
end
|
|
51
70
|
# #909: order summaries are per-instance pinned records —
|
|
52
71
|
# read-only consumers skip them via the config.
|
|
53
72
|
if node && plan[:ordered] &&
|
|
54
73
|
Lutaml::Model::Config.instance.element_order_tracking
|
|
55
74
|
instance.element_order = PlanOrder.build(node)
|
|
56
75
|
end
|
|
76
|
+
# Root declaration-plan parity (the interpretive
|
|
77
|
+
# transform's eager mode): consumers and the interpretive
|
|
78
|
+
# serialize fallback read import_declaration_plan state —
|
|
79
|
+
# plan-hydrated roots must carry the same in-scope xmlns
|
|
80
|
+
# declarations or round-trips drop them (#872). The root's
|
|
81
|
+
# xsi:schemaLocation rides the same state (the
|
|
82
|
+
# interpretive set_schema_location contract).
|
|
83
|
+
if parent.nil? && node
|
|
84
|
+
record_declaration_plan(instance, node, model_class) if declaration_plan_mode != :skip
|
|
85
|
+
record_schema_location(instance, node)
|
|
86
|
+
end
|
|
57
87
|
children.each do |child|
|
|
58
88
|
child.lutaml_parent = instance
|
|
59
89
|
child.lutaml_root ||= instance.lutaml_root || instance
|
|
60
90
|
end
|
|
61
|
-
interpret_deferred(model_class, plan, value, instance, node,
|
|
91
|
+
interpret_deferred(model_class, plan, value, instance, node,
|
|
92
|
+
buckets, run_children)
|
|
62
93
|
route_delegates(delegates, instance)
|
|
63
94
|
instance
|
|
64
95
|
end
|
|
65
96
|
|
|
66
97
|
private
|
|
67
98
|
|
|
99
|
+
def record_declaration_plan(instance, node, model_class)
|
|
100
|
+
ns_data = collect_native_namespaces(node)
|
|
101
|
+
return if ns_data.empty?
|
|
102
|
+
|
|
103
|
+
plan = Lutaml::Xml::DeclarationPlan.from_input_with_locations(
|
|
104
|
+
ns_data, model_class.mappings_for(:xml)
|
|
105
|
+
)
|
|
106
|
+
instance.import_declaration_plan = plan if plan
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The interpretive parse stores the root's xsi:schemaLocation
|
|
110
|
+
# verbatim (set_schema_location); the writer re-emits it.
|
|
111
|
+
def record_schema_location(instance, node)
|
|
112
|
+
schema_loc = node.attributes.each_value.find do |a|
|
|
113
|
+
a.name.to_s.split(":", 2).last == "schemaLocation"
|
|
114
|
+
end
|
|
115
|
+
instance.raw_schema_location = schema_loc.value if schema_loc
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Same ns_data shape as ModelTransform.collect_element_namespaces,
|
|
119
|
+
# collected straight off the engine nodes — no wrapper tree.
|
|
120
|
+
# Iterates children with an inline element filter:
|
|
121
|
+
# element_children materializes a filtered array per node,
|
|
122
|
+
# which the niso allocation gate caught at scale.
|
|
123
|
+
def collect_native_namespaces(element, path = [], result = {})
|
|
124
|
+
own = element.namespace_definitions
|
|
125
|
+
unless own.empty?
|
|
126
|
+
namespaces = {}
|
|
127
|
+
own.each do |ns|
|
|
128
|
+
key = ns.prefix || :default
|
|
129
|
+
namespaces[key] = { uri: ns.href, prefix: ns.prefix,
|
|
130
|
+
format: ns.prefix ? :prefix : :default }
|
|
131
|
+
end
|
|
132
|
+
result[path.dup] = namespaces
|
|
133
|
+
end
|
|
134
|
+
element.children.each do |child|
|
|
135
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
136
|
+
|
|
137
|
+
path.push(child.name)
|
|
138
|
+
collect_native_namespaces(child, path, result)
|
|
139
|
+
path.pop
|
|
140
|
+
end
|
|
141
|
+
result
|
|
142
|
+
end
|
|
143
|
+
|
|
68
144
|
def register
|
|
69
145
|
Lutaml::Model::Config.default_register
|
|
70
146
|
end
|
|
@@ -73,8 +149,12 @@ module Lutaml
|
|
|
73
149
|
kwargs = {}
|
|
74
150
|
sole_claimants = sole_claimant_names(plan)
|
|
75
151
|
ns_attr_names = plan[:ns_attr_names]
|
|
76
|
-
plan_attr_rows(plan)
|
|
77
|
-
|
|
152
|
+
rows = plan_attr_rows(plan)
|
|
153
|
+
# #1670 batch face (leptris 1.9.351.1): one FFI crossing
|
|
154
|
+
# per node instead of one per (node x attribute name).
|
|
155
|
+
bound = value.attributes_snapshot
|
|
156
|
+
rows.each do |rule, attr, name|
|
|
157
|
+
v = bound[name]
|
|
78
158
|
if v.nil? && node && sole_claimants.include?(name)
|
|
79
159
|
# #754/#790 parity: a sole-claimant attribute rule binds
|
|
80
160
|
# any qualification (the interpretive matcher's lenient
|
|
@@ -152,8 +232,8 @@ module Lutaml
|
|
|
152
232
|
# hydration allocated two or three strings per attribute row
|
|
153
233
|
# per instance.
|
|
154
234
|
def plan_attr_rows(plan)
|
|
155
|
-
plan[:attr_row_names] ||= plan[:attr_rows].map do |rule, attr|
|
|
156
|
-
[rule, attr, rule.name.to_s]
|
|
235
|
+
plan[:attr_row_names] ||= plan[:attr_rows].map do |rule, attr, row_name|
|
|
236
|
+
[rule, attr, row_name || rule.name.to_s]
|
|
157
237
|
end
|
|
158
238
|
end
|
|
159
239
|
|
|
@@ -164,7 +244,8 @@ module Lutaml
|
|
|
164
244
|
def children_kwargs(_model_class, plan, value, buckets = nil,
|
|
165
245
|
register = nil)
|
|
166
246
|
register ||= Lutaml::Model::Config.default_register
|
|
167
|
-
grouped, tagged = group_children(value,
|
|
247
|
+
grouped, tagged, run_children = group_children(value,
|
|
248
|
+
plan[:row_tags])
|
|
168
249
|
buckets = nil unless buckets && plan[:needs_nodes]
|
|
169
250
|
kwargs = {}
|
|
170
251
|
children = []
|
|
@@ -173,8 +254,21 @@ module Lutaml
|
|
|
173
254
|
# the block-backed Hash is a fresh object per hydrated row.
|
|
174
255
|
spellings = nil
|
|
175
256
|
row_tags = plan[:row_tags]
|
|
176
|
-
|
|
257
|
+
wire_names = row_wire_names(plan)
|
|
258
|
+
row_memo = child_row_plans(plan, register)
|
|
259
|
+
# Array#each with a top-of-loop counter: each_with_index
|
|
260
|
+
# mints an enumerator per hydrated node — 14.7K allocations
|
|
261
|
+
# per parse on the pnas guardrail — and next must keep its
|
|
262
|
+
# continue semantics.
|
|
263
|
+
rows = plan[:rows]
|
|
264
|
+
idx = -1
|
|
265
|
+
# rubocop:disable Metrics/BlockLength -- one row-kind
|
|
266
|
+
# dispatch table; splitting it would scatter the contract
|
|
267
|
+
rows.each do |row|
|
|
268
|
+
idx += 1
|
|
269
|
+
rule, attr, kind, spelling, delegate, _row_type = row
|
|
177
270
|
tag = row_tags&.[](idx)
|
|
271
|
+
name = wire_names[idx]
|
|
178
272
|
case kind
|
|
179
273
|
when :scalar
|
|
180
274
|
# Raw passthrough: the model constructor is the single
|
|
@@ -192,7 +286,7 @@ module Lutaml
|
|
|
192
286
|
first = if tag
|
|
193
287
|
tagged[tag]&.first
|
|
194
288
|
else
|
|
195
|
-
grouped.dig(
|
|
289
|
+
grouped.dig(name, 0)
|
|
196
290
|
end
|
|
197
291
|
v = first&.string_value
|
|
198
292
|
v = mapped_empty_value(rule) if v.nil? && first
|
|
@@ -204,7 +298,7 @@ module Lutaml
|
|
|
204
298
|
when :wildcard_any
|
|
205
299
|
# interpreted post-instance (interpret_deferred)
|
|
206
300
|
when :content
|
|
207
|
-
runs = content_runs(value)
|
|
301
|
+
runs = content_runs(value, run_children)
|
|
208
302
|
if runs.empty? && !attr.collection?
|
|
209
303
|
# An empty element's content binds through the rule's
|
|
210
304
|
# value map for scalar content attributes: "" for the
|
|
@@ -220,13 +314,13 @@ module Lutaml
|
|
|
220
314
|
# walk against its own node + element_order from the
|
|
221
315
|
# node's children. Without one (direct PlanHydrator
|
|
222
316
|
# use), the subtree fragment-parses interpretively.
|
|
223
|
-
if buckets
|
|
224
|
-
child_type =
|
|
225
|
-
|
|
226
|
-
items =
|
|
317
|
+
if buckets && row_memo[idx]
|
|
318
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
319
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
320
|
+
items = (selected || []).map do |n|
|
|
227
321
|
call(child_type, child_plan,
|
|
228
322
|
child_plan[:descriptor].walk(n), node: n,
|
|
229
|
-
register:
|
|
323
|
+
register: child_register)
|
|
230
324
|
end
|
|
231
325
|
unless items.empty?
|
|
232
326
|
children.concat(items)
|
|
@@ -234,8 +328,28 @@ module Lutaml
|
|
|
234
328
|
attr.collection? ? items : items.first)
|
|
235
329
|
end
|
|
236
330
|
end
|
|
331
|
+
when :child_deferred
|
|
332
|
+
# Uncompilable child (cycles, opted-out shapes): each
|
|
333
|
+
# source node casts through the attribute — the full
|
|
334
|
+
# interpretive machinery on the bridged island,
|
|
335
|
+
# identical semantics to the interpretive path.
|
|
336
|
+
contested_uri = if plan[:contested_names]&.include?(name)
|
|
337
|
+
child_type_uri(attr, register)
|
|
338
|
+
end
|
|
339
|
+
elements = deferred_elements(value, name, buckets,
|
|
340
|
+
contested_uri)
|
|
341
|
+
next if elements.empty?
|
|
342
|
+
|
|
343
|
+
results = elements.map do |element|
|
|
344
|
+
attr.cast(element, :xml, register)
|
|
345
|
+
end
|
|
346
|
+
unless results.empty?
|
|
347
|
+
children.concat(results)
|
|
348
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
349
|
+
attr.collection? ? results : results.first)
|
|
350
|
+
end
|
|
237
351
|
when :collection_cb
|
|
238
|
-
values = grouped[
|
|
352
|
+
values = grouped[name].to_a.map do |item|
|
|
239
353
|
item&.string_value
|
|
240
354
|
end
|
|
241
355
|
values = mapped_capture_values(values, rule)
|
|
@@ -249,7 +363,7 @@ module Lutaml
|
|
|
249
363
|
values = if tag
|
|
250
364
|
tagged[tag].to_a.map(&:string_value)
|
|
251
365
|
else
|
|
252
|
-
native_collection(value,
|
|
366
|
+
native_collection(value, name)
|
|
253
367
|
end
|
|
254
368
|
values = mapped_capture_values(values, rule) unless values.nil?
|
|
255
369
|
unless values.nil?
|
|
@@ -259,16 +373,34 @@ module Lutaml
|
|
|
259
373
|
spellings ||= Hash.new { |h, k| h[k] = [] }
|
|
260
374
|
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
261
375
|
when :nested
|
|
262
|
-
child_type =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.map do |
|
|
269
|
-
call(child_type, child_plan,
|
|
270
|
-
|
|
376
|
+
child_type, child_plan, child_register, contested = row_memo[idx]
|
|
377
|
+
selected = child_selection(buckets, name, child_plan, contested)
|
|
378
|
+
if selected && contested
|
|
379
|
+
# Contested name: the walk's grouped values cross-echo
|
|
380
|
+
# every same-name element into every row — dispatch
|
|
381
|
+
# per source node instead (the walk is row-scoped).
|
|
382
|
+
items = selected.map do |n|
|
|
383
|
+
call(child_type, child_plan,
|
|
384
|
+
child_plan[:descriptor].walk(n), node: n,
|
|
385
|
+
register: child_register)
|
|
271
386
|
end
|
|
387
|
+
else
|
|
388
|
+
values = grouped.fetch(name, [])
|
|
389
|
+
cursor = if child_plan[:needs_nodes] && buckets
|
|
390
|
+
selected
|
|
391
|
+
end
|
|
392
|
+
# Indexed loop: each_with_index.map mints an
|
|
393
|
+
# enumerator per node with nested rows.
|
|
394
|
+
count = values.size
|
|
395
|
+
items = Array.new(count)
|
|
396
|
+
pos = 0
|
|
397
|
+
while pos < count
|
|
398
|
+
items[pos] = call(child_type, child_plan, values[pos],
|
|
399
|
+
node: cursor && cursor[pos],
|
|
400
|
+
register: child_register)
|
|
401
|
+
pos += 1
|
|
402
|
+
end
|
|
403
|
+
end
|
|
272
404
|
next if items.empty?
|
|
273
405
|
|
|
274
406
|
children.concat(items)
|
|
@@ -276,6 +408,7 @@ module Lutaml
|
|
|
276
408
|
attr.collection? ? items : items.first)
|
|
277
409
|
end
|
|
278
410
|
end
|
|
411
|
+
# rubocop:enable Metrics/BlockLength
|
|
279
412
|
unless !spellings || spellings.empty?
|
|
280
413
|
spellings.each do |(rule, attr), groups|
|
|
281
414
|
# Interpretive order for shared-attribute groups is
|
|
@@ -289,7 +422,7 @@ module Lutaml
|
|
|
289
422
|
attr.collection? ? values : values.first)
|
|
290
423
|
end
|
|
291
424
|
end
|
|
292
|
-
[kwargs, children, delegates]
|
|
425
|
+
[kwargs, children, delegates, run_children]
|
|
293
426
|
end
|
|
294
427
|
|
|
295
428
|
# Deferred islands: raw subtrees become wrapper elements via a
|
|
@@ -300,11 +433,20 @@ module Lutaml
|
|
|
300
433
|
# node (no fragment parse — the reparse dominated deferred
|
|
301
434
|
# hydration). Ordered children only land here without a node.
|
|
302
435
|
def interpret_deferred(model_class, plan, value, instance, node = nil,
|
|
303
|
-
buckets = nil)
|
|
304
|
-
|
|
436
|
+
buckets = nil, run_children = nil)
|
|
437
|
+
# Array#each + top counter: no per-node enumerator (see
|
|
438
|
+
# children_kwargs).
|
|
439
|
+
rows = plan[:rows]
|
|
440
|
+
idx = -1
|
|
441
|
+
rows.each do |row|
|
|
442
|
+
idx += 1
|
|
443
|
+
rule = row[0]
|
|
444
|
+
attr = row[1]
|
|
445
|
+
kind = row[2]
|
|
446
|
+
wire_name = row_wire_names(plan)[idx]
|
|
305
447
|
case kind
|
|
306
448
|
when :content_deferred
|
|
307
|
-
runs = content_runs(value)
|
|
449
|
+
runs = content_runs(value, run_children)
|
|
308
450
|
if runs.empty?
|
|
309
451
|
# The interpretive pipeline classifies the raw ""
|
|
310
452
|
# through the rule's value map: the :empty directive
|
|
@@ -327,13 +469,13 @@ module Lutaml
|
|
|
327
469
|
# concatenated).
|
|
328
470
|
instance.public_send(:"#{attr.name}=", runs.join)
|
|
329
471
|
when :raw
|
|
330
|
-
raws = raw_strings(value,
|
|
472
|
+
raws = raw_strings(value, wire_name)
|
|
331
473
|
next if raws.empty?
|
|
332
474
|
|
|
333
475
|
instance.public_send(:"#{attr.name}=",
|
|
334
476
|
attr.collection? ? raws : raws.first)
|
|
335
477
|
when :custom_method
|
|
336
|
-
elements = deferred_elements(value,
|
|
478
|
+
elements = deferred_elements(value, wire_name, buckets)
|
|
337
479
|
next if elements.empty?
|
|
338
480
|
|
|
339
481
|
args = attr.collection? ? elements : elements.first
|
|
@@ -343,7 +485,7 @@ module Lutaml
|
|
|
343
485
|
when :ordered_deferred
|
|
344
486
|
next if node # hydrated natively in children_kwargs
|
|
345
487
|
|
|
346
|
-
results = deferred_elements(value,
|
|
488
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
347
489
|
attr.cast(element, :xml, register,
|
|
348
490
|
lutaml_parent: instance,
|
|
349
491
|
lutaml_root: instance.lutaml_root || instance)
|
|
@@ -386,7 +528,7 @@ module Lutaml
|
|
|
386
528
|
attr.collection? ? results : results.first,
|
|
387
529
|
)
|
|
388
530
|
when :polymorphic
|
|
389
|
-
results = deferred_elements(value,
|
|
531
|
+
results = deferred_elements(value, wire_name, buckets).map do |element|
|
|
390
532
|
attr.cast(element, :xml, register,
|
|
391
533
|
polymorphic: rule.polymorphic,
|
|
392
534
|
lutaml_parent: instance,
|
|
@@ -405,11 +547,30 @@ module Lutaml
|
|
|
405
547
|
# construction only, no parse), falling back to a fragment
|
|
406
548
|
# parse of the captured subtree when no node is available.
|
|
407
549
|
# Both lists are document-ordered for the row's name.
|
|
408
|
-
def deferred_elements(value,
|
|
409
|
-
|
|
410
|
-
|
|
550
|
+
def deferred_elements(value, name, buckets, contested_uri = nil)
|
|
551
|
+
return raw_strings(value, name).map { |raw| fragment_element(raw) } unless buckets
|
|
552
|
+
|
|
553
|
+
nodes = buckets[name]
|
|
554
|
+
return [] unless nodes
|
|
555
|
+
|
|
556
|
+
if contested_uri
|
|
557
|
+
# Contested name: bind only the (URI, local) matches —
|
|
558
|
+
# the type's own mapping namespace is the row identity.
|
|
559
|
+
nodes = buckets[[contested_uri, name]] || []
|
|
560
|
+
end
|
|
561
|
+
nodes.map { |n| bridge_element(n) }
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def child_type_uri(attr, register)
|
|
565
|
+
type = attr.type(register)
|
|
566
|
+
return nil unless type.is_a?(Class) &&
|
|
567
|
+
type.include?(::Lutaml::Model::Serialize)
|
|
411
568
|
|
|
412
|
-
|
|
569
|
+
type.mappings_for(:xml, register)&.namespace_uri
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def contested_name?(plan, rule)
|
|
573
|
+
plan[:contested_names]&.include?(rule.name.to_s) || false
|
|
413
574
|
end
|
|
414
575
|
|
|
415
576
|
def bridge_element(node)
|
|
@@ -451,15 +612,14 @@ module Lutaml
|
|
|
451
612
|
end
|
|
452
613
|
end
|
|
453
614
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
615
|
+
# Content runs ride the children snapshot: the nil-name
|
|
616
|
+
# entries ARE the :collection run groups (one crossing per
|
|
617
|
+
# node via group_children, none here). The per-group
|
|
618
|
+
# at/count crossings are the drain itself.
|
|
619
|
+
def content_runs(_value, run_children)
|
|
620
|
+
(run_children || []).flat_map do |group|
|
|
621
|
+
Array.new(group.count) { |j| group.at(j).string_value }
|
|
461
622
|
end
|
|
462
|
-
out
|
|
463
623
|
end
|
|
464
624
|
|
|
465
625
|
# Native collection rows echo their producing row's name
|
|
@@ -474,11 +634,11 @@ module Lutaml
|
|
|
474
634
|
nil
|
|
475
635
|
end
|
|
476
636
|
|
|
477
|
-
def raw_strings(value,
|
|
637
|
+
def raw_strings(value, name)
|
|
478
638
|
out = []
|
|
479
639
|
value.count.times do |idx|
|
|
480
640
|
child = value.at(idx)
|
|
481
|
-
if child.name ==
|
|
641
|
+
if child.name == name && child.kind == :raw
|
|
482
642
|
out << child.string_value
|
|
483
643
|
end
|
|
484
644
|
end
|
|
@@ -499,7 +659,7 @@ module Lutaml
|
|
|
499
659
|
# Frozen with the plan: recomputing the tally per hydration
|
|
500
660
|
# showed in the #876-era profile as Enumerable#find/tally churn.
|
|
501
661
|
plan[:sole_claimants] ||= plan[:attr_rows]
|
|
502
|
-
.map { |rule, _attr| rule.name.to_s }.tally
|
|
662
|
+
.map { |rule, _attr, row_name| row_name || rule.name.to_s }.tally
|
|
503
663
|
.select { |_n, c| c == 1 }.keys
|
|
504
664
|
end
|
|
505
665
|
|
|
@@ -527,8 +687,14 @@ module Lutaml
|
|
|
527
687
|
grouped = {}
|
|
528
688
|
want_tags = !row_tags.nil?
|
|
529
689
|
tagged = want_tags ? {} : nil
|
|
690
|
+
run_children = nil
|
|
530
691
|
names.each_with_index do |name, i|
|
|
531
|
-
|
|
692
|
+
if name.nil?
|
|
693
|
+
# Content runs: same snapshot pass — at()/count() per
|
|
694
|
+
# child cost a crossing and a wrapper each.
|
|
695
|
+
(run_children ||= []) << children[i]
|
|
696
|
+
next
|
|
697
|
+
end
|
|
532
698
|
|
|
533
699
|
child = children[i]
|
|
534
700
|
(grouped[name] ||= []) << child
|
|
@@ -536,7 +702,7 @@ module Lutaml
|
|
|
536
702
|
(tagged[tags[i]] ||= []) << child
|
|
537
703
|
end
|
|
538
704
|
end
|
|
539
|
-
[grouped, tagged]
|
|
705
|
+
[grouped, tagged, run_children]
|
|
540
706
|
end
|
|
541
707
|
|
|
542
708
|
# Element children bucketed by local name, document order
|
|
@@ -544,15 +710,67 @@ module Lutaml
|
|
|
544
710
|
# ordered-child hydration. Only built when the plan's subtree
|
|
545
711
|
# needs source nodes; namespace-qualified models never get
|
|
546
712
|
# here (the compiler keeps them interpretive).
|
|
547
|
-
|
|
713
|
+
# Contested names need (URI, local) node buckets; every other
|
|
714
|
+
# plan hydrates from the name view alone — the qualified keys
|
|
715
|
+
# are one array + hash write per child and measured as an
|
|
716
|
+
# allocation regression on element-heavy corpora (niso gate).
|
|
717
|
+
def element_buckets(node, contested_names = nil)
|
|
548
718
|
buckets = {}
|
|
719
|
+
qualified = contested_names && !contested_names.empty?
|
|
549
720
|
node.children.each do |child|
|
|
550
721
|
next unless child.is_a?(::Leptris::XML::Element)
|
|
551
722
|
|
|
552
|
-
|
|
723
|
+
name = child.name
|
|
724
|
+
(buckets[name] ||= []) << child
|
|
725
|
+
if qualified
|
|
726
|
+
uri = child.namespace&.href
|
|
727
|
+
(buckets[[uri, name]] ||= []) << child if uri
|
|
728
|
+
end
|
|
553
729
|
end
|
|
554
730
|
buckets
|
|
555
731
|
end
|
|
732
|
+
|
|
733
|
+
# Selected source nodes for a child row: contested names
|
|
734
|
+
# dispatch by (URI, local); every other row binds the whole
|
|
735
|
+
# name bucket leniently (any qualification). The pair form
|
|
736
|
+
# this replaces allocated a fresh array per row per node on
|
|
737
|
+
# the ordered walk (niso gate).
|
|
738
|
+
def child_selection(buckets, name, child_plan, contested)
|
|
739
|
+
all = buckets && buckets[name]
|
|
740
|
+
return all unless buckets && all && contested
|
|
741
|
+
|
|
742
|
+
tree_ns = child_plan[:tree][:ns]
|
|
743
|
+
exact_uri = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
744
|
+
exact_uri ? (buckets[[exact_uri, name]] || []) : all
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
# Per-(plan, register) child-row tuples, computed once and
|
|
748
|
+
# reused across every node the walk hands this plan: the
|
|
749
|
+
# per-node type lookup, compile hit, child register
|
|
750
|
+
# resolution, and contested flag were each a measurable slice
|
|
751
|
+
# of the ordered-lane allocation profile (niso gate).
|
|
752
|
+
def child_row_plans(plan, register)
|
|
753
|
+
plan[:child_row_memo] ||= {}
|
|
754
|
+
plan[:child_row_memo][register] ||= plan[:rows].map do |rule, attr, kind, _spelling, _delegate, row_type|
|
|
755
|
+
next nil unless CHILD_ROW_KINDS.include?(kind)
|
|
756
|
+
|
|
757
|
+
child_type = row_type || attr.type(register)
|
|
758
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
759
|
+
child_register = Lutaml::Model::Register.resolve_for_child(
|
|
760
|
+
child_type, register
|
|
761
|
+
)
|
|
762
|
+
[child_type, child_plan, child_register,
|
|
763
|
+
contested_name?(plan, rule)]
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Plan-frozen wire names: rule.name.to_s minted a fresh string
|
|
768
|
+
# per row per node in the hot loop.
|
|
769
|
+
def row_wire_names(plan)
|
|
770
|
+
plan[:row_wire_names] ||= plan[:rows].map do |rule, _attr, _kind, _spelling, _delegate, _row_type|
|
|
771
|
+
rule.name.to_s
|
|
772
|
+
end
|
|
773
|
+
end
|
|
556
774
|
end
|
|
557
775
|
end
|
|
558
776
|
end
|