lutaml-model 0.8.102 → 0.8.103
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/lutaml/model/configuration.rb +7 -0
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +5 -1
- data/lib/lutaml/model/serialize/initialization.rb +1 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/leptris/native_emission.rb +2 -2
- data/lib/lutaml/xml/leptris/plan_compiler.rb +148 -32
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +185 -23
- data/lib/lutaml/xml/leptris/plan_serializer.rb +270 -43
- data/lib/lutaml/xml/leptris/plan_walk.rb +6 -2
- data/spec/lutaml/xml/plan_fast_path_spec.rb +9 -4
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- metadata +2 -1
|
@@ -16,13 +16,18 @@ module Lutaml
|
|
|
16
16
|
# echo neither — leptris-ruby#220).
|
|
17
17
|
module PlanHydrator
|
|
18
18
|
class << self
|
|
19
|
-
# plan: the
|
|
19
|
+
# plan: the compiled plan entry for model_class
|
|
20
20
|
# value: the walk root PlanValue (element)
|
|
21
21
|
# node: the parsed source element (leptris); ordered/mixed
|
|
22
22
|
# plans rebuild element_order from it and ordered children
|
|
23
23
|
# hydrate natively against their own nodes
|
|
24
|
+
# declaration_plan_mode: mirrors from_xml's
|
|
25
|
+
# :import_declaration_plan option (:skip records nothing;
|
|
26
|
+
# :lazy is honored as :eager — the lazy variant pins the
|
|
27
|
+
# raw engine DOM, the retention the eager default exists
|
|
28
|
+
# to avoid)
|
|
24
29
|
def call(model_class, plan, value, parent: nil, node: nil,
|
|
25
|
-
register: nil)
|
|
30
|
+
register: nil, declaration_plan_mode: :eager)
|
|
26
31
|
# The child model's declared lutaml_default_register takes
|
|
27
32
|
# precedence over the ambient register — the same
|
|
28
33
|
# resolve_for_child contract the interpretive path applies.
|
|
@@ -32,7 +37,9 @@ module Lutaml
|
|
|
32
37
|
register = Lutaml::Model::Register.resolve_for_child(
|
|
33
38
|
model_class, register || Lutaml::Model::Config.default_register
|
|
34
39
|
)
|
|
35
|
-
buckets = node && plan[:needs_nodes]
|
|
40
|
+
buckets = if node && plan[:needs_nodes]
|
|
41
|
+
element_buckets(node, plan[:contested_names])
|
|
42
|
+
end
|
|
36
43
|
attr_kwargs = attributes_kwargs(plan, value, node)
|
|
37
44
|
child_kwargs, children, delegates =
|
|
38
45
|
children_kwargs(model_class, plan, value, buckets, register)
|
|
@@ -48,12 +55,31 @@ module Lutaml
|
|
|
48
55
|
register)
|
|
49
56
|
instance.lutaml_parent = parent if parent
|
|
50
57
|
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
58
|
+
# Root prefix fidelity (the interpretive transform records
|
|
59
|
+
# it from the source element): prefixed sources keep their
|
|
60
|
+
# prefixes through the serialize lane's format
|
|
61
|
+
# preservation.
|
|
62
|
+
if parent.nil? && node.respond_to?(:prefix) && node.prefix &&
|
|
63
|
+
!node.prefix.empty?
|
|
64
|
+
instance.xml_namespace_prefix = node.prefix
|
|
65
|
+
end
|
|
51
66
|
# #909: order summaries are per-instance pinned records —
|
|
52
67
|
# read-only consumers skip them via the config.
|
|
53
68
|
if node && plan[:ordered] &&
|
|
54
69
|
Lutaml::Model::Config.instance.element_order_tracking
|
|
55
70
|
instance.element_order = PlanOrder.build(node)
|
|
56
71
|
end
|
|
72
|
+
# Root declaration-plan parity (the interpretive
|
|
73
|
+
# transform's eager mode): consumers and the interpretive
|
|
74
|
+
# serialize fallback read import_declaration_plan state —
|
|
75
|
+
# plan-hydrated roots must carry the same in-scope xmlns
|
|
76
|
+
# declarations or round-trips drop them (#872). The root's
|
|
77
|
+
# xsi:schemaLocation rides the same state (the
|
|
78
|
+
# interpretive set_schema_location contract).
|
|
79
|
+
if parent.nil? && node
|
|
80
|
+
record_declaration_plan(instance, node, model_class) if declaration_plan_mode != :skip
|
|
81
|
+
record_schema_location(instance, node)
|
|
82
|
+
end
|
|
57
83
|
children.each do |child|
|
|
58
84
|
child.lutaml_parent = instance
|
|
59
85
|
child.lutaml_root ||= instance.lutaml_root || instance
|
|
@@ -65,6 +91,51 @@ module Lutaml
|
|
|
65
91
|
|
|
66
92
|
private
|
|
67
93
|
|
|
94
|
+
def record_declaration_plan(instance, node, model_class)
|
|
95
|
+
ns_data = collect_native_namespaces(node)
|
|
96
|
+
return if ns_data.empty?
|
|
97
|
+
|
|
98
|
+
plan = Lutaml::Xml::DeclarationPlan.from_input_with_locations(
|
|
99
|
+
ns_data, model_class.mappings_for(:xml)
|
|
100
|
+
)
|
|
101
|
+
instance.import_declaration_plan = plan if plan
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The interpretive parse stores the root's xsi:schemaLocation
|
|
105
|
+
# verbatim (set_schema_location); the writer re-emits it.
|
|
106
|
+
def record_schema_location(instance, node)
|
|
107
|
+
schema_loc = node.attributes.each_value.find do |a|
|
|
108
|
+
a.name.to_s.split(":", 2).last == "schemaLocation"
|
|
109
|
+
end
|
|
110
|
+
instance.raw_schema_location = schema_loc.value if schema_loc
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Same ns_data shape as ModelTransform.collect_element_namespaces,
|
|
114
|
+
# collected straight off the engine nodes — no wrapper tree.
|
|
115
|
+
# Iterates children with an inline element filter:
|
|
116
|
+
# element_children materializes a filtered array per node,
|
|
117
|
+
# which the niso allocation gate caught at scale.
|
|
118
|
+
def collect_native_namespaces(element, path = [], result = {})
|
|
119
|
+
own = element.namespace_definitions
|
|
120
|
+
unless own.empty?
|
|
121
|
+
namespaces = {}
|
|
122
|
+
own.each do |ns|
|
|
123
|
+
key = ns.prefix || :default
|
|
124
|
+
namespaces[key] = { uri: ns.href, prefix: ns.prefix,
|
|
125
|
+
format: ns.prefix ? :prefix : :default }
|
|
126
|
+
end
|
|
127
|
+
result[path.dup] = namespaces
|
|
128
|
+
end
|
|
129
|
+
element.children.each do |child|
|
|
130
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
131
|
+
|
|
132
|
+
path.push(child.name)
|
|
133
|
+
collect_native_namespaces(child, path, result)
|
|
134
|
+
path.pop
|
|
135
|
+
end
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
|
|
68
139
|
def register
|
|
69
140
|
Lutaml::Model::Config.default_register
|
|
70
141
|
end
|
|
@@ -152,8 +223,8 @@ module Lutaml
|
|
|
152
223
|
# hydration allocated two or three strings per attribute row
|
|
153
224
|
# per instance.
|
|
154
225
|
def plan_attr_rows(plan)
|
|
155
|
-
plan[:attr_row_names] ||= plan[:attr_rows].map do |rule, attr|
|
|
156
|
-
[rule, attr, rule.name.to_s]
|
|
226
|
+
plan[:attr_row_names] ||= plan[:attr_rows].map do |rule, attr, row_name|
|
|
227
|
+
[rule, attr, row_name || rule.name.to_s]
|
|
157
228
|
end
|
|
158
229
|
end
|
|
159
230
|
|
|
@@ -173,7 +244,9 @@ module Lutaml
|
|
|
173
244
|
# the block-backed Hash is a fresh object per hydrated row.
|
|
174
245
|
spellings = nil
|
|
175
246
|
row_tags = plan[:row_tags]
|
|
176
|
-
|
|
247
|
+
# rubocop:disable Metrics/BlockLength -- one row-kind
|
|
248
|
+
# dispatch table; splitting it would scatter the contract
|
|
249
|
+
plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate, row_type), idx|
|
|
177
250
|
tag = row_tags&.[](idx)
|
|
178
251
|
case kind
|
|
179
252
|
when :scalar
|
|
@@ -221,9 +294,11 @@ module Lutaml
|
|
|
221
294
|
# node's children. Without one (direct PlanHydrator
|
|
222
295
|
# use), the subtree fragment-parses interpretively.
|
|
223
296
|
if buckets
|
|
224
|
-
child_type = attr.type(register)
|
|
297
|
+
child_type = row_type || attr.type(register)
|
|
225
298
|
child_plan = PlanCompiler.compile(child_type, register)
|
|
226
|
-
|
|
299
|
+
selected, = child_bucket(buckets, rule, child_plan,
|
|
300
|
+
contested_name?(plan, rule))
|
|
301
|
+
items = (selected || []).map do |n|
|
|
227
302
|
call(child_type, child_plan,
|
|
228
303
|
child_plan[:descriptor].walk(n), node: n,
|
|
229
304
|
register: register)
|
|
@@ -234,6 +309,26 @@ module Lutaml
|
|
|
234
309
|
attr.collection? ? items : items.first)
|
|
235
310
|
end
|
|
236
311
|
end
|
|
312
|
+
when :child_deferred
|
|
313
|
+
# Uncompilable child (cycles, opted-out shapes): each
|
|
314
|
+
# source node casts through the attribute — the full
|
|
315
|
+
# interpretive machinery on the bridged island,
|
|
316
|
+
# identical semantics to the interpretive path.
|
|
317
|
+
contested_uri = if plan[:contested_names]&.include?(rule.name.to_s)
|
|
318
|
+
child_type_uri(attr, register)
|
|
319
|
+
end
|
|
320
|
+
elements = deferred_elements(value, rule, buckets,
|
|
321
|
+
contested_uri)
|
|
322
|
+
next if elements.empty?
|
|
323
|
+
|
|
324
|
+
results = elements.map do |element|
|
|
325
|
+
attr.cast(element, :xml, register)
|
|
326
|
+
end
|
|
327
|
+
unless results.empty?
|
|
328
|
+
children.concat(results)
|
|
329
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
330
|
+
attr.collection? ? results : results.first)
|
|
331
|
+
end
|
|
237
332
|
when :collection_cb
|
|
238
333
|
values = grouped[rule.name.to_s].to_a.map do |item|
|
|
239
334
|
item&.string_value
|
|
@@ -259,16 +354,30 @@ module Lutaml
|
|
|
259
354
|
spellings ||= Hash.new { |h, k| h[k] = [] }
|
|
260
355
|
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
261
356
|
when :nested
|
|
262
|
-
child_type = attr.type(register)
|
|
357
|
+
child_type = row_type || attr.type(register)
|
|
263
358
|
child_plan = PlanCompiler.compile(child_type, register)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
359
|
+
selected, = child_bucket(buckets, rule, child_plan,
|
|
360
|
+
contested_name?(plan, rule))
|
|
361
|
+
if selected && plan[:contested_names]&.include?(rule.name.to_s)
|
|
362
|
+
# Contested name: the walk's grouped values cross-echo
|
|
363
|
+
# every same-name element into every row — dispatch
|
|
364
|
+
# per source node instead (the walk is row-scoped).
|
|
365
|
+
items = selected.map do |n|
|
|
366
|
+
call(child_type, child_plan,
|
|
367
|
+
child_plan[:descriptor].walk(n), node: n,
|
|
368
|
+
register: register)
|
|
271
369
|
end
|
|
370
|
+
else
|
|
371
|
+
values = grouped.fetch(rule.name.to_s, [])
|
|
372
|
+
cursor = if child_plan[:needs_nodes] && buckets
|
|
373
|
+
selected
|
|
374
|
+
end
|
|
375
|
+
items = values.each_with_index
|
|
376
|
+
.map do |v, i|
|
|
377
|
+
call(child_type, child_plan, v,
|
|
378
|
+
node: cursor && cursor[i], register: register)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
272
381
|
next if items.empty?
|
|
273
382
|
|
|
274
383
|
children.concat(items)
|
|
@@ -276,6 +385,7 @@ module Lutaml
|
|
|
276
385
|
attr.collection? ? items : items.first)
|
|
277
386
|
end
|
|
278
387
|
end
|
|
388
|
+
# rubocop:enable Metrics/BlockLength
|
|
279
389
|
unless !spellings || spellings.empty?
|
|
280
390
|
spellings.each do |(rule, attr), groups|
|
|
281
391
|
# Interpretive order for shared-attribute groups is
|
|
@@ -405,11 +515,30 @@ module Lutaml
|
|
|
405
515
|
# construction only, no parse), falling back to a fragment
|
|
406
516
|
# parse of the captured subtree when no node is available.
|
|
407
517
|
# Both lists are document-ordered for the row's name.
|
|
408
|
-
def deferred_elements(value, rule, buckets)
|
|
409
|
-
|
|
410
|
-
return nodes.map { |n| bridge_element(n) } if nodes
|
|
518
|
+
def deferred_elements(value, rule, buckets, contested_uri = nil)
|
|
519
|
+
return raw_strings(value, rule).map { |raw| fragment_element(raw) } unless buckets
|
|
411
520
|
|
|
412
|
-
|
|
521
|
+
nodes = buckets[rule.name.to_s]
|
|
522
|
+
return [] unless nodes
|
|
523
|
+
|
|
524
|
+
if contested_uri
|
|
525
|
+
# Contested name: bind only the (URI, local) matches —
|
|
526
|
+
# the type's own mapping namespace is the row identity.
|
|
527
|
+
nodes = buckets[[contested_uri, rule.name.to_s]] || []
|
|
528
|
+
end
|
|
529
|
+
nodes.map { |n| bridge_element(n) }
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def child_type_uri(attr, register)
|
|
533
|
+
type = attr.type(register)
|
|
534
|
+
return nil unless type.is_a?(Class) &&
|
|
535
|
+
type.include?(::Lutaml::Model::Serialize)
|
|
536
|
+
|
|
537
|
+
type.mappings_for(:xml, register)&.namespace_uri
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def contested_name?(plan, rule)
|
|
541
|
+
plan[:contested_names]&.include?(rule.name.to_s) || false
|
|
413
542
|
end
|
|
414
543
|
|
|
415
544
|
def bridge_element(node)
|
|
@@ -499,7 +628,7 @@ module Lutaml
|
|
|
499
628
|
# Frozen with the plan: recomputing the tally per hydration
|
|
500
629
|
# showed in the #876-era profile as Enumerable#find/tally churn.
|
|
501
630
|
plan[:sole_claimants] ||= plan[:attr_rows]
|
|
502
|
-
.map { |rule, _attr| rule.name.to_s }.tally
|
|
631
|
+
.map { |rule, _attr, row_name| row_name || rule.name.to_s }.tally
|
|
503
632
|
.select { |_n, c| c == 1 }.keys
|
|
504
633
|
end
|
|
505
634
|
|
|
@@ -544,15 +673,48 @@ module Lutaml
|
|
|
544
673
|
# ordered-child hydration. Only built when the plan's subtree
|
|
545
674
|
# needs source nodes; namespace-qualified models never get
|
|
546
675
|
# here (the compiler keeps them interpretive).
|
|
547
|
-
|
|
676
|
+
# Contested names need (URI, local) node buckets; every other
|
|
677
|
+
# plan hydrates from the name view alone — the qualified keys
|
|
678
|
+
# are one array + hash write per child and measured as an
|
|
679
|
+
# allocation regression on element-heavy corpora (niso gate).
|
|
680
|
+
def element_buckets(node, contested_names = nil)
|
|
548
681
|
buckets = {}
|
|
682
|
+
qualified = contested_names && !contested_names.empty?
|
|
549
683
|
node.children.each do |child|
|
|
550
684
|
next unless child.is_a?(::Leptris::XML::Element)
|
|
551
685
|
|
|
552
|
-
|
|
686
|
+
name = child.name
|
|
687
|
+
(buckets[name] ||= []) << child
|
|
688
|
+
if qualified
|
|
689
|
+
uri = child.namespace&.href
|
|
690
|
+
(buckets[[uri, name]] ||= []) << child if uri
|
|
691
|
+
end
|
|
553
692
|
end
|
|
554
693
|
buckets
|
|
555
694
|
end
|
|
695
|
+
|
|
696
|
+
# (URI, local) child identity — the interpretive matcher's
|
|
697
|
+
# typed-element dispatch: same-name rows in different
|
|
698
|
+
# namespaces bind by the child type's exact URI (w14:docId vs
|
|
699
|
+
# w15:docId under one model). Only CONTESTED names dispatch
|
|
700
|
+
# exactly — a uniquely-named row binds the whole name bucket
|
|
701
|
+
# leniently (any qualification), and the uri-keyed view is
|
|
702
|
+
# built only for plans that carry contested names. Returns
|
|
703
|
+
# [selected, all] where all is the name bucket (aligned with
|
|
704
|
+
# the walk's grouped values for position filtering).
|
|
705
|
+
def child_bucket(buckets, rule, child_plan, contested)
|
|
706
|
+
name = rule.name.to_s
|
|
707
|
+
all = buckets ? buckets[name] : nil
|
|
708
|
+
return [all, all] unless buckets && all && contested
|
|
709
|
+
|
|
710
|
+
tree_ns = child_plan[:tree][:ns]
|
|
711
|
+
exact_uri = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
712
|
+
if exact_uri
|
|
713
|
+
qualified = buckets[[exact_uri, name]]
|
|
714
|
+
return [qualified || [], all]
|
|
715
|
+
end
|
|
716
|
+
[all, all]
|
|
717
|
+
end
|
|
556
718
|
end
|
|
557
719
|
end
|
|
558
720
|
end
|