lutaml-model 0.8.101 → 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/Gemfile +6 -5
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- 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 -2
- 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 +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +161 -102
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +185 -23
- data/lib/lutaml/xml/leptris/plan_serializer.rb +291 -61
- data/lib/lutaml/xml/leptris/plan_walk.rb +6 -3
- data/lib/lutaml/xml/leptris.rb +11 -94
- data/spec/lutaml/xml/plan_fast_path_spec.rb +9 -4
- 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
- metadata +4 -2
|
@@ -53,12 +53,16 @@ module Lutaml
|
|
|
53
53
|
# name and wildcard rows carry their own :any.
|
|
54
54
|
ROW_KINDS_NS_UNQUALIFIED = %i[scalar collection nested callback
|
|
55
55
|
raw].freeze
|
|
56
|
+
# Row kinds whose hydration can dispatch per source node —
|
|
57
|
+
# the only safe claimants of a contested (multi-row) name.
|
|
58
|
+
PER_NODE_ROW_KINDS = %i[nested ordered_deferred
|
|
59
|
+
child_deferred].freeze
|
|
56
60
|
|
|
57
61
|
class << self
|
|
58
62
|
def compile(model_class, register)
|
|
59
|
-
# Version gate
|
|
60
|
-
#
|
|
61
|
-
#
|
|
63
|
+
# Version gate: below MIN_LEPTRIS_VERSION (the single
|
|
64
|
+
# capability floor) gems take the interpretive path via
|
|
65
|
+
# the nil opt-out.
|
|
62
66
|
return nil unless Leptris.plan_path_compatible?
|
|
63
67
|
|
|
64
68
|
# A child model's declared lutaml_default_register takes
|
|
@@ -96,58 +100,31 @@ module Lutaml
|
|
|
96
100
|
end
|
|
97
101
|
end
|
|
98
102
|
|
|
99
|
-
# Two engine walk defects gate walk-side consumers here; both
|
|
100
|
-
# bail models to the interpretive matcher, whose exact-first-
|
|
101
|
-
# then-any-qualification precedence binds what the walk drops.
|
|
102
|
-
# The serialize side (PlanSerializer) reads the Ruby tree and
|
|
103
|
-
# stays on the fast path. leptris 1.9.320.0 (#1587) fixes
|
|
104
|
-
# both defects — the scan is skipped there and every model
|
|
105
|
-
# rides the walk again.
|
|
106
|
-
#
|
|
107
|
-
# leptris#1585: exact-URI CHILD-row ns_uri strings are not
|
|
108
|
-
# retained engine-side — once the build anchors are GC'd (a
|
|
109
|
-
# warm walk plus one GC suffices), those rows stop matching
|
|
110
|
-
# and hydration silently answers nil.
|
|
111
|
-
#
|
|
112
|
-
# leptris#1586: plain attribute rows do not leniently match
|
|
113
|
-
# namespace-qualified wire attributes (uniword fontTable:
|
|
114
|
-
# every w:name/w:val against a plain attr row hydrates nil).
|
|
115
|
-
# A plan level carrying plain attr rows under a namespaced
|
|
116
|
-
# chain therefore cannot be trusted on the walk; attribute
|
|
117
|
-
# rows with an exact URI (or :any) and plans without any
|
|
118
|
-
# namespace form are unaffected.
|
|
119
|
-
def engine_walk_safe?(plan)
|
|
120
|
-
return true if Leptris.walk_ns_safety_compatible?
|
|
121
|
-
|
|
122
|
-
tree_safe?(plan[:tree], false)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
103
|
private
|
|
126
104
|
|
|
127
|
-
def tree_safe?(tree, ancestor_ns)
|
|
128
|
-
namespaced = ancestor_ns || tree[:ns].is_a?(Hash)
|
|
129
|
-
attrs = tree[:attributes] || []
|
|
130
|
-
return false if namespaced &&
|
|
131
|
-
attrs.any? { |a| !a[:ns].is_a?(Hash) && a[:ns] != :any }
|
|
132
|
-
|
|
133
|
-
(tree[:children] || []).all? do |row|
|
|
134
|
-
return false if row[:ns].is_a?(Hash)
|
|
135
|
-
|
|
136
|
-
next true unless row[:kind] == :nested && row[:plan]
|
|
137
|
-
|
|
138
|
-
tree_safe?(row[:plan], namespaced)
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
105
|
def build(model_class, register)
|
|
143
106
|
return nil unless model_class.is_a?(Class) &&
|
|
144
107
|
model_class.include?(::Lutaml::Model::Serialize)
|
|
145
108
|
|
|
146
109
|
mapping = model_class.mappings_for(:xml, register)
|
|
147
110
|
return nil unless compilable_mapping?(mapping)
|
|
111
|
+
# Ordered/mixed models ride the plan path only when opted
|
|
112
|
+
# in: the per-node ordered hydration beats the interpretive
|
|
113
|
+
# lane on time but allocates above the downstream budget
|
|
114
|
+
# gates (niso pnas ratio 1.25) — it ships dark until the
|
|
115
|
+
# allocation diet lands.
|
|
116
|
+
if (mapping.ordered? || mapping.mixed_content?) &&
|
|
117
|
+
!Lutaml::Model::Config.instance.xml_plan_ordered_path
|
|
118
|
+
return opt_out!(:ordered_opt_in)
|
|
119
|
+
end
|
|
120
|
+
# Processing-instruction mappings (rfcxml's <?rfc toc?>)
|
|
121
|
+
# have no plan-row spelling — the interpretive path owns
|
|
122
|
+
# them (compiling here silently dropped pi_settings).
|
|
123
|
+
return opt_out!(:processing_instruction) unless
|
|
124
|
+
mapping.processing_instruction_mappings.empty?
|
|
148
125
|
|
|
149
126
|
rows = []
|
|
150
|
-
attr_rows = [] # [[rule, attr]] hydration metadata
|
|
127
|
+
attr_rows = [] # [[rule, attr, row_name]] hydration metadata
|
|
151
128
|
plan_attrs = [] # [{name:, kind:}] rows for the engine plan
|
|
152
129
|
compiled = [] # [rule, attr, kind, spelling, delegate_target]
|
|
153
130
|
ns_attr_names = [] # exact-URI attribute rows (ambiguity guard)
|
|
@@ -243,28 +220,26 @@ module Lutaml
|
|
|
243
220
|
type = attr.type(register)
|
|
244
221
|
if serializable_type?(type)
|
|
245
222
|
child = compile(type, register)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
delegate_target]
|
|
259
|
-
rows << { name: rule.name.to_s, kind: :raw }
|
|
260
|
-
else
|
|
261
|
-
compiled << [rule, attr, :nested, nil, delegate_target]
|
|
262
|
-
needs_nodes ||= child[:needs_nodes]
|
|
263
|
-
nested_row = { name: rule.name.to_s, kind: :nested,
|
|
264
|
-
plan: child[:tree] }
|
|
265
|
-
nested_row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
266
|
-
rows << nested_row
|
|
223
|
+
if child.nil? &&
|
|
224
|
+
!Lutaml::Model::Config.instance.xml_plan_ordered_path
|
|
225
|
+
child_mapping = type.mappings_for(:xml, register)
|
|
226
|
+
if child_mapping&.ordered? ||
|
|
227
|
+
child_mapping&.mixed_content?
|
|
228
|
+
# A child refused ONLY by the ordered opt-in gate
|
|
229
|
+
# cascades: the bridged-island deferral drops its
|
|
230
|
+
# mixed children (customxml schemaRefs), so the
|
|
231
|
+
# parent compiles to nil — main's whole-model
|
|
232
|
+
# interpretive shape.
|
|
233
|
+
return opt_out!(:ordered_opt_in_cascade)
|
|
234
|
+
end
|
|
267
235
|
end
|
|
236
|
+
needs_nodes = true if child.nil? || child[:ordered]
|
|
237
|
+
compiled_entry, row = child_row_for(rule, attr, type, child,
|
|
238
|
+
delegate_target,
|
|
239
|
+
model_ns)
|
|
240
|
+
compiled << compiled_entry
|
|
241
|
+
needs_nodes ||= child && child[:needs_nodes]
|
|
242
|
+
rows << row
|
|
268
243
|
elsif rule.multiple_mappings?
|
|
269
244
|
rule.name.each do |spelling|
|
|
270
245
|
compiled << [rule, attr, :spelling, spelling.to_s,
|
|
@@ -312,6 +287,19 @@ module Lutaml
|
|
|
312
287
|
|
|
313
288
|
row_tags = partition_row_tags!(compiled, rows, tag)
|
|
314
289
|
|
|
290
|
+
# Same-name rows cross-echo in the engine walk: every source
|
|
291
|
+
# element of a name lands in every row of that name (two
|
|
292
|
+
# w14:/w15: docId rows each capture both elements). Names
|
|
293
|
+
# claimed by more than one element row are contested — they
|
|
294
|
+
# hydrate per source node (the ordered_deferred pattern)
|
|
295
|
+
# when every claimant is per-node-dispatchable, and opt the
|
|
296
|
+
# model out otherwise (the interpretive matcher owns the
|
|
297
|
+
# (URI, local) dispatch there).
|
|
298
|
+
contested = contested_element_names(rows, compiled)
|
|
299
|
+
return opt_out!(:contested_names) if contested == :unsafe
|
|
300
|
+
|
|
301
|
+
needs_nodes = true unless contested.empty?
|
|
302
|
+
|
|
315
303
|
# The plan serializer does not emit namespace declarations —
|
|
316
304
|
# namespaced models serialize through the interpretive
|
|
317
305
|
# writer (lutaml-model#847: standalone to_xml under leptris
|
|
@@ -335,13 +323,13 @@ module Lutaml
|
|
|
335
323
|
flags << :ns_lenient
|
|
336
324
|
|
|
337
325
|
# Namespace-less model child rows bind the strict unwritten
|
|
338
|
-
# spelling (bare, or inherited default xmlns)
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
-
if model_ns.nil?
|
|
326
|
+
# spelling (bare, or inherited default xmlns) — the
|
|
327
|
+
# interpretive matcher's exact no-namespace semantic,
|
|
328
|
+
# refusing prefixed spellings where the ns_lenient
|
|
329
|
+
# superset bound them. Rows with an explicit form (rule ns,
|
|
330
|
+
# type ns, :none, :any) are untouched; name-matched element
|
|
331
|
+
# row kinds only.
|
|
332
|
+
if model_ns.nil?
|
|
345
333
|
rows.each do |r|
|
|
346
334
|
next if r[:ns]
|
|
347
335
|
|
|
@@ -373,14 +361,6 @@ module Lutaml
|
|
|
373
361
|
# must receive one through the buckets chain.
|
|
374
362
|
needs_nodes = true unless ns_attr_names.empty?
|
|
375
363
|
|
|
376
|
-
# A model combining exact-URI attribute rows with nested
|
|
377
|
-
# child rows needs the walk's nested attribute capture
|
|
378
|
-
# (leptris#1563, fixed 1.9.313.0): older engines drop the
|
|
379
|
-
# children's attribute capture under such plans, so they
|
|
380
|
-
# stay interpretive. Leaf models are unaffected.
|
|
381
|
-
return opt_out!(:attr_type_ns_nested) if plan_attrs.any? { |a| a[:ns] } &&
|
|
382
|
-
rows.any? && !Leptris.nested_attr_capture_compatible?
|
|
383
|
-
|
|
384
364
|
# The catch-all row: kind :wildcard with an explicit :any ns
|
|
385
365
|
# form (pad0) — the two-pass walk routes every child no
|
|
386
366
|
# named row claimed here, tagged for hydrator routing.
|
|
@@ -403,6 +383,7 @@ module Lutaml
|
|
|
403
383
|
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
404
384
|
needs_nodes: needs_nodes,
|
|
405
385
|
collection_defaults: collection_defaults,
|
|
386
|
+
contested_names: contested.empty? ? nil : contested,
|
|
406
387
|
ns_attr_names: ns_attr_names.empty? ? nil : ns_attr_names,
|
|
407
388
|
wildcard_tag: (wildcard_tag if defined?(wildcard_tag)) }
|
|
408
389
|
end
|
|
@@ -424,9 +405,19 @@ module Lutaml
|
|
|
424
405
|
attr_local_claims)
|
|
425
406
|
return :non_scalar unless scalar_type?(attr, register)
|
|
426
407
|
|
|
427
|
-
|
|
408
|
+
raw_name = rule.name.to_s
|
|
409
|
+
# W3C-reserved xml:* attributes bind by raw name — the xml
|
|
410
|
+
# prefix is implicit (never a model namespace), so form
|
|
411
|
+
# defaults must not qualify them under the model's URI
|
|
412
|
+
# (w:t's xml:space under a :qualified WML mapping).
|
|
413
|
+
if raw_name.start_with?("xml:")
|
|
414
|
+
attr_local_claims[raw_name][:plain] += 1
|
|
415
|
+
plan_attrs << { name: raw_name }
|
|
416
|
+
attr_rows << [rule, attr, raw_name]
|
|
417
|
+
return nil
|
|
418
|
+
end
|
|
428
419
|
type_ns = attr.type_namespace_class(register)
|
|
429
|
-
|
|
420
|
+
raw_name
|
|
430
421
|
if type_ns.nil?
|
|
431
422
|
# attribute_form_default :qualified on the model's
|
|
432
423
|
# namespace qualifies locally-declared attributes under
|
|
@@ -438,39 +429,108 @@ module Lutaml
|
|
|
438
429
|
ns_class = mapping.namespace_class
|
|
439
430
|
if rule.namespace_param != :blank && ns_class &&
|
|
440
431
|
ns_class.attribute_form_default == :qualified
|
|
441
|
-
|
|
442
|
-
|
|
432
|
+
row_name = local_attr_name(raw_name)
|
|
433
|
+
attr_local_claims[row_name][:ns] += 1
|
|
434
|
+
plan_attrs << { name: row_name,
|
|
443
435
|
ns: { exact: ns_class.uri.to_s,
|
|
444
436
|
prefix: ns_class.prefix_default&.to_s } }
|
|
445
|
-
ns_attr_names <<
|
|
437
|
+
ns_attr_names << row_name
|
|
438
|
+
attr_rows << [rule, attr, row_name]
|
|
446
439
|
return nil
|
|
447
440
|
end
|
|
448
441
|
|
|
449
|
-
|
|
450
|
-
plan_attrs << { name:
|
|
442
|
+
attr_local_claims[raw_name][:plain] += 1
|
|
443
|
+
plan_attrs << { name: raw_name }
|
|
444
|
+
attr_rows << [rule, attr, raw_name]
|
|
451
445
|
return nil
|
|
452
446
|
end
|
|
453
447
|
return :attr_type_ns_symbol if type_ns.is_a?(Symbol)
|
|
454
|
-
return :attr_type_ns_aliases unless
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
448
|
+
return :attr_type_ns_aliases unless type_ns.all_uris.size == 1
|
|
449
|
+
|
|
450
|
+
# Exact-URI rows match the engine's (URI, local) capture by
|
|
451
|
+
# the LOCAL name — a prefixed rule spelling (xlink:href)
|
|
452
|
+
# binds its local part under the row's URI. Plain rows keep
|
|
453
|
+
# the raw name (matches-any-qualification capture). The
|
|
454
|
+
# claims tally keys by the effective match key so an exact
|
|
455
|
+
# row and a plain row claiming the same local name still
|
|
456
|
+
# opt the model out (#758).
|
|
457
|
+
row_name = local_attr_name(raw_name)
|
|
458
|
+
attr_local_claims[row_name][:ns] += 1
|
|
459
|
+
plan_attrs << { name: row_name,
|
|
459
460
|
ns: { exact: type_ns.uri.to_s,
|
|
460
461
|
prefix: type_ns.prefix_default&.to_s } }
|
|
461
|
-
ns_attr_names <<
|
|
462
|
+
ns_attr_names << row_name
|
|
463
|
+
attr_rows << [rule, attr, row_name]
|
|
462
464
|
nil
|
|
463
465
|
end
|
|
464
466
|
|
|
467
|
+
def local_attr_name(name)
|
|
468
|
+
name.include?(":") ? name.split(":", 2).last : name
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Names claimed by more than one element row (engine-row
|
|
472
|
+
# view; content and wildcard pseudo-rows are exempt). Returns
|
|
473
|
+
# the contested name list, or :unsafe when any claimant row
|
|
474
|
+
# cannot dispatch per node — the model then compiles to nil
|
|
475
|
+
# and the interpretive matcher owns the dispatch.
|
|
476
|
+
def contested_element_names(rows, compiled)
|
|
477
|
+
contested = []
|
|
478
|
+
pseudo_row_kinds = %i[content wildcard].freeze
|
|
479
|
+
rows.group_by { |r| r[:name] }.each do |name, group|
|
|
480
|
+
next if group.length < 2 || name.nil?
|
|
481
|
+
next if group.any? { |r| pseudo_row_kinds.include?(r[:kind]) }
|
|
482
|
+
# when_attribute partitions route exclusively by engine
|
|
483
|
+
# predicate (leptris 1.9.221+ #1272) — same-name partition
|
|
484
|
+
# groups are engine-managed, not cross-echoed.
|
|
485
|
+
next if group.any? { |r| r[:when] }
|
|
486
|
+
|
|
487
|
+
kinds = compiled.filter_map do |rule, _attr, kind, _spelling, _del|
|
|
488
|
+
next if rule.nil?
|
|
489
|
+
|
|
490
|
+
names = rule.name.is_a?(Array) ? rule.name.map(&:to_s) : [rule.name.to_s]
|
|
491
|
+
kind if names.include?(name)
|
|
492
|
+
end
|
|
493
|
+
return :unsafe unless kinds.all? do |k|
|
|
494
|
+
PER_NODE_ROW_KINDS.include?(k)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
contested << name
|
|
498
|
+
end
|
|
499
|
+
contested
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Row entries for a serializable child. Three shapes: # - nil child (a cyclic graph, or a shape that opted out):
|
|
503
|
+
# :child_deferred — the ROW defers per-node instead of
|
|
504
|
+
# opting the whole model out; the buckets chain delivers
|
|
505
|
+
# source nodes and the interpretive cast runs the full
|
|
506
|
+
# machinery on each island. (The whole-model bail here
|
|
507
|
+
# cascaded child_uncompilable up entire trees — uniword's
|
|
508
|
+
# Run → pict cycle nilled DocumentRoot.)
|
|
509
|
+
# - ordered child: :ordered_deferred — the per-node plan
|
|
510
|
+
# walk + element_order rebuild (PlanOrder) carry the
|
|
511
|
+
# namespace context in the child plan's own exact-URI
|
|
512
|
+
# rows; nothing is lost under a namespaced parent.
|
|
513
|
+
# - otherwise: :nested with the child's tree.
|
|
514
|
+
# Returns [[compiled tuple], tree row].
|
|
515
|
+
def child_row_for(rule, attr, type, child, delegate_target,
|
|
516
|
+
model_ns)
|
|
517
|
+
if child.nil?
|
|
518
|
+
[[rule, attr, :child_deferred, nil, delegate_target, type],
|
|
519
|
+
{ name: rule.name.to_s, kind: :raw }]
|
|
520
|
+
elsif child[:ordered]
|
|
521
|
+
[[rule, attr, :ordered_deferred, nil, delegate_target, type],
|
|
522
|
+
{ name: rule.name.to_s, kind: :raw }]
|
|
523
|
+
else
|
|
524
|
+
row = { name: rule.name.to_s, kind: :nested,
|
|
525
|
+
plan: child[:tree] }
|
|
526
|
+
row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
527
|
+
[[rule, attr, :nested, nil, delegate_target, type], row]
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
465
531
|
def compilable_mapping?(mapping)
|
|
466
532
|
# Mapping's uniform interface: root_mappings defaults to
|
|
467
533
|
# false on the base (KeyValue overrides with its own).
|
|
468
|
-
# The map_any_element catch-all rides the engine's wildcard
|
|
469
|
-
# row (leptris#1552, named rows take precedence); engines
|
|
470
|
-
# without the kind keep the interpretive refusal.
|
|
471
|
-
return false if mapping.any_element_rule &&
|
|
472
|
-
!Leptris.wildcard_rows_compatible?
|
|
473
|
-
|
|
474
534
|
mapping.root_element && !mapping.root_mappings?
|
|
475
535
|
end
|
|
476
536
|
|
|
@@ -548,8 +608,7 @@ module Lutaml
|
|
|
548
608
|
type_ns = attr.type_namespace_class(register)
|
|
549
609
|
return nil if type_ns.nil? || type_ns == :inherit
|
|
550
610
|
return :bail_symbol if type_ns.is_a?(Symbol)
|
|
551
|
-
return :bail_aliases unless
|
|
552
|
-
type_ns.all_uris.size == 1
|
|
611
|
+
return :bail_aliases unless type_ns.all_uris.size == 1
|
|
553
612
|
|
|
554
613
|
form = { exact: type_ns.uri.to_s,
|
|
555
614
|
prefix: type_ns.prefix_default&.to_s }
|