lutaml-model 0.8.100 → 0.8.102

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bd54a574eb02eec487d980543835abb2bec61c12104d6eea6b898a516fb301f
4
- data.tar.gz: ffc5d3ed5b0c6df7140839064baec52624a034b2d1c5cfdc764dc4ee78c199ce
3
+ metadata.gz: d307b0cf0381619d2f8a8d239a0f0ccdd3bc869b9ece087af504adfc19c1ef1c
4
+ data.tar.gz: d2b28b3820c8331b45f078325d21492091cbbb15b1240d1750924599ee8ed3ce
5
5
  SHA512:
6
- metadata.gz: 191c5ee654cc6863a6f8d865448f0fbb29c31f7d602e3ab2a356148fdca7117b254aa497bb1ebdd80da1c80f495cf46c5f1029897cf87e7423a7409747e4e97c
7
- data.tar.gz: 4b55a05f437df7e0c85c7545d2042990572c70a4d64b42d9ae3ee44950ca6875cd640c2089cd9620951264180556e2e3776e7b32028b1b1d9962cbdaad739ecb
6
+ metadata.gz: 6d631f610cf9e057a8c23f3e27945d0ac7ca15a9f9633f5e49143a693148f496a1dfb3a99857efea37653f50241a96f6069d6d03839774368c82479c965942fb
7
+ data.tar.gz: a4bc2ee7e75df18ebda941551972fb85a960503005dbeae0be453f792db53616785209068d38d43eb68e6e84ad85e2e1ac31fcf07acd41780a15adf4b571ed5b
data/Gemfile CHANGED
@@ -44,11 +44,12 @@ gem "rexml"
44
44
  # Native engines stay development-only: every adapter detects and
45
45
  # degrades gracefully at runtime (safe requires + prefer-if-available).
46
46
  # yeptris ships no Windows prebuilt platform gem.
47
- # leptris floor: the plan hydrator's one-crossing snapshot
48
- # (leptris_plan_value_children_snapshot) landed in 1.9.273.0; older
49
- # gems fall back to accessor enumeration at runtime, but CI and dev
50
- # resolve the snapshot-capable line.
51
- gem "leptris", ">= 1.9.273.0"
47
+ # leptris floor: the single capability line for the plan fast path
48
+ # (see Lutaml::Xml::Leptris::MIN_LEPTRIS_VERSION) — child snapshots,
49
+ # exact-URI attribute rows, nested-attribute capture, wildcard rows,
50
+ # walk-side ns retention and leniency, :unqualified rows, and the
51
+ # address-based text attach all ride it.
52
+ gem "leptris", ">= 1.9.336.0"
52
53
  gem "teptris"
53
54
  gem "yeptris" unless Gem.win_platform?
54
55
  # TODO: revert rng branch to main when lutaml/rng#32 is merged
@@ -382,6 +382,7 @@ require "lutaml/xml/error/namespace_mismatch_error"
382
382
  require "lutaml/xml/error/schema_validation_error"
383
383
  require "lutaml/xml/error/xml_configuration_error"
384
384
  require "lutaml/xml/error/xml_error"
385
+ require "lutaml/xml/leptris/native_emission"
385
386
  require "lutaml/xml/leptris/plan_compiler"
386
387
  require "lutaml/xml/leptris/plan_hydrator"
387
388
  require "lutaml/xml/leptris/plan_order"
@@ -138,7 +138,6 @@ module Lutaml
138
138
  register = Lutaml::Model::Config.default_register
139
139
  plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
140
140
  return nil unless plan
141
- return nil unless Lutaml::Xml::Leptris::PlanCompiler.engine_walk_safe?(plan)
142
141
 
143
142
  root = ::Leptris::XML.parse(data.to_s).root
144
143
  return nil if root.nil?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.100"
5
+ VERSION = "0.8.102"
6
6
  end
7
7
  end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ module Leptris
