lutaml-model 0.8.84 → 0.8.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/compat/opal/lutaml_model_boot.rb +6 -5
- data/lib/lutaml/key_value/transform.rb +3 -1
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +5 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/format.rb +3 -5
- data/lib/lutaml/xml/leptris/plan_compiler.rb +372 -0
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +441 -0
- data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
- data/lib/lutaml/xml/leptris/plan_serializer.rb +273 -0
- data/lib/lutaml/xml/leptris/plan_walk.rb +32 -0
- data/lib/lutaml/xml/leptris.rb +33 -0
- data/lib/lutaml/xml/mapping.rb +47 -3
- data/lib/lutaml/xml/model_transform.rb +29 -2
- data/lib/lutaml/xml/parse_session.rb +4 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +1 -1
- data/lib/tasks/performance_probe_plan_walk.rb +1 -1
- data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
- data/spec/lutaml/model/store_spec.rb +6 -2
- data/spec/lutaml/xml/plan_fast_path_spec.rb +8 -8
- data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
- data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
- metadata +8 -6
- data/lib/lutaml/xml/plan_compiler.rb +0 -362
- data/lib/lutaml/xml/plan_hydrator.rb +0 -464
- data/lib/lutaml/xml/plan_order.rb +0 -53
- data/lib/lutaml/xml/plan_serializer.rb +0 -271
- data/lib/lutaml/xml/plan_walk.rb +0 -30
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# Serialize-side mirror of the plan fast path: builds the output
|
|
7
|
+
# tree directly through leptris's fused create_child chain — no
|
|
8
|
+
# XmlElement tree, no DeclarationPlanner pass, no builder context
|
|
9
|
+
# stack, no moxml wrapper minting. Serialized shapes must be the
|
|
10
|
+
# plain ones (attributes, scalars, collections, nested models,
|
|
11
|
+
# raw fragments, content runs); custom-method, polymorphic,
|
|
12
|
+
# union, and multi-spelling rows keep the interpretive serializer
|
|
13
|
+
# (their output flows through hash-shaped custom APIs).
|
|
14
|
+
#
|
|
15
|
+
# Ordered/mixed models serialize from element_order when the
|
|
16
|
+
# instance carries one (parsed models): text runs, comments, and
|
|
17
|
+
# element entries in document order, mirroring the interpretive
|
|
18
|
+
# order applier — including its content-attr-first text reads
|
|
19
|
+
# (mutations to the model show up) and CDATA flattening. Models
|
|
20
|
+
# without element_order fall back interpretively (return nil).
|
|
21
|
+
module PlanSerializer
|
|
22
|
+
SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
|
|
23
|
+
nested ordered_deferred raw content
|
|
24
|
+
content_deferred].freeze
|
|
25
|
+
CONTENT_KINDS = %i[content content_deferred].freeze
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# plan: the compiler's entry for instance's class
|
|
29
|
+
def call(instance, plan, register = nil)
|
|
30
|
+
# The child model's declared lutaml_default_register takes
|
|
31
|
+
# precedence over the ambient register — the resolve_for_child
|
|
32
|
+
# contract (#876); without it, child plans and attribute
|
|
33
|
+
# serialization resolve in the parent context.
|
|
34
|
+
register = Lutaml::Model::Register.resolve_for_child(
|
|
35
|
+
instance.class,
|
|
36
|
+
register || Lutaml::Model::Config.default_register,
|
|
37
|
+
)
|
|
38
|
+
return nil if plan[:ordered] && instance.element_order.nil?
|
|
39
|
+
|
|
40
|
+
doc = ::Leptris::XML::Document.create
|
|
41
|
+
root = doc.create_element(plan[:tree][:name])
|
|
42
|
+
doc.root = root
|
|
43
|
+
if plan[:ordered]
|
|
44
|
+
build_ordered(root, instance, plan, doc, register)
|
|
45
|
+
else
|
|
46
|
+
build(root, instance, plan, doc, register)
|
|
47
|
+
end
|
|
48
|
+
doc.to_xml(indent: 2, no_decl: true)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Whether the compiled plan is serialize-shaped. Ordered/mixed
|
|
52
|
+
# models serialize through build_ordered from element_order;
|
|
53
|
+
# plain models through plan rows.
|
|
54
|
+
def serializable?(plan)
|
|
55
|
+
plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
|
|
56
|
+
SERIALIZABLE_KINDS.include?(kind)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def register
|
|
63
|
+
Lutaml::Model::Config.default_register
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def build(element, instance, plan, doc, reg = nil)
|
|
67
|
+
reg ||= register
|
|
68
|
+
write_attributes(element, instance, plan, reg)
|
|
69
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
70
|
+
value = value_of(instance, attr, delegate)
|
|
71
|
+
next unless rule.render?(value, instance)
|
|
72
|
+
|
|
73
|
+
case kind
|
|
74
|
+
when :scalar
|
|
75
|
+
# Multi-capture parity: an attribute that collected
|
|
76
|
+
# several occurrences serializes one element per item,
|
|
77
|
+
# as the interpretive writer does. A plain scalar skips
|
|
78
|
+
# the Array() wrap — the common case.
|
|
79
|
+
if value.is_a?(::Array)
|
|
80
|
+
value.each do |item|
|
|
81
|
+
add_leaf(element, row_name(rule, spelling),
|
|
82
|
+
attr.serialize(item, :xml, reg), doc,
|
|
83
|
+
attrs: rule.when_attribute)
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
add_leaf(element, row_name(rule, spelling),
|
|
87
|
+
attr.serialize(value, :xml, reg), doc,
|
|
88
|
+
attrs: rule.when_attribute)
|
|
89
|
+
end
|
|
90
|
+
when :collection_native, :collection_cb
|
|
91
|
+
Array(value).each do |item|
|
|
92
|
+
add_leaf(element, rule.name.to_s,
|
|
93
|
+
attr.serialize(item, :xml, reg), doc,
|
|
94
|
+
attrs: rule.when_attribute)
|
|
95
|
+
end
|
|
96
|
+
when :nested, :ordered_deferred
|
|
97
|
+
Array(value).each do |item|
|
|
98
|
+
child_plan = PlanCompiler.compile(item.class, reg)
|
|
99
|
+
child = element.create_child(child_plan[:tree][:name])
|
|
100
|
+
if child_plan[:ordered]
|
|
101
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
102
|
+
doc, reg)
|
|
103
|
+
|
|
104
|
+
else
|
|
105
|
+
build(child, item, child_plan, doc, reg)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
when :raw
|
|
109
|
+
Array(value).each { |raw| element.add_child(raw.to_s) }
|
|
110
|
+
when :content, :content_deferred
|
|
111
|
+
Array(value).each { |run| element.add_child(doc.create_text_node(run.to_s)) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Element_order-driven emission (the interpretive order
|
|
117
|
+
# applier's contract): text runs and comments verbatim, each
|
|
118
|
+
# element entry consuming the next item of its collection
|
|
119
|
+
# (document order). Text reads prefer the content attribute
|
|
120
|
+
# when it lines up with the text-node count, so mutations to
|
|
121
|
+
# the model are reflected; element_order text is the fallback.
|
|
122
|
+
# PIs drop (interpretive parity). Returns nil when a nested
|
|
123
|
+
# ordered child lacks element_order (caller falls back).
|
|
124
|
+
def build_ordered(element, instance, plan, doc, reg = nil)
|
|
125
|
+
reg ||= register
|
|
126
|
+
write_attributes(element, instance, plan, reg)
|
|
127
|
+
rows_by_name = {}
|
|
128
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
129
|
+
next unless rule.name
|
|
130
|
+
|
|
131
|
+
rows_by_name[rule.name.to_s] ||= [rule, attr, kind, spelling,
|
|
132
|
+
delegate]
|
|
133
|
+
end
|
|
134
|
+
content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
|
|
135
|
+
CONTENT_KINDS.include?(kind) && rule.name.nil?
|
|
136
|
+
end
|
|
137
|
+
order = instance.element_order
|
|
138
|
+
element_indices = ::Hash.new(0)
|
|
139
|
+
content_attr = content_entry && content_entry[1]
|
|
140
|
+
content_value = content_attr &&
|
|
141
|
+
value_of(instance, content_attr, content_entry[4])
|
|
142
|
+
text_node_count = order.count { |o| o.type == "Text" }
|
|
143
|
+
use_content_index = content_value.is_a?(Array) &&
|
|
144
|
+
content_value.length == text_node_count
|
|
145
|
+
content_cdata = content_entry && content_entry[0].cdata
|
|
146
|
+
text_node_index = 0
|
|
147
|
+
|
|
148
|
+
order.each do |object|
|
|
149
|
+
case object.type
|
|
150
|
+
when "Text"
|
|
151
|
+
text = if content_attr && !content_value.nil?
|
|
152
|
+
if use_content_index
|
|
153
|
+
content_value[text_node_index].to_s
|
|
154
|
+
elsif !content_value.is_a?(Array) && text_node_count <= 1
|
|
155
|
+
content_value.to_s
|
|
156
|
+
else
|
|
157
|
+
object.text_content || object.name
|
|
158
|
+
end
|
|
159
|
+
else
|
|
160
|
+
object.text_content || object.name
|
|
161
|
+
end
|
|
162
|
+
text_node_index += 1
|
|
163
|
+
next if text.nil?
|
|
164
|
+
# Mixed content keeps whitespace; ordered-only skips it
|
|
165
|
+
next if content_attr.nil? && text.strip.empty?
|
|
166
|
+
|
|
167
|
+
element.add_child(content_cdata ? doc.create_cdata(text) : doc.create_text_node(text))
|
|
168
|
+
when "Comment"
|
|
169
|
+
element.add_child(doc.create_comment(object.text_content))
|
|
170
|
+
when "Element"
|
|
171
|
+
entry = rows_by_name[object.name]
|
|
172
|
+
next unless entry
|
|
173
|
+
|
|
174
|
+
_, attr, kind, _spelling, delegate = entry
|
|
175
|
+
value = value_of(instance, attr, delegate)
|
|
176
|
+
case kind
|
|
177
|
+
when :collection_native, :collection_cb
|
|
178
|
+
index = element_indices[object.name]
|
|
179
|
+
items = Array(value)
|
|
180
|
+
next unless index < items.length
|
|
181
|
+
|
|
182
|
+
element_indices[object.name] += 1
|
|
183
|
+
add_leaf(element, object.name,
|
|
184
|
+
attr.serialize(items[index], :xml, reg), doc)
|
|
185
|
+
when :nested, :ordered_deferred
|
|
186
|
+
item = if attr.collection?
|
|
187
|
+
index = element_indices[object.name]
|
|
188
|
+
element_indices[object.name] += 1
|
|
189
|
+
Array(value)[index]
|
|
190
|
+
else
|
|
191
|
+
value
|
|
192
|
+
end
|
|
193
|
+
next unless item
|
|
194
|
+
|
|
195
|
+
child_plan = PlanCompiler.compile(item.class, reg)
|
|
196
|
+
child = element.create_child(child_plan[:tree][:name])
|
|
197
|
+
if child_plan[:ordered]
|
|
198
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
199
|
+
doc, reg)
|
|
200
|
+
else
|
|
201
|
+
build(child, item, child_plan, doc, reg)
|
|
202
|
+
end
|
|
203
|
+
when :raw
|
|
204
|
+
element.add_child(value.to_s) unless value.nil?
|
|
205
|
+
when :scalar
|
|
206
|
+
add_leaf(element, object.name,
|
|
207
|
+
attr.serialize(value, :xml, reg), doc)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Attributes in document order when the instance recorded it
|
|
215
|
+
# (parsed models), else plan row order.
|
|
216
|
+
def write_attributes(element, instance, plan, _reg = nil)
|
|
217
|
+
recorded = instance.attribute_order
|
|
218
|
+
if recorded && !recorded.empty?
|
|
219
|
+
by_name = {}
|
|
220
|
+
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
221
|
+
by_name[rule.name.to_s] = [rule, attr, delegate]
|
|
222
|
+
end
|
|
223
|
+
names = recorded |
|
|
224
|
+
plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
|
|
225
|
+
names.each do |name|
|
|
226
|
+
write_attribute(element, instance, name, by_name[name])
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
230
|
+
write_attribute(element, instance, rule.name.to_s,
|
|
231
|
+
[rule, attr, delegate])
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def write_attribute(element, instance, name, entry, reg = nil)
|
|
237
|
+
return unless entry
|
|
238
|
+
|
|
239
|
+
_rule, attr, delegate = entry
|
|
240
|
+
value = value_of(instance, attr, delegate)
|
|
241
|
+
return if value.nil?
|
|
242
|
+
|
|
243
|
+
element.set_attribute(name,
|
|
244
|
+
attr.serialize(value, :xml, reg || register).to_s)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Partition rows (#88) re-emit their discriminator: the wire
|
|
248
|
+
# element carries the when_attribute pairs so the round trip
|
|
249
|
+
# re-routes to the same attribute on reparse.
|
|
250
|
+
def add_leaf(element, name, value, doc, attrs: nil)
|
|
251
|
+
child = element.create_child(name)
|
|
252
|
+
attrs&.each { |k, v| child[k.to_s] = v.to_s }
|
|
253
|
+
child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
|
|
254
|
+
child
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def value_of(instance, attr, delegate)
|
|
258
|
+
if delegate
|
|
259
|
+
target = instance.public_send(delegate)
|
|
260
|
+
target&.public_send(attr.name)
|
|
261
|
+
else
|
|
262
|
+
instance.public_send(attr.name)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def row_name(rule, spelling)
|
|
267
|
+
(spelling || rule.name).to_s
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
module Leptris
|
|
6
|
+
# Public consumer surface for the plan fast path: one engine
|
|
7
|
+
# parse + descriptor walk and direct model hydration, bypassing
|
|
8
|
+
# the wrapper-tree construction that dominates large-document
|
|
9
|
+
# consumer paths. The returned object is the mapped model root;
|
|
10
|
+
# collection attributes remain collections on that model.
|
|
11
|
+
module PlanWalk
|
|
12
|
+
class << self
|
|
13
|
+
# Build the plan, walk the document, and hydrate the mapped
|
|
14
|
+
# root. Returns nil when the model is not plan-compilable.
|
|
15
|
+
def call(model_class, xml, _options = {})
|
|
16
|
+
register = Lutaml::Model::Config.default_register
|
|
17
|
+
plan = PlanCompiler.compile(model_class, register)
|
|
18
|
+
return nil unless plan
|
|
19
|
+
|
|
20
|
+
document = ::Leptris::XML.parse(xml)
|
|
21
|
+
root = document.root
|
|
22
|
+
return nil unless root && root.name == plan[:tree][:name]
|
|
23
|
+
|
|
24
|
+
PlanHydrator.call(model_class, plan,
|
|
25
|
+
plan[:descriptor].walk(root),
|
|
26
|
+
node: root)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# The leptris plan fast path: model mapping to libleptris descriptor
|
|
6
|
+
# plan, native walk, direct hydration. Everything under this
|
|
7
|
+
# namespace is bound to the leptris gem; nothing above it may
|
|
8
|
+
# reference leptris types directly (the moxml adapters and the
|
|
9
|
+
# interpretive path never enter here).
|
|
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
|
+
def self.plan_path_compatible?
|
|
20
|
+
return false unless defined?(Gem)
|
|
21
|
+
|
|
22
|
+
spec = Gem.loaded_specs["leptris"]
|
|
23
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
|
|
27
|
+
autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
|
|
28
|
+
autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
|
|
29
|
+
autoload :PlanSerializer, "lutaml/xml/leptris/plan_serializer"
|
|
30
|
+
autoload :PlanWalk, "lutaml/xml/leptris/plan_walk"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -59,6 +59,7 @@ module Lutaml
|
|
|
59
59
|
|
|
60
60
|
@elements = {}
|
|
61
61
|
@attributes = {}
|
|
62
|
+
@any_element_rule = nil
|
|
62
63
|
@element_sequence = []
|
|
63
64
|
@content_mapping = nil
|
|
64
65
|
@raw_mapping = nil
|
|
@@ -504,6 +505,26 @@ module Lutaml
|
|
|
504
505
|
# @return [String, nil] the type name
|
|
505
506
|
alias xsd_type type_name
|
|
506
507
|
|
|
508
|
+
# Catch-all element mapping: every child element with no
|
|
509
|
+
# explicit rule routes to one collection attribute. For
|
|
510
|
+
# parse-side consumption of dynamic vocabularies (semantic__
|
|
511
|
+
# trees); element_order preserves the original tag names.
|
|
512
|
+
# Serialization of a catch-all attribute emits the attribute's
|
|
513
|
+
# declared element name (not per-item originals).
|
|
514
|
+
def map_any_element(to:, render_nil: false, render_default: false)
|
|
515
|
+
validate!(nil, to, {}, render_nil, false, type: TYPES[:element])
|
|
516
|
+
@any_element_rule = MappingRule.new(
|
|
517
|
+
nil,
|
|
518
|
+
to: to,
|
|
519
|
+
render_nil: render_nil,
|
|
520
|
+
render_default: render_default,
|
|
521
|
+
)
|
|
522
|
+
@element_rules_index = nil if instance_variable_defined?(:@element_rules_index)
|
|
523
|
+
@any_element_rule
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
attr_reader :any_element_rule
|
|
527
|
+
|
|
507
528
|
def map_instances(to:, polymorphic: {})
|
|
508
529
|
map_element(to, to: to, polymorphic: polymorphic)
|
|
509
530
|
end
|
|
@@ -1342,9 +1363,9 @@ module Lutaml
|
|
|
1342
1363
|
elsif ["text", "#cdata-section"].include?(name.to_s) && type == "Text"
|
|
1343
1364
|
content_mapping
|
|
1344
1365
|
else
|
|
1345
|
-
candidates =
|
|
1346
|
-
|
|
1347
|
-
|
|
1366
|
+
candidates = element_rules_index[name.to_s]
|
|
1367
|
+
return @any_element_rule if candidates.nil? && @any_element_rule
|
|
1368
|
+
return nil if candidates.nil?
|
|
1348
1369
|
return candidates.first if namespace_uri.nil? || candidates.one?
|
|
1349
1370
|
|
|
1350
1371
|
candidates.find do |r|
|
|
@@ -1353,6 +1374,29 @@ module Lutaml
|
|
|
1353
1374
|
end
|
|
1354
1375
|
end
|
|
1355
1376
|
|
|
1377
|
+
# O(1) name -> rules lookup. Parsing consults this once per
|
|
1378
|
+
# child element; the linear scan over every rule made large
|
|
1379
|
+
# documents quadratic (155s on a 7MB vocabulary tree). Keyed on
|
|
1380
|
+
# the stable source hashes (@elements/@attributes mutate only at
|
|
1381
|
+
# DSL time), never on the per-call mappings array.
|
|
1382
|
+
def element_rules_index
|
|
1383
|
+
cache_key = [@elements.object_id, @attributes.object_id]
|
|
1384
|
+
cached = (@element_rules_index ||= {})
|
|
1385
|
+
idx = cached[cache_key]
|
|
1386
|
+
return idx if idx
|
|
1387
|
+
|
|
1388
|
+
idx = {}
|
|
1389
|
+
([content_mapping, raw_mapping].compact + @elements.values +
|
|
1390
|
+
@attributes.values).flatten.each do |rule|
|
|
1391
|
+
next if rule.is_a?(::Hash)
|
|
1392
|
+
|
|
1393
|
+
(idx[rule.name.to_s] ||= []) << rule
|
|
1394
|
+
end
|
|
1395
|
+
cached[cache_key] = idx
|
|
1396
|
+
idx
|
|
1397
|
+
end
|
|
1398
|
+
private :element_rules_index
|
|
1399
|
+
|
|
1356
1400
|
def find_by_to(to)
|
|
1357
1401
|
mappings.detect { |rule| rule.to.to_s == to.to_s }
|
|
1358
1402
|
end
|
|
@@ -263,6 +263,9 @@ module Lutaml
|
|
|
263
263
|
# Performance: Resolve xml_mapping once — used for both rules and metadata
|
|
264
264
|
xml_mapping = mappings_for(:xml, effective_register)
|
|
265
265
|
mappings = options[:mappings] || xml_mapping.mappings(effective_register)
|
|
266
|
+
# Catch-all rule (map_any_element): iterated like any other,
|
|
267
|
+
# claiming every child no explicit rule matches.
|
|
268
|
+
mappings += [xml_mapping.any_element_rule].compact if options[:mappings].nil?
|
|
266
269
|
default_namespace = options[:default_namespace]
|
|
267
270
|
ordered_option = options[:ordered]
|
|
268
271
|
mixed_content_option = options[:mixed_content]
|
|
@@ -361,7 +364,25 @@ module Lutaml
|
|
|
361
364
|
options
|
|
362
365
|
end
|
|
363
366
|
|
|
364
|
-
value = if rule.
|
|
367
|
+
value = if !rule.attribute? && rule.name.nil? &&
|
|
368
|
+
xml_mapping.any_element_rule.equal?(rule)
|
|
369
|
+
# map_any_element: every child element no explicit
|
|
370
|
+
# element rule claims, in document order. A name-less
|
|
371
|
+
# MappingRule is otherwise indistinguishable from a
|
|
372
|
+
# content rule (the dispatch consumed it as doc.text);
|
|
373
|
+
# name-derived lookup also cannot express this — it
|
|
374
|
+
# re-finds by name, losing order and double-picking.
|
|
375
|
+
# The pass walks the document children once; the
|
|
376
|
+
# normalize/cast pipeline below handles the rest.
|
|
377
|
+
claimed = Set.new(
|
|
378
|
+
xml_mapping.mappings(effective_register)
|
|
379
|
+
.select { |r| !r.attribute? && r.name }
|
|
380
|
+
.flat_map { |r| [r.name.to_s] },
|
|
381
|
+
)
|
|
382
|
+
doc.element_children.reject do |child|
|
|
383
|
+
claimed.include?(child.unprefixed_name)
|
|
384
|
+
end
|
|
385
|
+
elsif rule.raw_mapping?
|
|
365
386
|
scoped_raw_inner_xml(doc, xml_mapping)
|
|
366
387
|
elsif rule.content_mapping?
|
|
367
388
|
rule.cdata ? doc.cdata : doc.text
|
|
@@ -369,6 +390,8 @@ module Lutaml
|
|
|
369
390
|
group.size > 1 && attr&.collection?
|
|
370
391
|
# First rule of a #765 group: match every spelling of
|
|
371
392
|
# the group so the children merge in document order.
|
|
393
|
+
raise "GROUPED-BODY" if ENV["CATCHDBG"] && rule.name.nil?
|
|
394
|
+
|
|
372
395
|
if child_names_set &&
|
|
373
396
|
group.drop(1).none? do |s|
|
|
374
397
|
child_matches_rule?(s, child_names_set,
|
|
@@ -385,9 +408,11 @@ module Lutaml
|
|
|
385
408
|
value_for_rule(session, rule, new_opts, attr,
|
|
386
409
|
extra_names)
|
|
387
410
|
end
|
|
388
|
-
elsif child_names_set && !rule.attribute? &&
|
|
411
|
+
elsif child_names_set && !rule.attribute? && rule.name &&
|
|
389
412
|
!child_matches_rule?(rule, child_names_set,
|
|
390
413
|
default_namespace)
|
|
414
|
+
raise "FASTSKIP-BODY" if ENV["CATCHDBG"] && rule.name.nil?
|
|
415
|
+
|
|
391
416
|
# Pre-match: no child element matches this rule.
|
|
392
417
|
# Skip expensive value_for_rule, handle defaults inline.
|
|
393
418
|
# NOTE: do not "read the instance" here (to_value_for) —
|
|
@@ -400,6 +425,8 @@ module Lutaml
|
|
|
400
425
|
::Lutaml::Model::UninitializedClass.instance
|
|
401
426
|
end
|
|
402
427
|
else
|
|
428
|
+
raise "ELSE-BODY" if ENV["CATCHDBG"] && rule.name.nil?
|
|
429
|
+
|
|
403
430
|
# Performance: Pass cached attr to avoid recomputing attribute_for_rule
|
|
404
431
|
val = value_for_rule(session, rule, new_opts, attr)
|
|
405
432
|
|
|
@@ -24,6 +24,10 @@ module Lutaml
|
|
|
24
24
|
@instance_is_serialize ||= instance.is_a?(::Lutaml::Model::Serialize)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def mapping
|
|
28
|
+
@mapping ||= instance.class.mappings_for(:xml, register)
|
|
29
|
+
end
|
|
30
|
+
|
|
27
31
|
# (element, rule name) -> resolved attribute name / matched
|
|
28
32
|
# attribute VALUE for the lenient lookup paths. Both are pure
|
|
29
33
|
# functions of the element and the name; ISO-13849-shaped
|
|
@@ -63,7 +63,11 @@ module Lutaml
|
|
|
63
63
|
#
|
|
64
64
|
# @return [Array<String>]
|
|
65
65
|
def xml_schema_paths
|
|
66
|
-
|
|
66
|
+
# Type check, not a probe: every Serializable carries the
|
|
67
|
+
# class method; a plain (non-Serializable) superclass ends
|
|
68
|
+
# the chain.
|
|
69
|
+
inherited = if superclass.is_a?(Class) &&
|
|
70
|
+
superclass.include?(Lutaml::Model::Serialize)
|
|
67
71
|
superclass.xml_schema_paths
|
|
68
72
|
else
|
|
69
73
|
[]
|
|
@@ -428,7 +428,7 @@ text_node_count = 0, use_content_index = false)
|
|
|
428
428
|
# reconciliation could not place
|
|
429
429
|
def apply_remaining_rules(model_instance, options, compiled_rules,
|
|
430
430
|
mapping, processed_text_nodes, fallback_rules)
|
|
431
|
-
attr_order = model_instance.
|
|
431
|
+
attr_order = model_instance.is_a?(Lutaml::Model::Serialize) &&
|
|
432
432
|
model_instance.attribute_order
|
|
433
433
|
|
|
434
434
|
rules_to_apply = if attr_order
|
|
@@ -47,7 +47,7 @@ end
|
|
|
47
47
|
|
|
48
48
|
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
49
49
|
measure.call("PlanWalk parse + 5000 model.new") do
|
|
50
|
-
result = Lutaml::Xml::PlanWalk.call(PlanWalkProbe::BulkRoot, xml)
|
|
50
|
+
result = Lutaml::Xml::Leptris::PlanWalk.call(PlanWalkProbe::BulkRoot, xml)
|
|
51
51
|
raise "PlanWalk returned nil" unless result&.item&.size == 5000
|
|
52
52
|
end
|
|
53
53
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
class AnyElementNode < Lutaml::Model::Serializable
|
|
6
|
+
attribute :id, :string
|
|
7
|
+
attribute :children, "AnyElementNode", collection: true
|
|
8
|
+
|
|
9
|
+
xml do
|
|
10
|
+
element "root"
|
|
11
|
+
map_element "id", to: :id
|
|
12
|
+
map_any_element to: :children
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
RSpec.describe AnyElementNode do
|
|
17
|
+
it "routes unmapped child elements to the catch-all collection" do
|
|
18
|
+
doc = described_class.from_xml(<<~XML)
|
|
19
|
+
<root>
|
|
20
|
+
<id>r-1</id>
|
|
21
|
+
<semantic__p>hello</semantic__p>
|
|
22
|
+
<semantic__title>Nested</semantic__title>
|
|
23
|
+
<semantic__p><semantic__em>deep</semantic__em></semantic__p>
|
|
24
|
+
</root>
|
|
25
|
+
XML
|
|
26
|
+
|
|
27
|
+
expect(doc.id).to eq("r-1")
|
|
28
|
+
expect(doc.children.size).to eq(3)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "keeps explicit rules ahead of the catch-all" do
|
|
32
|
+
doc = described_class.from_xml("<root><id>x</id></root>")
|
|
33
|
+
expect(doc.id).to eq("x")
|
|
34
|
+
expect(doc.children).to be_empty
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "nests catch-all children recursively" do
|
|
38
|
+
doc = described_class.from_xml("<root><semantic__p><semantic__em>deep</semantic__em></semantic__p></root>")
|
|
39
|
+
expect(doc.children.first.children.size).to eq(1)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -86,14 +86,18 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
it "does not mix indices across different classes" do
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
# Hold strong references: Store keeps WeakRefs, so unreferenced
|
|
90
|
+
# instances can be collected between registration and resolve,
|
|
91
|
+
# making this example fail only under in-suite GC pressure.
|
|
92
|
+
a = model_class.new(id: "a")
|
|
93
|
+
b = other_class.new(id: "a")
|
|
91
94
|
|
|
92
95
|
result = described_class.resolve(model_class, :id, "a")
|
|
93
96
|
expect(result).to be_a(model_class)
|
|
94
97
|
|
|
95
98
|
result2 = described_class.resolve(other_class, :id, "a")
|
|
96
99
|
expect(result2).to be_a(other_class)
|
|
100
|
+
expect([a, b]).to all(be_a(Lutaml::Model::Serializable))
|
|
97
101
|
end
|
|
98
102
|
|
|
99
103
|
it "registering class B does not iterate class A's indices" do
|
|
@@ -57,7 +57,7 @@ RSpec.describe "XML plan fast path" do
|
|
|
57
57
|
end
|
|
58
58
|
stub_const("PlanFastPath::SelfRef", node)
|
|
59
59
|
|
|
60
|
-
expect(Lutaml::Xml::PlanCompiler.compile(node, :default)).to be_nil
|
|
60
|
+
expect(Lutaml::Xml::Leptris::PlanCompiler.compile(node, :default)).to be_nil
|
|
61
61
|
|
|
62
62
|
xml = "<n>#{'<n>t</n>' * 200}</n>"
|
|
63
63
|
parsed = node.from_xml(xml)
|
|
@@ -94,7 +94,7 @@ RSpec.describe "XML plan fast path" do
|
|
|
94
94
|
|
|
95
95
|
expect(parsed.xmi_type).to eq("uml:Parameter")
|
|
96
96
|
expect(parsed.classifier_type).to eq("EAnone_void")
|
|
97
|
-
expect(Lutaml::Xml::PlanCompiler.compile(parameter, :default)).to be_nil
|
|
97
|
+
expect(Lutaml::Xml::Leptris::PlanCompiler.compile(parameter, :default)).to be_nil
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
it "is on by default and can be disabled" do
|
|
@@ -156,7 +156,7 @@ RSpec.describe "XML plan fast path" do
|
|
|
156
156
|
end
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
-
expect(Lutaml::Xml::PlanCompiler.compile(klass,
|
|
159
|
+
expect(Lutaml::Xml::Leptris::PlanCompiler.compile(klass,
|
|
160
160
|
Lutaml::Model::Config.default_register)).not_to be_nil
|
|
161
161
|
expect(klass.from_xml("<doc><note>hi</note></doc>").note).to eq("HI")
|
|
162
162
|
end
|
|
@@ -223,14 +223,14 @@ RSpec.describe "XML plan fast path" do
|
|
|
223
223
|
end
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
-
expect(Lutaml::Xml::PlanCompiler.compile(klass,
|
|
226
|
+
expect(Lutaml::Xml::Leptris::PlanCompiler.compile(klass,
|
|
227
227
|
Lutaml::Model::Config.default_register)).not_to be_nil
|
|
228
228
|
parsed = klass.from_xml("<doc><note>hi</note></doc>")
|
|
229
229
|
expect(parsed.tgt.note).to eq("hi")
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
it "routes a single collection row natively (hybrid routing)" do
|
|
233
|
-
plan = Lutaml::Xml::PlanCompiler.compile(
|
|
233
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
|
|
234
234
|
Class.new(Lutaml::Model::Serializable) do
|
|
235
235
|
attribute :xs, :string, collection: true
|
|
236
236
|
attribute :name, :string
|
|
@@ -400,7 +400,7 @@ RSpec.describe "XML plan fast path" do
|
|
|
400
400
|
map_element "para", to: :para
|
|
401
401
|
end
|
|
402
402
|
end
|
|
403
|
-
plan = Lutaml::Xml::PlanCompiler.compile(
|
|
403
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
|
|
404
404
|
doc_class, Lutaml::Model::Config.default_register
|
|
405
405
|
)
|
|
406
406
|
expect(plan).not_to be_nil
|
|
@@ -623,7 +623,7 @@ RSpec.describe "XML plan fast path" do
|
|
|
623
623
|
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
624
624
|
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
625
625
|
|
|
626
|
-
expect(Lutaml::Xml::PlanCompiler.compile(partitioned,
|
|
626
|
+
expect(Lutaml::Xml::Leptris::PlanCompiler.compile(partitioned,
|
|
627
627
|
:default)).to be_nil
|
|
628
628
|
end
|
|
629
629
|
|
|
@@ -708,7 +708,7 @@ RSpec.describe "XML plan fast path" do
|
|
|
708
708
|
stub_const("PlanFastPath::UpdateFields847", klass)
|
|
709
709
|
|
|
710
710
|
expect(klass.new(value: false).to_xml).to include("http://example.com/ord847")
|
|
711
|
-
plan = Lutaml::Xml::PlanCompiler.compile(
|
|
711
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
|
|
712
712
|
klass, Lutaml::Model::Config.default_register
|
|
713
713
|
)
|
|
714
714
|
expect(plan[:namespaced]).to be(true)
|
|
@@ -37,7 +37,7 @@ RSpec.describe "Plan fast path with when_attribute partitions" do
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
it "compiles the partitioned model with row tags" do
|
|
40
|
-
plan = Lutaml::Xml::PlanCompiler.compile(
|
|
40
|
+
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
|
|
41
41
|
model, Lutaml::Model::Config.default_register
|
|
42
42
|
)
|
|
43
43
|
expect(plan).not_to be_nil
|