lutaml-model 0.8.84 → 0.8.85
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/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/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 +7 -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
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Lutaml
|
|
4
|
-
module Xml
|
|
5
|
-
# Serialize-side mirror of the plan fast path: builds the output
|
|
6
|
-
# tree directly through leptris's fused create_child chain — no
|
|
7
|
-
# XmlElement tree, no DeclarationPlanner pass, no builder context
|
|
8
|
-
# stack, no moxml wrapper minting. Serialized shapes must be the
|
|
9
|
-
# plain ones (attributes, scalars, collections, nested models,
|
|
10
|
-
# raw fragments, content runs); custom-method, polymorphic,
|
|
11
|
-
# union, and multi-spelling rows keep the interpretive serializer
|
|
12
|
-
# (their output flows through hash-shaped custom APIs).
|
|
13
|
-
#
|
|
14
|
-
# Ordered/mixed models serialize from element_order when the
|
|
15
|
-
# instance carries one (parsed models): text runs, comments, and
|
|
16
|
-
# element entries in document order, mirroring the interpretive
|
|
17
|
-
# order applier — including its content-attr-first text reads
|
|
18
|
-
# (mutations to the model show up) and CDATA flattening. Models
|
|
19
|
-
# without element_order fall back interpretively (return nil).
|
|
20
|
-
module PlanSerializer
|
|
21
|
-
SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
|
|
22
|
-
nested ordered_deferred raw content
|
|
23
|
-
content_deferred].freeze
|
|
24
|
-
CONTENT_KINDS = %i[content content_deferred].freeze
|
|
25
|
-
|
|
26
|
-
class << self
|
|
27
|
-
# plan: the compiler's entry for instance's class
|
|
28
|
-
def call(instance, plan, register = nil)
|
|
29
|
-
# The child model's declared lutaml_default_register takes
|
|
30
|
-
# precedence over the ambient register — the resolve_for_child
|
|
31
|
-
# contract (#876); without it, child plans and attribute
|
|
32
|
-
# serialization resolve in the parent context.
|
|
33
|
-
register = Lutaml::Model::Register.resolve_for_child(
|
|
34
|
-
instance.class,
|
|
35
|
-
register || Lutaml::Model::Config.default_register,
|
|
36
|
-
)
|
|
37
|
-
return nil if plan[:ordered] && instance.element_order.nil?
|
|
38
|
-
|
|
39
|
-
doc = ::Leptris::XML::Document.create
|
|
40
|
-
root = doc.create_element(plan[:tree][:name])
|
|
41
|
-
doc.root = root
|
|
42
|
-
if plan[:ordered]
|
|
43
|
-
build_ordered(root, instance, plan, doc, register)
|
|
44
|
-
else
|
|
45
|
-
build(root, instance, plan, doc, register)
|
|
46
|
-
end
|
|
47
|
-
doc.to_xml(indent: 2, no_decl: true)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Whether the compiled plan is serialize-shaped. Ordered/mixed
|
|
51
|
-
# models serialize through build_ordered from element_order;
|
|
52
|
-
# plain models through plan rows.
|
|
53
|
-
def serializable?(plan)
|
|
54
|
-
plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
|
|
55
|
-
SERIALIZABLE_KINDS.include?(kind)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
def register
|
|
62
|
-
Lutaml::Model::Config.default_register
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def build(element, instance, plan, doc, reg = nil)
|
|
66
|
-
reg ||= register
|
|
67
|
-
write_attributes(element, instance, plan, reg)
|
|
68
|
-
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
69
|
-
value = value_of(instance, attr, delegate)
|
|
70
|
-
next unless rule.render?(value, instance)
|
|
71
|
-
|
|
72
|
-
case kind
|
|
73
|
-
when :scalar
|
|
74
|
-
# Multi-capture parity: an attribute that collected
|
|
75
|
-
# several occurrences serializes one element per item,
|
|
76
|
-
# as the interpretive writer does. A plain scalar skips
|
|
77
|
-
# the Array() wrap — the common case.
|
|
78
|
-
if value.is_a?(::Array)
|
|
79
|
-
value.each do |item|
|
|
80
|
-
add_leaf(element, row_name(rule, spelling),
|
|
81
|
-
attr.serialize(item, :xml, reg), doc,
|
|
82
|
-
attrs: rule.when_attribute)
|
|
83
|
-
end
|
|
84
|
-
else
|
|
85
|
-
add_leaf(element, row_name(rule, spelling),
|
|
86
|
-
attr.serialize(value, :xml, reg), doc,
|
|
87
|
-
attrs: rule.when_attribute)
|
|
88
|
-
end
|
|
89
|
-
when :collection_native, :collection_cb
|
|
90
|
-
Array(value).each do |item|
|
|
91
|
-
add_leaf(element, rule.name.to_s,
|
|
92
|
-
attr.serialize(item, :xml, reg), doc,
|
|
93
|
-
attrs: rule.when_attribute)
|
|
94
|
-
end
|
|
95
|
-
when :nested, :ordered_deferred
|
|
96
|
-
Array(value).each do |item|
|
|
97
|
-
child_plan = PlanCompiler.compile(item.class, reg)
|
|
98
|
-
child = element.create_child(child_plan[:tree][:name])
|
|
99
|
-
if child_plan[:ordered]
|
|
100
|
-
return nil unless build_ordered(child, item, child_plan,
|
|
101
|
-
doc, reg)
|
|
102
|
-
|
|
103
|
-
else
|
|
104
|
-
build(child, item, child_plan, doc, reg)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
when :raw
|
|
108
|
-
Array(value).each { |raw| element.add_child(raw.to_s) }
|
|
109
|
-
when :content, :content_deferred
|
|
110
|
-
Array(value).each { |run| element.add_child(doc.create_text_node(run.to_s)) }
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Element_order-driven emission (the interpretive order
|
|
116
|
-
# applier's contract): text runs and comments verbatim, each
|
|
117
|
-
# element entry consuming the next item of its collection
|
|
118
|
-
# (document order). Text reads prefer the content attribute
|
|
119
|
-
# when it lines up with the text-node count, so mutations to
|
|
120
|
-
# the model are reflected; element_order text is the fallback.
|
|
121
|
-
# PIs drop (interpretive parity). Returns nil when a nested
|
|
122
|
-
# ordered child lacks element_order (caller falls back).
|
|
123
|
-
def build_ordered(element, instance, plan, doc, reg = nil)
|
|
124
|
-
reg ||= register
|
|
125
|
-
write_attributes(element, instance, plan, reg)
|
|
126
|
-
rows_by_name = {}
|
|
127
|
-
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
128
|
-
next unless rule.name
|
|
129
|
-
|
|
130
|
-
rows_by_name[rule.name.to_s] ||= [rule, attr, kind, spelling,
|
|
131
|
-
delegate]
|
|
132
|
-
end
|
|
133
|
-
content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
|
|
134
|
-
CONTENT_KINDS.include?(kind) && rule.name.nil?
|
|
135
|
-
end
|
|
136
|
-
order = instance.element_order
|
|
137
|
-
element_indices = ::Hash.new(0)
|
|
138
|
-
content_attr = content_entry && content_entry[1]
|
|
139
|
-
content_value = content_attr &&
|
|
140
|
-
value_of(instance, content_attr, content_entry[4])
|
|
141
|
-
text_node_count = order.count { |o| o.type == "Text" }
|
|
142
|
-
use_content_index = content_value.is_a?(Array) &&
|
|
143
|
-
content_value.length == text_node_count
|
|
144
|
-
content_cdata = content_entry && content_entry[0].cdata
|
|
145
|
-
text_node_index = 0
|
|
146
|
-
|
|
147
|
-
order.each do |object|
|
|
148
|
-
case object.type
|
|
149
|
-
when "Text"
|
|
150
|
-
text = if content_attr && !content_value.nil?
|
|
151
|
-
if use_content_index
|
|
152
|
-
content_value[text_node_index].to_s
|
|
153
|
-
elsif !content_value.is_a?(Array) && text_node_count <= 1
|
|
154
|
-
content_value.to_s
|
|
155
|
-
else
|
|
156
|
-
object.text_content || object.name
|
|
157
|
-
end
|
|
158
|
-
else
|
|
159
|
-
object.text_content || object.name
|
|
160
|
-
end
|
|
161
|
-
text_node_index += 1
|
|
162
|
-
next if text.nil?
|
|
163
|
-
# Mixed content keeps whitespace; ordered-only skips it
|
|
164
|
-
next if content_attr.nil? && text.strip.empty?
|
|
165
|
-
|
|
166
|
-
element.add_child(content_cdata ? doc.create_cdata(text) : doc.create_text_node(text))
|
|
167
|
-
when "Comment"
|
|
168
|
-
element.add_child(doc.create_comment(object.text_content))
|
|
169
|
-
when "Element"
|
|
170
|
-
entry = rows_by_name[object.name]
|
|
171
|
-
next unless entry
|
|
172
|
-
|
|
173
|
-
_, attr, kind, _spelling, delegate = entry
|
|
174
|
-
value = value_of(instance, attr, delegate)
|
|
175
|
-
case kind
|
|
176
|
-
when :collection_native, :collection_cb
|
|
177
|
-
index = element_indices[object.name]
|
|
178
|
-
items = Array(value)
|
|
179
|
-
next unless index < items.length
|
|
180
|
-
|
|
181
|
-
element_indices[object.name] += 1
|
|
182
|
-
add_leaf(element, object.name,
|
|
183
|
-
attr.serialize(items[index], :xml, reg), doc)
|
|
184
|
-
when :nested, :ordered_deferred
|
|
185
|
-
item = if attr.collection?
|
|
186
|
-
index = element_indices[object.name]
|
|
187
|
-
element_indices[object.name] += 1
|
|
188
|
-
Array(value)[index]
|
|
189
|
-
else
|
|
190
|
-
value
|
|
191
|
-
end
|
|
192
|
-
next unless item
|
|
193
|
-
|
|
194
|
-
child_plan = PlanCompiler.compile(item.class, reg)
|
|
195
|
-
child = element.create_child(child_plan[:tree][:name])
|
|
196
|
-
if child_plan[:ordered]
|
|
197
|
-
return nil unless build_ordered(child, item, child_plan,
|
|
198
|
-
doc, reg)
|
|
199
|
-
else
|
|
200
|
-
build(child, item, child_plan, doc, reg)
|
|
201
|
-
end
|
|
202
|
-
when :raw
|
|
203
|
-
element.add_child(value.to_s) unless value.nil?
|
|
204
|
-
when :scalar
|
|
205
|
-
add_leaf(element, object.name,
|
|
206
|
-
attr.serialize(value, :xml, reg), doc)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
true
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Attributes in document order when the instance recorded it
|
|
214
|
-
# (parsed models), else plan row order.
|
|
215
|
-
def write_attributes(element, instance, plan, _reg = nil)
|
|
216
|
-
recorded = instance.attribute_order
|
|
217
|
-
if recorded && !recorded.empty?
|
|
218
|
-
by_name = {}
|
|
219
|
-
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
220
|
-
by_name[rule.name.to_s] = [rule, attr, delegate]
|
|
221
|
-
end
|
|
222
|
-
names = recorded |
|
|
223
|
-
plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
|
|
224
|
-
names.each do |name|
|
|
225
|
-
write_attribute(element, instance, name, by_name[name])
|
|
226
|
-
end
|
|
227
|
-
else
|
|
228
|
-
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
229
|
-
write_attribute(element, instance, rule.name.to_s,
|
|
230
|
-
[rule, attr, delegate])
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def write_attribute(element, instance, name, entry, reg = nil)
|
|
236
|
-
return unless entry
|
|
237
|
-
|
|
238
|
-
_rule, attr, delegate = entry
|
|
239
|
-
value = value_of(instance, attr, delegate)
|
|
240
|
-
return if value.nil?
|
|
241
|
-
|
|
242
|
-
element.set_attribute(name,
|
|
243
|
-
attr.serialize(value, :xml, reg || register).to_s)
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Partition rows (#88) re-emit their discriminator: the wire
|
|
247
|
-
# element carries the when_attribute pairs so the round trip
|
|
248
|
-
# re-routes to the same attribute on reparse.
|
|
249
|
-
def add_leaf(element, name, value, doc, attrs: nil)
|
|
250
|
-
child = element.create_child(name)
|
|
251
|
-
attrs&.each { |k, v| child[k.to_s] = v.to_s }
|
|
252
|
-
child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
|
|
253
|
-
child
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def value_of(instance, attr, delegate)
|
|
257
|
-
if delegate
|
|
258
|
-
target = instance.public_send(delegate)
|
|
259
|
-
target&.public_send(attr.name)
|
|
260
|
-
else
|
|
261
|
-
instance.public_send(attr.name)
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
def row_name(rule, spelling)
|
|
266
|
-
(spelling || rule.name).to_s
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
end
|
data/lib/lutaml/xml/plan_walk.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Lutaml
|
|
4
|
-
module Xml
|
|
5
|
-
# Public consumer surface for the plan fast path: one engine
|
|
6
|
-
# parse + descriptor walk and direct model hydration, bypassing
|
|
7
|
-
# the wrapper-tree construction that dominates large-document
|
|
8
|
-
# consumer paths. The returned object is the mapped model root;
|
|
9
|
-
# collection attributes remain collections on that model.
|
|
10
|
-
module PlanWalk
|
|
11
|
-
class << self
|
|
12
|
-
# Build the plan, walk the document, and hydrate the mapped
|
|
13
|
-
# root. Returns nil when the model is not plan-compilable.
|
|
14
|
-
def call(model_class, xml, _options = {})
|
|
15
|
-
register = Lutaml::Model::Config.default_register
|
|
16
|
-
plan = PlanCompiler.compile(model_class, register)
|
|
17
|
-
return nil unless plan
|
|
18
|
-
|
|
19
|
-
document = ::Leptris::XML.parse(xml)
|
|
20
|
-
root = document.root
|
|
21
|
-
return nil unless root && root.name == plan[:tree][:name]
|
|
22
|
-
|
|
23
|
-
PlanHydrator.call(model_class, plan,
|
|
24
|
-
plan[:descriptor].walk(root),
|
|
25
|
-
node: root)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|