6
+ # The native emission lane of the plan fast path: instead of
7
+ # minting the output DOM node-by-node through the Ruby
8
+ # create_child chain, build a plan RESULT from ops
9
+ # (leptris_plan_result_build), render it through the engine's
10
+ # plan serializer, reparse the compact output, and write it with
11
+ # the engine's indented document writer. Both renderings are
12
+ # pure C; measured 2.4x faster than the DOM-mint loop on the
13
+ # 500-row audit at a modest allocation increase.
14
+ #
15
+ # Byte-parity with the DOM lane holds because both lanes end at
16
+ # the same document writer over structurally identical
17
+ # documents: the compact emission escapes exactly as the DOM
18
+ # text nodes do, and reparsing yields one text node per leaf,
19
+ # matching the minted tree. The reparse is an engine-internal
20
+ # cost, not a Ruby one.
21
+ #
22
+ # Emission is enabled for the provably static subset: trees with
23
+ # no namespace form anywhere (every spelling is the unwritten
24
+ # local name, nothing to declare, no instance-dependent
25
+ # prefix/default flips) and the non-ordered build() shapes
26
+ # (attributes, scalars, collections, nested models). Wildcard,
27
+ # raw, callback, and content rows and ordered models keep the
28
+ # DOM lane — their emission shapes are not expressible as ops
29
+ # (or not verified to be) and the DOM lane is byte-exact.
30
+ module NativeEmission
31
+ ROOT_ROW = 0xFFFF_FFFF
32
+ EMITTABLE_KINDS = %i[scalar collection nested].freeze
33
+
34
+ class << self
35
+ def compatible?(plan)
36
+ !plan[:ordered] && tree_emittable?(plan[:tree])
37
+ end
38
+
39
+ # Pre-order depth-first, children in row order — the same
40
+ # walk Descriptor.flatten_plan performs, so the plan_index
41
+ # counters here match the engine's flattened plans array.
42
+ def tree_emittable?(tree)
43
+ return false if tree[:ns]
44
+ return false if (tree[:attributes] || []).any? { |a| a[:ns].is_a?(Hash) }
45
+
46
+ (tree[:children] || []).all? do |row|
47
+ EMITTABLE_KINDS.include?(row[:kind]) &&
48
+ (row[:kind] != :nested || !row[:plan] || tree_emittable?(row[:plan]))
49
+ end
50
+ end
51
+
52
+ def emit(instance, plan, register)
53
+ ops = [{ kind: :element, row_index: ROOT_ROW }]
54
+ emit_element(ops, instance, plan, register, 0)
55
+ ops << { kind: :end }
56
+ compact = plan[:descriptor].serialize(
57
+ plan[:descriptor].build_result(ops),
58
+ )
59
+ ::Leptris::XML.parse(compact).to_xml(indent: 2, no_decl: true)
60
+ end
61
+
62
+ private
63
+
64
+ # plan_index: this plan's flattened index. The engine's
65
+ # flatten_plan pushes each nested row's child plan exactly
66
+ # once — collection members share that slot — so child flat
67
+ # indices come from a static per-plan layout (relative to
68
+ # this plan's index), never from a runtime counter.
69
+ def emit_element(ops, instance, plan, register, plan_index)
70
+ my_index = plan_index
71
+ layout = flat_layout_for(plan)
72
+
73
+ emit_attributes(ops, instance, plan, my_index, register)
74
+
75
+ plan[:rows].each_with_index do |(rule, attr, kind, _spelling, delegate), row_index|
76
+ value = value_of(instance, attr, delegate)
77
+ next unless rule.render?(value, instance)
78
+
79
+ case kind
80
+ when :scalar
81
+ # Multi-capture parity: an attribute that collected
82
+ # several occurrences serializes one element per item,
83
+ # as the DOM lane does.
84
+ if value.is_a?(::Array)
85
+ value.each do |item|
86
+ ops << scalar_op(my_index, row_index, attr, item, register)
87
+ end
88
+ else
89
+ ops << scalar_op(my_index, row_index, attr, value, register)
90
+ end
91
+ when :collection_native, :collection_cb
92
+ Array(value).each do |item|
93
+ ops << scalar_op(my_index, row_index, attr, item, register)
94
+ end
95
+ when :nested
96
+ _tree, child_rel = layout[row_index]
97
+ next unless child_rel
98
+
99
+ Array(value).each do |item|
100
+ child_plan = nested_plans_for(plan)[item.class] ||
101
+ (nested_plans_for(plan)[item.class] =
102
+ PlanCompiler.compile(item.class, register))
103
+ ops << { kind: :element, plan_index: my_index,
104
+ row_index: row_index }
105
+ emit_element(ops, item, child_plan,
106
+ register, my_index + child_rel)
107
+ ops << { kind: :end }
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # {nested row_index => [child tree, relative flat index]} —
114
+ # children of this plan occupy slots my_index+1 .. in row
115
+ # order, each nested row's subtree sized once.
116
+ def flat_layout_for(plan)
117
+ plan[:flat_layout] ||= begin
118
+ layout = {}
119
+ rel = 1
120
+ (plan[:tree][:children] || []).each_with_index do |row, i|
121
+ next unless row[:kind] == :nested && row[:plan]
122
+
123
+ layout[i] = [row[:plan], rel]
124
+ rel += subtree_size(row[:plan])
125
+ end
126
+ layout
127
+ end
128
+ end
129
+
130
+ def subtree_size(tree)
131
+ 1 + (tree[:children] || []).sum do |row|
132
+ row[:kind] == :nested && row[:plan] ? subtree_size(row[:plan]) : 0
133
+ end
134
+ end
135
+
136
+ def emit_attributes(ops, instance, plan, plan_index, register)
137
+ recorded = instance.attribute_order
138
+ attr_rows = plan[:attr_rows]
139
+ if recorded && !recorded.empty?
140
+ by_name = {}
141
+ attr_rows.each { |rule, attr, _sp, delegate| by_name[rule.name.to_s] = [rule, attr, delegate] }
142
+ names = recorded | attr_rows.map { |rule, _a, _s, _d| rule.name.to_s }
143
+ else
144
+ names = attr_rows.map { |rule, _a, _s, _d| rule.name.to_s }
145
+ by_name = {}
146
+ attr_rows.each { |rule, attr, _sp, delegate| by_name[rule.name.to_s] = [rule, attr, delegate] }
147
+ end
148
+ tree_attrs = plan[:tree][:attributes] || []
149
+ names.each do |name|
150
+ entry = by_name[name]
151
+ next unless entry
152
+
153
+ _rule, attr, delegate = entry
154
+ value = value_of(instance, attr, delegate)
155
+ next if value.nil?
156
+
157
+ row_index = tree_attrs.index { |a| a[:name] == name }
158
+ next unless row_index
159
+
160
+ ops << { kind: :attr, plan_index: plan_index,
161
+ row_index: row_index, name: name,
162
+ value: attr.serialize(value, :xml, register).to_s }
163
+ end
164
+ end
165
+
166
+ def scalar_op(plan_index, row_index, attr, value, register)
167
+ { kind: :scalar, plan_index: plan_index, row_index: row_index,
168
+ value: attr.serialize(value, :xml, register).to_s }
169
+ end
170
+
171
+ def value_of(instance, attr, delegate)
172
+ if delegate
173
+ target = instance.public_send(delegate)
174
+ target&.public_send(attr.name)
175
+ else
176
+ instance.public_send(attr.name)
177
+ end
178
+ end
179
+
180
+ def nested_plans_for(plan)
181
+ plan[:nested_plans] ||= {}
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -56,9 +56,9 @@ module Lutaml
56
56
 
