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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 02366b5cf16f9598e6eae1c391d7b21002fe9744cc4d617bf4bb9ffc785141a8
|
|
4
|
+
data.tar.gz: b3e2928f228d2aa613e506c6b07ce70a765bb33d1574e61e871fd7f6d226ee43
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '096e5a0302f14999f0f0c33c5c08455cdea460bee145d0a2cbb222d5bf19157f931a8aecd9e99a2232e176db0a7c546b62908a91f851c083aedf75a4cbfb5993'
|
|
7
|
+
data.tar.gz: 79a62c6ce918c5ea416eec0a9343a9b52b65f5f9d7e234bcc94beaba817e2658d25ba680215ffaee282a8bfc4ba3f4d4fd2900ecfe5907c36f0ff317ec293028
|
|
@@ -19,6 +19,12 @@ module Lutaml
|
|
|
19
19
|
# compilable XML models (Leptris::XML::Descriptor walk). Opt-in
|
|
20
20
|
# while the full semantics audit completes.
|
|
21
21
|
attr_accessor :xml_plan_fast_path
|
|
22
|
+
# Ordered/mixed-content models on the plan path: opt-in. The
|
|
23
|
+
# per-node ordered hydration is FASTER than the interpretive
|
|
24
|
+
# lane on time but allocates above the downstream budget gates
|
|
25
|
+
# (niso pnas ratio 1.25); it ships dark until the allocation
|
|
26
|
+
# diet lands.
|
|
27
|
+
attr_accessor :xml_plan_ordered_path
|
|
22
28
|
# #909: when false, XML deserialization skips the per-instance
|
|
23
29
|
# element_order/attribute_order summaries — read-only consumers of
|
|
24
30
|
# large documents never serialize back and should not pay for
|
|
@@ -31,6 +37,7 @@ module Lutaml
|
|
|
31
37
|
@conversion_cache = nil
|
|
32
38
|
@configured = false
|
|
33
39
|
@xml_plan_fast_path = true
|
|
40
|
+
@xml_plan_ordered_path = false
|
|
34
41
|
@element_order_tracking = true
|
|
35
42
|
end
|
|
36
43
|
|
|
@@ -84,6 +84,26 @@ module Lutaml
|
|
|
84
84
|
@__order_tracking__ ? true : false
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Whether any writer setter ran on this instance. Parsed
|
|
88
|
+
# instances start pristine — every set element value is covered
|
|
89
|
+
# by the parse-built element_order, so ordered serialization
|
|
90
|
+
# can skip order reconciliation; the first writer-set marks the
|
|
91
|
+
# instance and reconciliation engages (writer-added values sit
|
|
92
|
+
# outside the parse order).
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def mutated_after_parse?
|
|
95
|
+
@__mutated_after_parse__ ? true : false
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Construction-time reset: hydration routes every attribute
|
|
99
|
+
# through the generated writers (which mark mutations); a
|
|
100
|
+
# freshly built instance is definitionally not
|
|
101
|
+
# mutated-after-parse.
|
|
102
|
+
# @return [void]
|
|
103
|
+
def clear_mutated_after_parse!
|
|
104
|
+
@__mutated_after_parse__ = false
|
|
105
|
+
end
|
|
106
|
+
|
|
87
107
|
# Record a singular attribute mutation in element_order.
|
|
88
108
|
#
|
|
89
109
|
# No-op unless order tracking is enabled (i.e. the instance was
|
|
@@ -102,6 +122,7 @@ module Lutaml
|
|
|
102
122
|
# content for content-mapped attributes, ignored otherwise
|
|
103
123
|
# @return [Object] the passed value
|
|
104
124
|
def record_mutation(attribute_name, value = nil)
|
|
125
|
+
@__mutated_after_parse__ = true
|
|
105
126
|
return value unless @__order_tracking__
|
|
106
127
|
|
|
107
128
|
track_order(attribute_name, value, nil)
|
|
@@ -122,6 +143,7 @@ module Lutaml
|
|
|
122
143
|
# @param value [Object, nil] The value assigned to the collection
|
|
123
144
|
# @return [Object] the passed value
|
|
124
145
|
def record_mutation_collection(attribute_name, value)
|
|
146
|
+
@__mutated_after_parse__ = true
|
|
125
147
|
return value unless @__order_tracking__
|
|
126
148
|
return value if value.nil? || Lutaml::Model::Utils.uninitialized?(value)
|
|
127
149
|
|
|
@@ -145,7 +145,11 @@ module Lutaml
|
|
|
145
145
|
|
|
146
146
|
Lutaml::Xml::Leptris::PlanHydrator.call(self, plan,
|
|
147
147
|
plan[:descriptor].walk(root),
|
|
148
|
-
node: root
|
|
148
|
+
node: root,
|
|
149
|
+
declaration_plan_mode:
|
|
150
|
+
options.fetch(
|
|
151
|
+
:import_declaration_plan, :eager
|
|
152
|
+
))
|
|
149
153
|
rescue ::Leptris::XML::ParseError => e
|
|
150
154
|
raise Lutaml::Model::InvalidFormatError.new(:xml, e.message)
|
|
151
155
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -97,8 +97,8 @@ module Lutaml
|
|
|
97
97
|
next unless child_rel
|
|
98
98
|
|
|
99
99
|
Array(value).each do |item|
|
|
100
|
-
child_plan = nested_plans_for(plan)[item.class] ||
|
|
101
|
-
(nested_plans_for(plan)[item.class] =
|
|
100
|
+
child_plan = nested_plans_for(plan)[[item.class, rule.name.to_s]] ||
|
|
101
|
+
(nested_plans_for(plan)[[item.class, rule.name.to_s]] =
|
|
102
102
|
PlanCompiler.compile(item.class, register))
|
|
103
103
|
ops << { kind: :element, plan_index: my_index,
|
|
104
104
|
row_index: row_index }
|
|
@@ -53,6 +53,10 @@ 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)
|
|
@@ -104,9 +108,23 @@ module Lutaml
|
|
|
104
108
|
|
|
105
109
|
mapping = model_class.mappings_for(:xml, register)
|
|
106
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?
|
|
107
125
|
|
|
108
126
|
rows = []
|
|
109
|
-
attr_rows = [] # [[rule, attr]] hydration metadata
|
|
127
|
+
attr_rows = [] # [[rule, attr, row_name]] hydration metadata
|
|
110
128
|
plan_attrs = [] # [{name:, kind:}] rows for the engine plan
|
|
111
129
|
compiled = [] # [rule, attr, kind, spelling, delegate_target]
|
|
112
130
|
ns_attr_names = [] # exact-URI attribute rows (ambiguity guard)
|
|
@@ -202,28 +220,26 @@ module Lutaml
|
|
|
202
220
|
type = attr.type(register)
|
|
203
221
|
if serializable_type?(type)
|
|
204
222
|
child = compile(type, register)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
delegate_target]
|
|
218
|
-
rows << { name: rule.name.to_s, kind: :raw }
|
|
219
|
-
else
|
|
220
|
-
compiled << [rule, attr, :nested, nil, delegate_target]
|
|
221
|
-
needs_nodes ||= child[:needs_nodes]
|
|
222
|
-
nested_row = { name: rule.name.to_s, kind: :nested,
|
|
223
|
-
plan: child[:tree] }
|
|
224
|
-
nested_row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
225
|
-
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
|
|
226
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
|
|
227
243
|
elsif rule.multiple_mappings?
|
|
228
244
|
rule.name.each do |spelling|
|
|
229
245
|
compiled << [rule, attr, :spelling, spelling.to_s,
|
|
@@ -271,6 +287,19 @@ module Lutaml
|
|
|
271
287
|
|
|
272
288
|
row_tags = partition_row_tags!(compiled, rows, tag)
|
|
273
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
|
+
|
|
274
303
|
# The plan serializer does not emit namespace declarations —
|
|
275
304
|
# namespaced models serialize through the interpretive
|
|
276
305
|
# writer (lutaml-model#847: standalone to_xml under leptris
|
|
@@ -354,6 +383,7 @@ module Lutaml
|
|
|
354
383
|
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
355
384
|
needs_nodes: needs_nodes,
|
|
356
385
|
collection_defaults: collection_defaults,
|
|
386
|
+
contested_names: contested.empty? ? nil : contested,
|
|
357
387
|
ns_attr_names: ns_attr_names.empty? ? nil : ns_attr_names,
|
|
358
388
|
wildcard_tag: (wildcard_tag if defined?(wildcard_tag)) }
|
|
359
389
|
end
|
|
@@ -375,9 +405,19 @@ module Lutaml
|
|
|
375
405
|
attr_local_claims)
|
|
376
406
|
return :non_scalar unless scalar_type?(attr, register)
|
|
377
407
|
|
|
378
|
-
|
|
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
|
|
379
419
|
type_ns = attr.type_namespace_class(register)
|
|
380
|
-
|
|
420
|
+
raw_name
|
|
381
421
|
if type_ns.nil?
|
|
382
422
|
# attribute_form_default :qualified on the model's
|
|
383
423
|
# namespace qualifies locally-declared attributes under
|
|
@@ -389,29 +429,105 @@ module Lutaml
|
|
|
389
429
|
ns_class = mapping.namespace_class
|
|
390
430
|
if rule.namespace_param != :blank && ns_class &&
|
|
391
431
|
ns_class.attribute_form_default == :qualified
|
|
392
|
-
|
|
393
|
-
|
|
432
|
+
row_name = local_attr_name(raw_name)
|
|
433
|
+
attr_local_claims[row_name][:ns] += 1
|
|
434
|
+
plan_attrs << { name: row_name,
|
|
394
435
|
ns: { exact: ns_class.uri.to_s,
|
|
395
436
|
prefix: ns_class.prefix_default&.to_s } }
|
|
396
|
-
ns_attr_names <<
|
|
437
|
+
ns_attr_names << row_name
|
|
438
|
+
attr_rows << [rule, attr, row_name]
|
|
397
439
|
return nil
|
|
398
440
|
end
|
|
399
441
|
|
|
400
|
-
|
|
401
|
-
plan_attrs << { name:
|
|
442
|
+
attr_local_claims[raw_name][:plain] += 1
|
|
443
|
+
plan_attrs << { name: raw_name }
|
|
444
|
+
attr_rows << [rule, attr, raw_name]
|
|
402
445
|
return nil
|
|
403
446
|
end
|
|
404
447
|
return :attr_type_ns_symbol if type_ns.is_a?(Symbol)
|
|
405
448
|
return :attr_type_ns_aliases unless type_ns.all_uris.size == 1
|
|
406
449
|
|
|
407
|
-
|
|
408
|
-
|
|
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,
|
|
409
460
|
ns: { exact: type_ns.uri.to_s,
|
|
410
461
|
prefix: type_ns.prefix_default&.to_s } }
|
|
411
|
-
ns_attr_names <<
|
|
462
|
+
ns_attr_names << row_name
|
|
463
|
+
attr_rows << [rule, attr, row_name]
|
|
412
464
|
nil
|
|
413
465
|
end
|
|
414
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
|
+
|
|
415
531
|
def compilable_mapping?(mapping)
|
|
416
532
|
# Mapping's uniform interface: root_mappings defaults to
|
|
417
533
|
# false on the base (KeyValue overrides with its own).
|