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.
@@ -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
- if plan[:ordered]
50
- build_ordered(root, instance, plan, doc, register)
51
- else
52
- build(root, instance, plan, doc, register, ns_ctx)
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.each do |uri, prefix|
161
- key = uri
162
- next if key.nil? || @declared[key]
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
- @declared[key] = true
165
- root.add_namespace_definition(prefix, uri)
166
- end
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
- SERIALIZABLE_KINDS.include?(kind)
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
- child_plan = nested_plans[item.class] ||
226
- (nested_plans[item.class] = PlanCompiler.compile(item.class, reg))
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(child_plan[:tree][:name], child_ns) : child_plan[:tree][:name],
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
- next unless build_ordered(child, item, child_plan,
234
- doc, reg, ns_ctx, child_ns)
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). Returns nil when a nested
274
- # ordered child lacks element_order (caller falls back).
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, _inherited_ns = 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
- rows_by_name = {}
281
- plan[:rows].each do |rule, attr, kind, spelling, delegate|
282
- next unless rule.name
283
-
284
- rows_by_name[rule.name.to_s] ||= [rule, attr, kind, spelling,
285
- delegate]
286
- end
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 = instance.element_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 = rows_by_name[object.name]
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[object.name]
501
+ index = element_indices[dispatch_key]
332
502
  items = Array(value)
333
503
  next unless index < items.length
334
504
 
335
- element_indices[object.name] += 1
336
- add_leaf(element, object.name,
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[object.name]
341
- element_indices[object.name] += 1
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
- child = element.create_child(child_plan[:tree][:name])
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, object.name,
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
- _rule, attr, delegate = entry
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
- # leptris 1.9.334) passes raw node addresses — the pointer
435
- # face mints an FFI::Pointer wrapper per fresh node, the
436
- # last per-row Ruby-side allocation on the DOM build. Same
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
- if Leptris.text_attach_addr_compatible?
442
- status = ::Leptris::XML::FFI.leptris_element_add_child_addr(
443
- parent.c_address, node.c_address
444
- )
445
- unless status.zero?
446
- raise ::Leptris::XML::Error,
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, _options = {})
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
@@ -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
- # The plan path requires the one-crossing child snapshot
12
- # (leptris_plan_value_children_snapshot with child handles,
13
- # PlanValue#children_snapshot), which shipped in leptris
14
- # 1.9.273.0. Bundles resolving an older gem keep the
15
- # interpretive path — PlanCompiler.compile answers nil — the
16
- # existing opt-out protocol.
17
- MIN_LEPTRIS_VERSION = "1.9.273.0"
18
-
19
- # AttrPlan rows carrying an ns form (exact-URI attribute matching,
20
- # leptris#1486) shipped in libleptris 1.9.289.0. Older gems keep
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
- def self.attr_ns_rows_compatible?
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"