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