57
57
  class << self
58
58
  def compile(model_class, register)
59
- # Version gate, not a capability probe: the plan path needs
60
- # leptris >= 1.9.273.0 (the child-handles snapshot). Older
61
- # gems take the interpretive path via the nil opt-out.
59
+ # Version gate: below MIN_LEPTRIS_VERSION (the single
60
+ # capability floor) gems take the interpretive path via
61
+ # the nil opt-out.
62
62
  return nil unless Leptris.plan_path_compatible?
63
63
 
64
64
  # A child model's declared lutaml_default_register takes
@@ -96,49 +96,8 @@ module Lutaml
96
96
  end
97
97
  end
98
98
 
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
99
  private
126
100
 
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
101
  def build(model_class, register)
143
102
  return nil unless model_class.is_a?(Class) &&
144
103
  model_class.include?(::Lutaml::Model::Serialize)
@@ -335,13 +294,13 @@ module Lutaml
335
294
  flags << :ns_lenient
336
295
 
337
296
  # Namespace-less model child rows bind the strict unwritten
338
- # spelling (bare, or inherited default xmlns) on engines
339
- # with the leptris#1560 :unqualified form — the interpretive
340
- # matcher's exact no-namespace semantic, refusing prefixed
341
- # spellings where the ns_lenient superset bound them.
342
- # Rows with an explicit form (rule ns, type ns, :none, :any)
343
- # are untouched; name-matched element row kinds only.
344
- if model_ns.nil? && Leptris.ns_unqualified_rows_compatible?
297
+ # spelling (bare, or inherited default xmlns) — the
298
+ # interpretive matcher's exact no-namespace semantic,
299
+ # refusing prefixed spellings where the ns_lenient
300
+ # superset bound them. Rows with an explicit form (rule ns,
301
+ # type ns, :none, :any) are untouched; name-matched element
302
+ # row kinds only.
303
+ if model_ns.nil?
345
304
  rows.each do |r|
