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
|
@@ -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)
|
|
@@ -37,6 +55,15 @@ module Lutaml
|
|
|
37
55
|
)
|
|
38
56
|
return nil if plan[:ordered] && instance.element_order.nil?
|
|
39
57
|
|
|
58
|
+
# The native lane first: purely namespace-less trees emit
|
|
59
|
+
# through the engine's result builder and writers, all-C
|
|
60
|
+
# after the ops list — 2.4x the DOM-mint loop on the
|
|
61
|
+
# audit corpus. Everything else keeps the DOM lane below
|
|
62
|
+
# (namespaced spellings are instance-dependent; ordered,
|
|
63
|
+
# wildcard, raw, and callback shapes are not
|
|
64
|
+
# ops-expressible).
|
|
65
|
+
return NativeEmission.emit(instance, plan, register) if NativeEmission.compatible?(plan)
|
|
66
|
+
|
|
40
67
|
doc = ::Leptris::XML::Document.create
|
|
41
68
|
ns_ctx = Namespaces.new(instance, plan, register)
|
|
42
69
|
return nil if ns_ctx.unspellable?
|
|
@@ -46,11 +73,25 @@ module Lutaml
|
|
|
46
73
|
)
|
|
47
74
|
doc.root = root
|
|
48
75
|
ns_ctx.declare_on(root)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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)
|
|
53
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
|
+
|
|
54
95
|
doc.to_xml(indent: 2, no_decl: true)
|
|
55
96
|
end
|
|
56
97
|
|
|
@@ -106,6 +147,38 @@ module Lutaml
|
|
|
106
147
|
@hoisted[uri] = prefix
|
|
107
148
|
@forms[uri] = { prefix: prefix, uri: uri }
|
|
108
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
|
|
109
182
|
collect(plan[:tree])
|
|
110
183
|
end
|
|
111
184
|
|
|
@@ -156,14 +229,18 @@ module Lutaml
|
|
|
156
229
|
ns_form.is_a?(Hash) ? { prefix: @hoisted[ns_form[:exact]], uri: ns_form[:exact] } : @forms[ns_form]
|
|
157
230
|
end
|
|
158
231
|
|
|
232
|
+
# Declaration order matches the interpretive writer: the
|
|
233
|
+
# prefixed namespaces sorted by prefix, the default
|
|
234
|
+
# namespace last.
|
|
159
235
|
def declare_on(root, _attr_form_prefix = nil)
|
|
160
|
-
@hoisted.
|
|
161
|
-
|
|
162
|
-
|
|
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]
|
|
163
240
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
241
|
+
@declared[key] = true
|
|
242
|
+
root.add_namespace_definition(prefix, uri)
|
|
243
|
+
end
|
|
167
244
|
end
|
|
168
245
|
end
|
|
169
246
|
|
|
@@ -172,7 +249,7 @@ module Lutaml
|
|
|
172
249
|
# plain models through plan rows.
|
|
173
250
|
def serializable?(plan)
|
|
174
251
|
plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
|
|
175
|
-
|
|
252
|
+
PLAN_SERIALIZABLE_KINDS.include?(kind)
|
|
176
253
|
end
|
|
177
254
|
end
|
|
178
255
|
|
|
@@ -222,23 +299,33 @@ module Lutaml
|
|
|
222
299
|
end
|
|
223
300
|
when :nested, :ordered_deferred
|
|
224
301
|
Array(value).each do |item|
|
|
225
|
-
|
|
226
|
-
|
|
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))
|
|
227
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.
|
|
228
309
|
child = element.create_child(
|
|
229
|
-
ns_ctx ? ns_ctx.spell(
|
|
310
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), child_ns) : row_name(rule, spelling),
|
|
230
311
|
)
|
|
231
312
|
ns_ctx&.collect(child_plan[:tree])
|
|
232
313
|
if child_plan[:ordered]
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
314
|
+
build_ordered(child, item, child_plan,
|
|
315
|
+
doc, reg, ns_ctx, child_ns) || return
|
|
236
316
|
else
|
|
237
|
-
build(child, item, child_plan, doc, reg, ns_ctx, child_ns)
|
|
317
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
238
318
|
end
|
|
239
319
|
end
|
|
240
320
|
when :raw
|
|
241
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?
|
|
242
329
|
when :content, :content_deferred
|
|
243
330
|
Array(value).each { |run| append_text(element, run.to_s, doc) }
|
|
244
331
|
end
|
|
@@ -264,30 +351,96 @@ module Lutaml
|
|
|
264
351
|
plan[:nested_plans] ||= {}
|
|
265
352
|
end
|
|
266
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
|
+
|
|
267
392
|
# Element_order-driven emission (the interpretive order
|
|
268
393
|
# applier's contract): text runs and comments verbatim, each
|
|
269
394
|
# element entry consuming the next item of its collection
|
|
270
395
|
# (document order). Text reads prefer the content attribute
|
|
271
396
|
# when it lines up with the text-node count, so mutations to
|
|
272
397
|
# the model are reflected; element_order text is the fallback.
|
|
273
|
-
# PIs drop (interpretive parity).
|
|
274
|
-
#
|
|
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.
|
|
275
401
|
def build_ordered(element, instance, plan, doc, reg = nil,
|
|
276
|
-
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
|
+
|
|
277
408
|
reg ||= register
|
|
278
409
|
ns_ctx&.collect(plan[:tree])
|
|
279
410
|
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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)
|
|
287
421
|
content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
|
|
288
422
|
CONTENT_KINDS.include?(kind) && rule.name.nil?
|
|
289
423
|
end
|
|
290
|
-
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
|
|
291
444
|
element_indices = ::Hash.new(0)
|
|
292
445
|
content_attr = content_entry && content_entry[1]
|
|
293
446
|
content_value = content_attr &&
|
|
@@ -321,50 +474,132 @@ module Lutaml
|
|
|
321
474
|
when "Comment"
|
|
322
475
|
element.add_child(doc.create_comment(object.text_content))
|
|
323
476
|
when "Element"
|
|
324
|
-
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
|
|
325
495
|
next unless entry
|
|
326
496
|
|
|
327
|
-
_, attr, kind, _spelling, delegate = entry
|
|
497
|
+
_, attr, kind, _spelling, delegate, row_ns = entry
|
|
328
498
|
value = value_of(instance, attr, delegate)
|
|
329
499
|
case kind
|
|
330
500
|
when :collection_native, :collection_cb
|
|
331
|
-
index = element_indices[
|
|
501
|
+
index = element_indices[dispatch_key]
|
|
332
502
|
items = Array(value)
|
|
333
503
|
next unless index < items.length
|
|
334
504
|
|
|
335
|
-
element_indices[
|
|
336
|
-
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,
|
|
337
508
|
attr.serialize(items[index], :xml, reg), doc)
|
|
338
509
|
when :nested, :ordered_deferred
|
|
339
510
|
item = if attr.collection?
|
|
340
|
-
index = element_indices[
|
|
341
|
-
element_indices[
|
|
511
|
+
index = element_indices[dispatch_key]
|
|
512
|
+
element_indices[dispatch_key] += 1
|
|
342
513
|
Array(value)[index]
|
|
343
514
|
else
|
|
344
515
|
value
|
|
345
516
|
end
|
|
346
517
|
next unless item
|
|
347
518
|
|
|
348
|
-
child_plan = nested_plans_for(plan)[item.class] ||
|
|
349
|
-
(nested_plans_for(plan)[item.class] = PlanCompiler.compile(item.class, reg))
|
|
350
|
-
|
|
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
|
+
)
|
|
351
525
|
if child_plan[:ordered]
|
|
352
526
|
return nil unless build_ordered(child, item, child_plan,
|
|
353
|
-
doc, reg)
|
|
527
|
+
doc, reg, ns_ctx, child_ns)
|
|
354
528
|
else
|
|
355
|
-
build(child, item, child_plan, doc, reg)
|
|
529
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
356
530
|
end
|
|
357
531
|
when :raw
|
|
358
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?
|
|
359
537
|
when :scalar
|
|
360
|
-
add_leaf(element,
|
|
538
|
+
add_leaf(element,
|
|
539
|
+
ns_ctx ? ns_ctx.spell(object.name, row_ns) : object.name,
|
|
361
540
|
attr.serialize(value, :xml, reg), doc)
|
|
362
541
|
end
|
|
363
542
|
end
|
|
364
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
|
+
|
|
365
548
|
true
|
|
366
549
|
end
|
|
367
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
|
|
596
|
+
true
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def compiled_rules_for(klass, reg)
|
|
600
|
+
klass.transformation_for(:xml, reg)&.compiled_rules
|
|
601
|
+
end
|
|
602
|
+
|
|
368
603
|
# Attributes in document order when the instance recorded it
|
|
369
604
|
# (parsed models), else plan row order.
|
|
370
605
|
def write_attributes(element, instance, plan, reg = nil,
|
|
@@ -411,9 +646,10 @@ module Lutaml
|
|
|
411
646
|
prefix = nil)
|
|
412
647
|
return unless entry
|
|
413
648
|
|
|
414
|
-
|
|
649
|
+
rule, attr, delegate = entry
|
|
415
650
|
value = value_of(instance, attr, delegate)
|
|
416
651
|
return if value.nil?
|
|
652
|
+
return unless rule.render?(value, instance)
|
|
417
653
|
|
|
418
654
|
spelled = prefix ? "#{prefix}:#{name}" : name
|
|
419
655
|
element.set_attribute(spelled,
|
|
@@ -430,27 +666,21 @@ module Lutaml
|
|
|
430
666
|
child
|
|
431
667
|
end
|
|
432
668
|
|
|
433
|
-
# Text attach: the address face (leptris#408 adoption
|
|
434
|
-
#
|
|
435
|
-
#
|
|
436
|
-
#
|
|
437
|
-
# semantics and status contract; probe-gated because a
|
|
438
|
-
# numerically newer lockstep can publish without the face.
|
|
669
|
+
# Text attach: the address face (leptris#408 adoption)
|
|
670
|
+
# passes raw node addresses — the pointer face mints an
|
|
671
|
+
# FFI::Pointer wrapper per fresh node, the last per-row
|
|
672
|
+
# Ruby-side allocation on the DOM build.
|
|
439
673
|
def append_text(parent, text, doc)
|
|
440
674
|
node = doc.create_text_node(text)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
"text attach failed (status=#{status})"
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
node
|
|
451
|
-
else
|
|
452
|
-
parent.add_child(node)
|
|
675
|
+
status = ::Leptris::XML::FFI.leptris_element_add_child_addr(
|
|
676
|
+
parent.c_address, node.c_address
|
|
677
|
+
)
|
|
678
|
+
unless status.zero?
|
|
679
|
+
raise ::Leptris::XML::Error,
|
|
680
|
+
"text attach failed (status=#{status})"
|
|
453
681
|
end
|
|
682
|
+
|
|
683
|
+
node
|
|
454
684
|
end
|
|
455
685
|
|
|
456
686
|
def value_of(instance, attr, delegate)
|
|
@@ -12,11 +12,10 @@ 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
|
|
19
|
-
return nil unless PlanCompiler.engine_walk_safe?(plan)
|
|
20
19
|
|
|
21
20
|
document = ::Leptris::XML.parse(xml)
|
|
22
21
|
root = document.root
|
|
@@ -24,7 +23,11 @@ module Lutaml
|
|
|
24
23
|
|
|
25
24
|
PlanHydrator.call(model_class, plan,
|
|
26
25
|
plan[:descriptor].walk(root),
|
|
27
|
-
node: root
|
|
26
|
+
node: root,
|
|
27
|
+
declaration_plan_mode:
|
|
28
|
+
options.fetch(
|
|
29
|
+
:import_declaration_plan, :eager
|
|
30
|
+
))
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
end
|
data/lib/lutaml/xml/leptris.rb
CHANGED
|
@@ -8,47 +8,16 @@ module Lutaml
|
|
|
8
8
|
# reference leptris types directly (the moxml adapters and the
|
|
9
9
|
# interpretive path never enter here).
|
|
10
10
|
module Leptris
|
|
11
|
-
#
|
|
12
|
-
# (
|
|
13
|
-
#
|
|
14
|
-
# 1.9.
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
# type-namespaced attributes on the interpretive path.
|
|
22
|
-
MIN_LEPTRIS_ATTR_NS_VERSION = "1.9.289.0"
|
|
23
|
-
|
|
24
|
-
# Nested child plans capture their declared attribute rows
|
|
25
|
-
# correctly again as of 1.9.313.0 (leptris#1563: the walk dropped
|
|
26
|
-
# children's attribute capture when the parent plan carried an
|
|
27
|
-
# exact-URI attribute row). Older gems keep those models
|
|
28
|
-
# interpretive.
|
|
29
|
-
MIN_LEPTRIS_NESTED_ATTR_VERSION = "1.9.313.0"
|
|
30
|
-
|
|
31
|
-
# Wildcard child rows (leptris#1552: named-rows-take-precedence
|
|
32
|
-
# two-pass walk, ns-form aware, type_tag echo) — the plan path's
|
|
33
|
-
# map_any_element catch-all. Ships with the 1.9.313 binding.
|
|
34
|
-
MIN_LEPTRIS_WILDCARD_VERSION = "1.9.313.0"
|
|
35
|
-
|
|
36
|
-
# Walk-side namespace safety (leptris#1587, closing #1585 and
|
|
37
|
-
# #1586): child-row exact-URI ns_uri is retained engine-side
|
|
38
|
-
# (deep-copied at build, freed with the plan) and plain attribute
|
|
39
|
-
# rows leniently match namespace-qualified wire attributes
|
|
40
|
-
# (exact-first-then-any, top level and nested). Both shipped in
|
|
41
|
-
# 1.9.320.0; older engines keep the affected models interpretive
|
|
42
|
-
# through PlanCompiler.engine_walk_safe?.
|
|
43
|
-
MIN_LEPTRIS_WALK_NS_SAFETY_VERSION = "1.9.320.0"
|
|
44
|
-
|
|
45
|
-
# The :unqualified child-row ns form (leptris#1560: matches only
|
|
46
|
-
# the UNWRITTEN spelling — bare or inherited-default-ns — and
|
|
47
|
-
# refuses prefixed spellings, the interpretive matcher's exact
|
|
48
|
-
# no-namespace semantic) ships in binding 1.9.323.0. Older
|
|
49
|
-
# engines keep namespace-less model rows on the ns_lenient
|
|
50
|
-
# superset (local-name matching under any qualification).
|
|
51
|
-
MIN_LEPTRIS_NS_UNQUALIFIED_VERSION = "1.9.323.0"
|
|
11
|
+
# Single capability floor (leptris 1.9.336.0): the child-handles
|
|
12
|
+
# snapshot (1.9.273), exact-URI attribute rows (#1486, 1.9.289),
|
|
13
|
+
# nested-attribute capture (#1563, 1.9.313), wildcard rows
|
|
14
|
+
# (#1552, 1.9.313), child-row ns_uri retention and qualified
|
|
15
|
+
# attribute leniency (#1585/#1586, 1.9.320), the :unqualified ns
|
|
16
|
+
# form (#1560, 1.9.323), and build_result with the address-based
|
|
17
|
+
# text attach (#408, 1.9.334/1.9.336). Bundles resolving an
|
|
18
|
+
# older gem keep the interpretive path — PlanCompiler.compile
|
|
19
|
+
# answers nil — the opt-out protocol.
|
|
20
|
+
MIN_LEPTRIS_VERSION = "1.9.336.0"
|
|
52
21
|
|
|
53
22
|
def self.plan_path_compatible?
|
|
54
23
|
return false unless defined?(Gem)
|
|
@@ -57,59 +26,7 @@ module Lutaml
|
|
|
57
26
|
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
|
|
58
27
|
end
|
|
59
28
|
|
|
60
|
-
|
|
61
|
-
return false unless defined?(Gem)
|
|
62
|
-
|
|
63
|
-
spec = Gem.loaded_specs["leptris"]
|
|
64
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_ATTR_NS_VERSION)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.nested_attr_capture_compatible?
|
|
68
|
-
return false unless defined?(Gem)
|
|
69
|
-
|
|
70
|
-
spec = Gem.loaded_specs["leptris"]
|
|
71
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_NESTED_ATTR_VERSION)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def self.wildcard_rows_compatible?
|
|
75
|
-
return false unless defined?(Gem)
|
|
76
|
-
|
|
77
|
-
spec = Gem.loaded_specs["leptris"]
|
|
78
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WILDCARD_VERSION)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Memoized: consulted per parse on the walk gate hot path, and
|
|
82
|
-
# the loaded leptris spec cannot change within a process.
|
|
83
|
-
@walk_ns_safety = nil
|
|
84
|
-
def self.walk_ns_safety_compatible?
|
|
85
|
-
return @walk_ns_safety unless @walk_ns_safety.nil?
|
|
86
|
-
return (@walk_ns_safety = false) unless defined?(Gem)
|
|
87
|
-
|
|
88
|
-
spec = Gem.loaded_specs["leptris"]
|
|
89
|
-
@walk_ns_safety =
|
|
90
|
-
!!(spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WALK_NS_SAFETY_VERSION))
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def self.ns_unqualified_rows_compatible?
|
|
94
|
-
return false unless defined?(Gem)
|
|
95
|
-
|
|
96
|
-
spec = Gem.loaded_specs["leptris"]
|
|
97
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_NS_UNQUALIFIED_VERSION)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# The address-based text attach (leptris#408, binding 1.9.334):
|
|
101
|
-
# leptris_element_add_child_addr skips the per-node FFI pointer
|
|
102
|
-
# mint. Capability-probed, not version-gated — the FFI face is
|
|
103
|
-
# attached or it is not, and a lockstep can publish without it.
|
|
104
|
-
@text_attach_addr = nil
|
|
105
|
-
def self.text_attach_addr_compatible?
|
|
106
|
-
return @text_attach_addr unless @text_attach_addr.nil?
|
|
107
|
-
|
|
108
|
-
@text_attach_addr =
|
|
109
|
-
!!(defined?(::Leptris::XML::FFI) &&
|
|
110
|
-
::Leptris::XML::FFI.respond_to?(:leptris_element_add_child_addr))
|
|
111
|
-
end
|
|
112
|
-
|
|
29
|
+
autoload :NativeEmission, "lutaml/xml/leptris/native_emission"
|
|
113
30
|
autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
|
|
114
31
|
autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
|
|
115
32
|
autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
|