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
|
@@ -18,12 +18,30 @@ module Lutaml
|
|
|
18
18
|
# order applier — including its content-attr-first text reads
|
|
19
19
|
# (mutations to the model show up) and CDATA flattening. Models
|
|
20
20
|
# without element_order fall back interpretively (return nil).
|
|
21
|
+
# Writer-added values the order snapshot does not cover ride the
|
|
22
|
+
# OrderReconciler: the walk consumes the reconciled order and
|
|
23
|
+
# unplaceable rules emit appended (the interpretive
|
|
24
|
+
# apply_remaining_rules contract).
|
|
21
25
|
module PlanSerializer
|
|
22
26
|
SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
|
|
23
27
|
nested ordered_deferred raw content
|
|
24
28
|
content_deferred].freeze
|
|
29
|
+
PLAN_SERIALIZABLE_KINDS = SERIALIZABLE_KINDS
|
|
25
30
|
CONTENT_KINDS = %i[content content_deferred].freeze
|
|
26
31
|
|
|
32
|
+
# Reconciler host: the interpretive support modules are written
|
|
33
|
+
# against the Transformation's combined surface (the reconciler
|
|
34
|
+
# consumes applier helpers such as find_rule_for_element and
|
|
35
|
+
# rule_discriminator), so the plan lane drives them through a
|
|
36
|
+
# dedicated host holding both. The helpers are pure functions
|
|
37
|
+
# of their arguments — the frozen host carries no state.
|
|
38
|
+
class ReconcilerHost
|
|
39
|
+
include(TransformationSupport::OrderedApplier)
|
|
40
|
+
include(TransformationSupport::OrderReconciler)
|
|
41
|
+
include(TransformationSupport::SkipLogic)
|
|
42
|
+
end
|
|
43
|
+
RECONCILER = ReconcilerHost.new.freeze
|
|
44
|
+
|
|
27
45
|
class << self
|
|
28
46
|
# plan: the compiler's entry for instance's class
|
|
29
47
|
def call(instance, plan, register = nil)
|
|
@@ -55,11 +73,25 @@ module Lutaml
|
|
|
55
73
|
)
|
|
56
74
|
doc.root = root
|
|
57
75
|
ns_ctx.declare_on(root)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
# The interpretive writer's raw schemaLocation re-emission
|
|
77
|
+
# (handle_schema_location): the parse-captured string rides
|
|
78
|
+
# back as xsi:schemaLocation with its declaration.
|
|
79
|
+
if instance.is_a?(::Lutaml::Model::Serialize) &&
|
|
80
|
+
instance.raw_schema_location &&
|
|
81
|
+
!instance.raw_schema_location.empty?
|
|
82
|
+
root.set_attribute(
|
|
83
|
+
"xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"
|
|
84
|
+
)
|
|
85
|
+
root.set_attribute("xsi:schemaLocation",
|
|
86
|
+
instance.raw_schema_location)
|
|
62
87
|
end
|
|
88
|
+
built = if plan[:ordered]
|
|
89
|
+
build_ordered(root, instance, plan, doc, register, ns_ctx)
|
|
90
|
+
else
|
|
91
|
+
build(root, instance, plan, doc, register, ns_ctx)
|
|
92
|
+
end
|
|
93
|
+
return nil unless built
|
|
94
|
+
|
|
63
95
|
doc.to_xml(indent: 2, no_decl: true)
|
|
64
96
|
end
|
|
65
97
|
|
|
@@ -115,6 +147,38 @@ module Lutaml
|
|
|
115
147
|
@hoisted[uri] = prefix
|
|
116
148
|
@forms[uri] = { prefix: prefix, uri: uri }
|
|
117
149
|
end
|
|
150
|
+
# Format preservation (the interpretive writer's
|
|
151
|
+
# rule): an instance parsed from a PREFIXED source keeps
|
|
152
|
+
# its prefixes — the model's own namespace hoists with
|
|
153
|
+
# the instance's recorded prefix even when the fresh
|
|
154
|
+
# planner preferred the default-ns format.
|
|
155
|
+
model_uri = plan[:tree][:ns] && plan[:tree][:ns][:exact]
|
|
156
|
+
if ns_prefix && !ns_prefix.empty? && model_uri &&
|
|
157
|
+
@hoisted.key?(model_uri)
|
|
158
|
+
@hoisted[model_uri] = ns_prefix
|
|
159
|
+
@forms[model_uri] = { prefix: ns_prefix, uri: model_uri }
|
|
160
|
+
end
|
|
161
|
+
# Source declaration preservation (the interpretive
|
|
162
|
+
# writer's stored-plan merge): prefixes the parsed source
|
|
163
|
+
# declared are kept for URIs the document emits. The
|
|
164
|
+
# planner's class-analysis path has no stored-plan
|
|
165
|
+
# access, so the merge reads the instance's recorded plan
|
|
166
|
+
# directly.
|
|
167
|
+
stored = if instance.is_a?(::Lutaml::Model::Serialize)
|
|
168
|
+
instance.import_declaration_plan
|
|
169
|
+
end
|
|
170
|
+
if stored.is_a?(Lutaml::Xml::DeclarationPlan) &&
|
|
171
|
+
stored.root_node.is_a?(
|
|
172
|
+
Lutaml::Xml::DeclarationPlan::ElementNode,
|
|
173
|
+
) && (ns_prefix.nil? || ns_prefix.empty?)
|
|
174
|
+
original_uris = stored.original_namespace_uris || {}
|
|
175
|
+
stored.root_node.hoisted_declarations.each do |prefix, uri|
|
|
176
|
+
effective_uri = original_uris[uri] || uri
|
|
177
|
+
already = @hoisted.value?(effective_uri) ||
|
|
178
|
+
@hoisted.value?(uri)
|
|
179
|
+
@hoisted[prefix] = uri if already && !@hoisted.key?(prefix)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
118
182
|
collect(plan[:tree])
|
|
119
183
|
end
|
|
120
184
|
|
|
@@ -165,14 +229,18 @@ module Lutaml
|
|
|
165
229
|
ns_form.is_a?(Hash) ? { prefix: @hoisted[ns_form[:exact]], uri: ns_form[:exact] } : @forms[ns_form]
|
|
166
230
|
end
|
|
167
231
|
|
|
232
|
+
# Declaration order matches the interpretive writer: the
|
|
233
|
+
# prefixed namespaces sorted by prefix, the default
|
|
234
|
+
# namespace last.
|
|
168
235
|
def declare_on(root, _attr_form_prefix = nil)
|
|
169
|
-
@hoisted.
|
|
170
|
-
|
|
171
|
-
|
|
236
|
+
@hoisted.sort_by { |_uri, prefix| [prefix ? 0 : 1, prefix.to_s] }
|
|
237
|
+
.each do |uri, prefix|
|
|
238
|
+
key = uri
|
|
239
|
+
next if key.nil? || @declared[key]
|
|
172
240
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
241
|
+
@declared[key] = true
|
|
242
|
+
root.add_namespace_definition(prefix, uri)
|
|
243
|
+
end
|
|
176
244
|
end
|
|
177
245
|
end
|
|
178
246
|
|
|
@@ -181,7 +249,7 @@ module Lutaml
|
|
|
181
249
|
# plain models through plan rows.
|
|
182
250
|
def serializable?(plan)
|
|
183
251
|
plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
|
|
184
|
-
|
|
252
|
+
PLAN_SERIALIZABLE_KINDS.include?(kind)
|
|
185
253
|
end
|
|
186
254
|
end
|
|
187
255
|
|
|
@@ -231,23 +299,33 @@ module Lutaml
|
|
|
231
299
|
end
|
|
232
300
|
when :nested, :ordered_deferred
|
|
233
301
|
Array(value).each do |item|
|
|
234
|
-
|
|
235
|
-
|
|
302
|
+
# Keyed by [class, row name]: distinct rows (sz, szCs)
|
|
303
|
+
# can share one class with different root names.
|
|
304
|
+
child_plan = nested_plans[[item.class, rule.name.to_s]] ||
|
|
305
|
+
(nested_plans[[item.class, rule.name.to_s]] = PlanCompiler.compile(item.class, reg))
|
|
236
306
|
child_ns = child_plan[:tree][:ns] || inherited_ns
|
|
307
|
+
# The ROW's wire name wins over the child plan's
|
|
308
|
+
# root — distinct rows (sz, szCs) share one class.
|
|
237
309
|
child = element.create_child(
|
|
238
|
-
ns_ctx ? ns_ctx.spell(
|
|
310
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), child_ns) : row_name(rule, spelling),
|
|
239
311
|
)
|
|
240
312
|
ns_ctx&.collect(child_plan[:tree])
|
|
241
313
|
if child_plan[:ordered]
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
314
|
+
build_ordered(child, item, child_plan,
|
|
315
|
+
doc, reg, ns_ctx, child_ns) || return
|
|
245
316
|
else
|
|
246
|
-
build(child, item, child_plan, doc, reg, ns_ctx, child_ns)
|
|
317
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
247
318
|
end
|
|
248
319
|
end
|
|
249
320
|
when :raw
|
|
250
321
|
Array(value).each { |raw| element.add_child(raw.to_s) }
|
|
322
|
+
when :child_deferred
|
|
323
|
+
# A plan-less child carrying a value (a cycle point
|
|
324
|
+
# with data, e.g. bibliography's Author → author):
|
|
325
|
+
# the interpretive serializer owns the model — the
|
|
326
|
+
# plan lane has no faithful spelling for it. Empty
|
|
327
|
+
# deferred rows ride the fast lane untouched.
|
|
328
|
+
return nil unless Array(value).empty?
|
|
251
329
|
when :content, :content_deferred
|
|
252
330
|
Array(value).each { |run| append_text(element, run.to_s, doc) }
|
|
253
331
|
end
|
|
@@ -273,30 +351,96 @@ module Lutaml
|
|
|
273
351
|
plan[:nested_plans] ||= {}
|
|
274
352
|
end
|
|
275
353
|
|
|
354
|
+
# The row's exact element URI — the child type's namespace,
|
|
355
|
+
# the same source the interpretive matcher dispatches by for
|
|
356
|
+
# (URI, local) element identity. Nil for scalar/raw/content
|
|
357
|
+
# rows (any-qualification binding). Memoized per plan (rules
|
|
358
|
+
# are stable per compiled plan).
|
|
359
|
+
def row_exact_uri(rule, attr, kind, type, reg, plan)
|
|
360
|
+
return nil unless %i[nested ordered_deferred].include?(kind)
|
|
361
|
+
|
|
362
|
+
cache = plan[:row_exact_uris] ||= {}.compare_by_identity
|
|
363
|
+
return cache[rule] if cache.key?(rule)
|
|
364
|
+
|
|
365
|
+
child_type = type || attr.type(reg)
|
|
366
|
+
child_plan = child_type && PlanCompiler.compile(child_type, reg)
|
|
367
|
+
tree_ns = child_plan && child_plan[:tree][:ns]
|
|
368
|
+
cache[rule] = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def ordered_layout_for(plan, inherited_ns, ns_index, reg)
|
|
372
|
+
cache = plan[:ordered_layouts] ||= {}.compare_by_identity
|
|
373
|
+
return cache[inherited_ns] if cache.key?(inherited_ns)
|
|
374
|
+
|
|
375
|
+
rows_by_name = {}
|
|
376
|
+
name_keys = ::Hash.new { |h, k| h[k] = [] }
|
|
377
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate, type|
|
|
378
|
+
next unless rule.name
|
|
379
|
+
|
|
380
|
+
exact = row_exact_uri(rule, attr, kind, type, reg, plan)
|
|
381
|
+
name = rule.name.to_s
|
|
382
|
+
key = exact ? [exact, name] : [nil, name]
|
|
383
|
+
rows_by_name[key] ||= [rule, attr, kind, spelling,
|
|
384
|
+
delegate,
|
|
385
|
+
ns_index ? ns_index[name] || inherited_ns : inherited_ns]
|
|
386
|
+
name_keys[name] << key
|
|
387
|
+
end
|
|
388
|
+
name_keys.default = nil
|
|
389
|
+
cache[inherited_ns] = [rows_by_name, name_keys]
|
|
390
|
+
end
|
|
391
|
+
|
|
276
392
|
# Element_order-driven emission (the interpretive order
|
|
277
393
|
# applier's contract): text runs and comments verbatim, each
|
|
278
394
|
# element entry consuming the next item of its collection
|
|
279
395
|
# (document order). Text reads prefer the content attribute
|
|
280
396
|
# when it lines up with the text-node count, so mutations to
|
|
281
397
|
# the model are reflected; element_order text is the fallback.
|
|
282
|
-
# PIs drop (interpretive parity).
|
|
283
|
-
#
|
|
398
|
+
# PIs drop (interpretive parity). A nested ordered instance
|
|
399
|
+
# without element_order (built by Ruby, not parsed) falls
|
|
400
|
+
# back to declaration order — the interpretive behavior.
|
|
284
401
|
def build_ordered(element, instance, plan, doc, reg = nil,
|
|
285
|
-
ns_ctx = nil,
|
|
402
|
+
ns_ctx = nil, inherited_ns = nil)
|
|
403
|
+
if instance.element_order.nil?
|
|
404
|
+
return build(element, instance, plan, doc, reg, ns_ctx,
|
|
405
|
+
inherited_ns)
|
|
406
|
+
end
|
|
407
|
+
|
|
286
408
|
reg ||= register
|
|
287
409
|
ns_ctx&.collect(plan[:tree])
|
|
288
410
|
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
411
|
+
ns_index = ns_ctx ? ns_index_for(plan) : nil
|
|
412
|
+
# (URI, local) row keys — the interpretive matcher's
|
|
413
|
+
# typed-element identity: same-name rows in different
|
|
414
|
+
# namespaces (w14:docId vs w15:docId) must not share one
|
|
415
|
+
# dispatch slot. Name-keyed rows (no exact URI) bind any
|
|
416
|
+
# qualification. The layout is static per (plan, inherited
|
|
417
|
+
# ns) — the reconciliation corpus re-serializes thousands
|
|
418
|
+
# of same-shaped paragraphs, so it is memoized.
|
|
419
|
+
rows_by_name, name_keys =
|
|
420
|
+
ordered_layout_for(plan, inherited_ns, ns_index, reg)
|
|
296
421
|
content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
|
|
297
422
|
CONTENT_KINDS.include?(kind) && rule.name.nil?
|
|
298
423
|
end
|
|
299
|
-
order
|
|
424
|
+
# Writer-added values sit outside the order snapshot — the
|
|
425
|
+
# reconciler inserts their entries at the interpretive
|
|
426
|
+
# anchors and returns the rules it could not place for
|
|
427
|
+
# appended emission (silent drops are data loss). The
|
|
428
|
+
# reconciler speaks CompiledRule — the interpretive
|
|
429
|
+
# transformation's memoized set; the plan's MappingRule
|
|
430
|
+
# rows cannot drive it. Pristine parsed instances skip the
|
|
431
|
+
# scan entirely: the parse-built order covers every set
|
|
432
|
+
# value, and the writer-set flag is the only flow that can
|
|
433
|
+
# produce deficits.
|
|
434
|
+
rules = if instance.mutated_after_parse?
|
|
435
|
+
compiled_rules_for(instance.class, reg)
|
|
436
|
+
end
|
|
437
|
+
if rules
|
|
438
|
+
order, fallback_rules =
|
|
439
|
+
RECONCILER.reconciled_element_order(instance, rules, {})
|
|
440
|
+
else
|
|
441
|
+
order = instance.element_order
|
|
442
|
+
fallback_rules = []
|
|
443
|
+
end
|
|
300
444
|
element_indices = ::Hash.new(0)
|
|
301
445
|
content_attr = content_entry && content_entry[1]
|
|
302
446
|
content_value = content_attr &&
|
|
@@ -330,50 +474,132 @@ module Lutaml
|
|
|
330
474
|
when "Comment"
|
|
331
475
|
element.add_child(doc.create_comment(object.text_content))
|
|
332
476
|
when "Element"
|
|
333
|
-
entry
|
|
477
|
+
# A uniquely-named row absorbs the entry at any
|
|
478
|
+
# qualification (the interpretive matcher's lenient
|
|
479
|
+
# binding — order entries written by consumers carry no
|
|
480
|
+
# namespace). Contested names dispatch strictly by
|
|
481
|
+
# (URI, local), falling back to URI-less rows.
|
|
482
|
+
name = object.name
|
|
483
|
+
keys = name_keys[name]
|
|
484
|
+
if keys.length == 1
|
|
485
|
+
dispatch_key = keys.first
|
|
486
|
+
entry = rows_by_name[dispatch_key]
|
|
487
|
+
else
|
|
488
|
+
dispatch_key = [object.namespace_uri, name]
|
|
489
|
+
entry = rows_by_name[dispatch_key]
|
|
490
|
+
unless entry
|
|
491
|
+
dispatch_key = [nil, name]
|
|
492
|
+
entry = rows_by_name[dispatch_key]
|
|
493
|
+
end
|
|
494
|
+
end
|
|
334
495
|
next unless entry
|
|
335
496
|
|
|
336
|
-
_, attr, kind, _spelling, delegate = entry
|
|
497
|
+
_, attr, kind, _spelling, delegate, row_ns = entry
|
|
337
498
|
value = value_of(instance, attr, delegate)
|
|
338
499
|
case kind
|
|
339
500
|
when :collection_native, :collection_cb
|
|
340
|
-
index = element_indices[
|
|
501
|
+
index = element_indices[dispatch_key]
|
|
341
502
|
items = Array(value)
|
|
342
503
|
next unless index < items.length
|
|
343
504
|
|
|
344
|
-
element_indices[
|
|
345
|
-
add_leaf(element,
|
|
505
|
+
element_indices[dispatch_key] += 1
|
|
506
|
+
add_leaf(element,
|
|
507
|
+
ns_ctx ? ns_ctx.spell(object.name, row_ns) : object.name,
|
|
346
508
|
attr.serialize(items[index], :xml, reg), doc)
|
|
347
509
|
when :nested, :ordered_deferred
|
|
348
510
|
item = if attr.collection?
|
|
349
|
-
index = element_indices[
|
|
350
|
-
element_indices[
|
|
511
|
+
index = element_indices[dispatch_key]
|
|
512
|
+
element_indices[dispatch_key] += 1
|
|
351
513
|
Array(value)[index]
|
|
352
514
|
else
|
|
353
515
|
value
|
|
354
516
|
end
|
|
355
517
|
next unless item
|
|
356
518
|
|
|
357
|
-
child_plan = nested_plans_for(plan)[item.class] ||
|
|
358
|
-
(nested_plans_for(plan)[item.class] = PlanCompiler.compile(item.class, reg))
|
|
359
|
-
|
|
519
|
+
child_plan = nested_plans_for(plan)[[item.class, object.name]] ||
|
|
520
|
+
(nested_plans_for(plan)[[item.class, object.name]] = PlanCompiler.compile(item.class, reg))
|
|
521
|
+
child_ns = child_plan[:tree][:ns] || row_ns || inherited_ns
|
|
522
|
+
child = element.create_child(
|
|
523
|
+
ns_ctx ? ns_ctx.spell(object.name, child_ns) : object.name,
|
|
524
|
+
)
|
|
360
525
|
if child_plan[:ordered]
|
|
361
526
|
return nil unless build_ordered(child, item, child_plan,
|
|
362
|
-
doc, reg)
|
|
527
|
+
doc, reg, ns_ctx, child_ns)
|
|
363
528
|
else
|
|
364
|
-
build(child, item, child_plan, doc, reg)
|
|
529
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
365
530
|
end
|
|
366
531
|
when :raw
|
|
367
532
|
element.add_child(value.to_s) unless value.nil?
|
|
533
|
+
when :child_deferred
|
|
534
|
+
# Valued cycle-point children: interpretive lane (see
|
|
535
|
+
# build).
|
|
536
|
+
return nil unless Array(value).empty?
|
|
368
537
|
when :scalar
|
|
369
|
-
add_leaf(element,
|
|
538
|
+
add_leaf(element,
|
|
539
|
+
ns_ctx ? ns_ctx.spell(object.name, row_ns) : object.name,
|
|
370
540
|
attr.serialize(value, :xml, reg), doc)
|
|
371
541
|
end
|
|
372
542
|
end
|
|
373
543
|
end
|
|
544
|
+
return nil if !fallback_rules.empty? &&
|
|
545
|
+
!emit_uncovered_values(element, instance, plan, fallback_rules,
|
|
546
|
+
doc, reg, ns_ctx, inherited_ns)
|
|
547
|
+
|
|
548
|
+
true
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# OrderReconciler fallback: element rules the reconciled order
|
|
552
|
+
# could not place but that still hold a value — emit them
|
|
553
|
+
# appended, the ordinary way (the interpretive
|
|
554
|
+
# apply_remaining_rules contract for the plan row model).
|
|
555
|
+
def emit_uncovered_values(element, instance, plan, fallback, doc,
|
|
556
|
+
reg, ns_ctx, inherited_ns)
|
|
557
|
+
by_attr = {}
|
|
558
|
+
plan[:rows].each { |row| by_attr[row[1].name] ||= row if row[1] }
|
|
559
|
+
fallback.each do |rule|
|
|
560
|
+
entry = by_attr[rule.attribute_name]
|
|
561
|
+
next unless entry
|
|
562
|
+
|
|
563
|
+
_rule, attr, kind, _spelling, delegate, row_ns = entry
|
|
564
|
+
value = value_of(instance, attr, delegate)
|
|
565
|
+
name = rule.name.to_s
|
|
566
|
+
Array(value).each do |item|
|
|
567
|
+
case kind
|
|
568
|
+
when :collection_native, :collection_cb, :scalar
|
|
569
|
+
add_leaf(element,
|
|
570
|
+
ns_ctx ? ns_ctx.spell(name, row_ns) : name,
|
|
571
|
+
attr.serialize(item, :xml, reg), doc)
|
|
572
|
+
when :nested, :ordered_deferred
|
|
573
|
+
next if item.nil?
|
|
574
|
+
|
|
575
|
+
child_plan =
|
|
576
|
+
nested_plans_for(plan)[[item.class, name]] ||
|
|
577
|
+
(nested_plans_for(plan)[[item.class, name]] =
|
|
578
|
+
PlanCompiler.compile(item.class, reg))
|
|
579
|
+
child_ns = child_plan[:tree][:ns] || row_ns || inherited_ns
|
|
580
|
+
child = element.create_child(
|
|
581
|
+
ns_ctx ? ns_ctx.spell(name, child_ns) : name,
|
|
582
|
+
)
|
|
583
|
+
if child_plan[:ordered]
|
|
584
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
585
|
+
doc, reg, ns_ctx, child_ns)
|
|
586
|
+
else
|
|
587
|
+
built = build(child, item, child_plan, doc, reg, ns_ctx,
|
|
588
|
+
child_ns)
|
|
589
|
+
return nil unless built
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
# Valued cycle-point rows keep the walk's contract.
|
|
594
|
+
return nil if kind == :child_deferred && !Array(value).empty?
|
|
595
|
+
end
|
|
374
596
|
true
|
|
375
597
|
end
|
|
376
598
|
|
|
599
|
+
def compiled_rules_for(klass, reg)
|
|
600
|
+
klass.transformation_for(:xml, reg)&.compiled_rules
|
|
601
|
+
end
|
|
602
|
+
|
|
377
603
|
# Attributes in document order when the instance recorded it
|
|
378
604
|
# (parsed models), else plan row order.
|
|
379
605
|
def write_attributes(element, instance, plan, reg = nil,
|
|
@@ -420,9 +646,10 @@ module Lutaml
|
|
|
420
646
|
prefix = nil)
|
|
421
647
|
return unless entry
|
|
422
648
|
|
|
423
|
-
|
|
649
|
+
rule, attr, delegate = entry
|
|
424
650
|
value = value_of(instance, attr, delegate)
|
|
425
651
|
return if value.nil?
|
|
652
|
+
return unless rule.render?(value, instance)
|
|
426
653
|
|
|
427
654
|
spelled = prefix ? "#{prefix}:#{name}" : name
|
|
428
655
|
element.set_attribute(spelled,
|
|
@@ -12,7 +12,7 @@ module Lutaml
|
|
|
12
12
|
class << self
|
|
13
13
|
# Build the plan, walk the document, and hydrate the mapped
|
|
14
14
|
# root. Returns nil when the model is not plan-compilable.
|
|
15
|
-
def call(model_class, xml,
|
|
15
|
+
def call(model_class, xml, options = {})
|
|
16
16
|
register = Lutaml::Model::Config.default_register
|
|
17
17
|
plan = PlanCompiler.compile(model_class, register)
|
|
18
18
|
return nil unless plan
|
|
@@ -23,7 +23,11 @@ module Lutaml
|
|
|
23
23
|
|
|
24
24
|
PlanHydrator.call(model_class, plan,
|
|
25
25
|
plan[:descriptor].walk(root),
|
|
26
|
-
node: root
|
|
26
|
+
node: root,
|
|
27
|
+
declaration_plan_mode:
|
|
28
|
+
options.fetch(
|
|
29
|
+
:import_declaration_plan, :eager
|
|
30
|
+
))
|
|
27
31
|
end
|
|
28
32
|
end
|
|
29
33
|
end
|
|
@@ -33,15 +33,18 @@ RSpec.describe "XML plan fast path" do
|
|
|
33
33
|
around do |example|
|
|
34
34
|
old = Lutaml::Model::Config.instance.xml_plan_fast_path
|
|
35
35
|
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
36
|
+
Lutaml::Model::Config.instance.xml_plan_ordered_path = true
|
|
36
37
|
example.run
|
|
37
38
|
ensure
|
|
38
39
|
Lutaml::Model::Config.instance.xml_plan_fast_path = old
|
|
40
|
+
Lutaml::Model::Config.instance.xml_plan_ordered_path = false
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# Real-corpus crash (niso-jats): sec-in-sec nesting is a
|
|
42
|
-
# self-referential model — compile must
|
|
43
|
-
#
|
|
44
|
-
|
|
44
|
+
# self-referential model — compile must not recurse to
|
|
45
|
+
# SystemStackError. The cycle point defers the child ROW (per-node
|
|
46
|
+
# interpretive islands) instead of opting the model out.
|
|
47
|
+
it "defers the self-referential child row instead of recursing" do
|
|
45
48
|
node = Class.new(Lutaml::Model::Serializable) do
|
|
46
49
|
attribute :text, :string
|
|
47
50
|
xml do
|
|
@@ -57,7 +60,9 @@ RSpec.describe "XML plan fast path" do
|
|
|
57
60
|
end
|
|
58
61
|
stub_const("PlanFastPath::SelfRef", node)
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(node, :default)
|
|
64
|
+
expect(plan).not_to be_nil
|
|
65
|
+
expect(plan[:rows].map { |r| r[2] }).to include(:child_deferred)
|
|
61
66
|
|
|
62
67
|
xml = "<n>#{'<n>t</n>' * 200}</n>"
|
|
63
68
|
parsed = node.from_xml(xml)
|