346
305
  next if r[:ns]
347
306
 
@@ -373,14 +332,6 @@ module Lutaml
373
332
  # must receive one through the buckets chain.
374
333
  needs_nodes = true unless ns_attr_names.empty?
375
334
 
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
335
  # The catch-all row: kind :wildcard with an explicit :any ns
385
336
  # form (pad0) — the two-pass walk routes every child no
386
337
  # named row claimed here, tagged for hydrator routing.
@@ -451,8 +402,7 @@ module Lutaml
451
402
  return nil
452
403
  end
453
404
  return :attr_type_ns_symbol if type_ns.is_a?(Symbol)
454
- return :attr_type_ns_aliases unless Leptris.attr_ns_rows_compatible? &&
455
- type_ns.all_uris.size == 1
405
+ return :attr_type_ns_aliases unless type_ns.all_uris.size == 1
456
406
 
457
407
  claim[:ns] += 1
458
408
  plan_attrs << { name: rule.name.to_s,
@@ -465,12 +415,6 @@ module Lutaml
465
415
  def compilable_mapping?(mapping)
466
416
  # Mapping's uniform interface: root_mappings defaults to
467
417
  # 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
418
  mapping.root_element && !mapping.root_mappings?
475
419
  end
476
420
 
@@ -548,8 +492,7 @@ module Lutaml
548
492
  type_ns = attr.type_namespace_class(register)
549
493
  return nil if type_ns.nil? || type_ns == :inherit
550
494
  return :bail_symbol if type_ns.is_a?(Symbol)
551
- return :bail_aliases unless Leptris.attr_ns_rows_compatible? &&
552
- type_ns.all_uris.size == 1
495
+ return :bail_aliases unless type_ns.all_uris.size == 1
553
496
 
554
497
  form = { exact: type_ns.uri.to_s,
555
498
  prefix: type_ns.prefix_default&.to_s }
@@ -37,6 +37,15 @@ module Lutaml
37
37
  )
38
38
  return nil if plan[:ordered] && instance.element_order.nil?
39
39
 
40
+ # The native lane first: purely namespace-less trees emit
41
+ # through the engine's result builder and writers, all-C
42
+ # after the ops list — 2.4x the DOM-mint loop on the
43
+ # audit corpus. Everything else keeps the DOM lane below
44
+ # (namespaced spellings are instance-dependent; ordered,
45
+ # wildcard, raw, and callback shapes are not
46
+ # ops-expressible).
47
+ return NativeEmission.emit(instance, plan, register) if NativeEmission.compatible?(plan)
48
+
40
49
  doc = ::Leptris::XML::Document.create
41
50
  ns_ctx = Namespaces.new(instance, plan, register)
42
51
  return nil if ns_ctx.unspellable?
@@ -240,7 +249,7 @@ module Lutaml
240
249
  when :raw
241
250
  Array(value).each { |raw| element.add_child(raw.to_s) }
242
251
  when :content, :content_deferred
243
- Array(value).each { |run| element.add_child(doc.create_text_node(run.to_s)) }
252
+ Array(value).each { |run| append_text(element, run.to_s, doc) }
244
253
  end
245
254
  end
246
255
  end
@@ -426,10 +435,27 @@ module Lutaml
426
435
  def add_leaf(element, name, value, doc, attrs: nil)
427
436
  child = element.create_child(name)
428
437
  attrs&.each { |k, v| child[k.to_s] = v.to_s }
429
- child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
438
+ append_text(child, value.to_s, doc) unless value.nil?
430
439
  child
431
440
  end
432
441
 
442
+ # Text attach: the address face (leptris#408 adoption)
443
+ # passes raw node addresses — the pointer face mints an
444
+ # FFI::Pointer wrapper per fresh node, the last per-row
445
+ # Ruby-side allocation on the DOM build.
446
+ def append_text(parent, text, doc)
447
+ node = doc.create_text_node(text)
448
+ status = ::Leptris::XML::FFI.leptris_element_add_child_addr(
449
+ parent.c_address, node.c_address
450
+ )
451
+ unless status.zero?
452
+ raise ::Leptris::XML::Error,
453
+ "text attach failed (status=#{status})"
454
+ end
455
+
456
+ node
457
+ end
458
+
433
459
  def value_of(instance, attr, delegate)
