lutaml-model 0.8.101 → 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 +4 -4
- data/Gemfile +6 -5
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +0 -1
- 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 +12 -69
- data/lib/lutaml/xml/leptris/plan_serializer.rb +21 -18
- data/lib/lutaml/xml/leptris/plan_walk.rb +0 -1
- data/lib/lutaml/xml/leptris.rb +11 -94
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +0 -11
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +0 -9
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d307b0cf0381619d2f8a8d239a0f0ccdd3bc869b9ece087af504adfc19c1ef1c
|
|
4
|
+
data.tar.gz: d2b28b3820c8331b45f078325d21492091cbbb15b1240d1750924599ee8ed3ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
48
|
-
# (
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
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?
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -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
|
|
60
|
-
#
|
|
61
|
-
#
|
|
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)
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
-
if model_ns.nil?
|
|
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
|
|
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
|
|
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?
|
|
@@ -430,27 +439,21 @@ module Lutaml
|
|
|
430
439
|
child
|
|
431
440
|
end
|
|
432
441
|
|
|
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.
|
|
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.
|
|
439
446
|
def append_text(parent, text, doc)
|
|
440
447
|
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)
|
|
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})"
|
|
453
454
|
end
|
|
455
|
+
|
|
456
|
+
node
|
|
454
457
|
end
|
|
455
458
|
|
|
456
459
|
def value_of(instance, attr, 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
|
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"
|
|
@@ -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.
|
|
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-
|
|
11
|
+
date: 2026-10-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -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
|