434
460
  if delegate
435
461
  target = instance.public_send(delegate)
@@ -16,7 +16,6 @@ module Lutaml
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
@@ -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,46 +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
-
29
+ autoload :NativeEmission, "lutaml/xml/leptris/native_emission"
100
30
  autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
101
31
  autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
102
32
  autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
data/lutaml-model.gemspec CHANGED
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
34
 
35
35
  # required for liquid
36
- spec.add_dependency "base64"
36
+ spec.add_dependency "base64", ">= 0.1.1"
37
37
  spec.add_dependency "bigdecimal"
38
38
  spec.add_dependency "canon"
39
39
  spec.add_dependency "concurrent-ruby"
@@ -44,7 +44,7 @@ Gem::Specification.new do |spec|
44
44
  spec.add_dependency "moxml", "~> 0.5.84"
45
45
  spec.add_dependency "ostruct"
46
46
  spec.add_dependency "rubyzip", "~> 3.7"
47
- spec.add_dependency "thor"
47
+ spec.add_dependency "thor", "~> 1.4"
48
48
 
49
49
  spec.metadata["rubygems_mfa_required"] = "true"
50
50
  end
@@ -128,15 +128,4 @@ RSpec.describe "XML plan fast path: type-namespaced attribute rows" do
128
128
  model, Lutaml::Model::Config.default_register
129
129
  )).to be_nil
130
130
  end
131
-
132
- # Engines older than the AttrPlan ns form (libleptris 1.9.289) keep
133
- # the opt-out — the existing version-gate protocol.
134
- it "opts out when the engine predates attribute ns rows" do
135
- allow(Lutaml::Xml::Leptris).to receive(:attr_ns_rows_compatible?)
136
- .and_return(false)
137
-
138
- expect(Lutaml::Xml::Leptris::PlanCompiler.compile(
139
- klass, Lutaml::Model::Config.default_register
140
- )).to be_nil
141
- end
142
131
  end
@@ -93,13 +93,4 @@ RSpec.describe "XML plan fast path: map_any_element catch-all" do
93
93
  member_shapes(interpretive.others).map { |n| n.sub("Nokogiri", "Leptris") },
94
94
  )
95
95
  end
96
-
97
- it "compiles under engines without wildcard rows to nil" do
98
- allow(Lutaml::Xml::Leptris).to receive(:wildcard_rows_compatible?)
99
- .and_return(false)
100
-
101
- expect(Lutaml::Xml::Leptris::PlanCompiler.compile(
102
- host_class, Lutaml::Model::Config.default_register
103
- )).to be_nil
104
- end
105
96
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.100
4
+ version: 0.8.102
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-10 00:00:00.000000000 Z
11
+ date: 2026-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.1.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.1.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bigdecimal
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -132,16 +132,16 @@ dependencies:
132
132
  name: thor
133
133
  requirement: !ruby/object:Gem::Requirement
134
134
  requirements:
135
- - - ">="
135
+ - - "~>"
136
136
  - !ruby/object:Gem::Version
137
- version: '0'
137
+ version: '1.4'
138
138
  type: :runtime
139
139
  prerelease: false
140
140
  version_requirements: !ruby/object:Gem::Requirement
141
141
  requirements:
142
- - - ">="
142
+ - - "~>"
143
143
  - !ruby/object:Gem::Version
144
- version: '0'
144
+ version: '1.4'
145
145
  description: 'LutaML creating data models in Ruby
146
146
 
147
147
  '
@@ -787,6 +787,7 @@ files:
787
787
  - lib/lutaml/xml/html_entities.rb
788
788
  - lib/lutaml/xml/leptris.rb
789
789
  - lib/lutaml/xml/leptris/element.rb
790
+ - lib/lutaml/xml/leptris/native_emission.rb
790
791
  - lib/lutaml/xml/leptris/plan_compiler.rb
791
792
  - lib/lutaml/xml/leptris/plan_hydrator.rb
792
793
  - lib/lutaml/xml/leptris/plan_order.rb