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,362 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Lutaml
|
|
4
|
-
module Xml
|
|
5
|
-
# Phase 5 slice: compile a model's XML mapping into a
|
|
6
|
-
# Leptris::XML::Descriptor plan and materialize whole documents in
|
|
7
|
-
# one native pass (leptris_plan_walk) — no moxml wrapper tree, no
|
|
8
|
-
# per-element Ruby dispatch. Measured on the 200-item probe:
|
|
9
|
-
# walk+hydrate 8.7x faster, 82% fewer allocations than the
|
|
10
|
-
# interpretive path, hydration-equal output.
|
|
11
|
-
#
|
|
12
|
-
# A model compiles when EVERY rule is plan-shaped; richer rows
|
|
13
|
-
# defer their subtree verbatim and interpret post-walk rather
|
|
14
|
-
# than opting the whole model out:
|
|
15
|
-
# - map_attribute / map_element / content / raw rows
|
|
16
|
-
# - Value-scalar types or nested Serializables
|
|
17
|
-
# - custom methods, polymorphism, unions, and ordered/mixed
|
|
18
|
-
# children capture :raw and interpret post-walk
|
|
19
|
-
# - ordered/mixed root mappings compile; the entry points
|
|
20
|
-
# reconstruct element_order from the node surface
|
|
21
|
-
# - attributes not derived/union/polymorphic
|
|
22
|
-
#
|
|
23
|
-
# The fast path is opt-in (Config.xml_plan_fast_path) while the
|
|
24
|
-
# full semantics audit (parent links, consolidation, ordering
|
|
25
|
-
# metadata) completes.
|
|
26
|
-
module PlanCompiler
|
|
27
|
-
# TODO.perf/15: env-gated histogram of why models opt out of the
|
|
28
|
-
# plan path. Set PLAN_COMPILE_STATS=1 and read PlanCompiler.stats
|
|
29
|
-
# after a parse. Zero cost when disabled.
|
|
30
|
-
def self.stats
|
|
31
|
-
@stats ||= Hash.new(0)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.opt_out!(clause)
|
|
35
|
-
@plan_stats ||= Hash.new(0)
|
|
36
|
-
@plan_stats[clause] += 1 if ENV["PLAN_COMPILE_STATS"]
|
|
37
|
-
nil
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Isolated holder: suites freeze model classes; a cache on a
|
|
41
|
-
# frozen constant would be immutable (TypeProbeCache precedent).
|
|
42
|
-
PLAN_CACHE = ::Class.new do
|
|
43
|
-
class << self
|
|
44
|
-
def cache
|
|
45
|
-
@cache ||= {}
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end.cache
|
|
49
|
-
|
|
50
|
-
class << self
|
|
51
|
-
def compile(model_class, register)
|
|
52
|
-
# A child model's declared lutaml_default_register takes
|
|
53
|
-
# precedence over the ambient (parent) register — the same
|
|
54
|
-
# contract the interpretive path applies through
|
|
55
|
-
# Register.resolve_for_child. Without this, plan compilation
|
|
56
|
-
# resolves the child's symbol attribute types in the parent
|
|
57
|
-
# context and raises UnknownTypeError for ids registered only
|
|
58
|
-
# in the child's own register (#876).
|
|
59
|
-
register = Lutaml::Model::Register.resolve_for_child(
|
|
60
|
-
model_class, register
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
key = [model_class, register]
|
|
64
|
-
return PLAN_CACHE[key] if PLAN_CACHE.key?(key)
|
|
65
|
-
|
|
66
|
-
# Cycle guard: a self-referential model (JATS sec-in-sec) must
|
|
67
|
-
# resolve to nil — the interpretive pipeline owns it. The guard
|
|
68
|
-
# is THREAD-LOCAL (a shared in-progress set races: a concurrent
|
|
69
|
-
# same-key compile would cache false permanently — the #828
|
|
70
|
-
# lesson); the nil at the cycle point is NOT cached — the
|
|
71
|
-
# outermost build completes and caches the real verdict.
|
|
72
|
-
stack = (Thread.current[:plan_compiler_stack] ||= [])
|
|
73
|
-
return nil if stack.include?(key)
|
|
74
|
-
|
|
75
|
-
stack.push(key)
|
|
76
|
-
begin
|
|
77
|
-
PLAN_CACHE[key] = build(model_class, register)
|
|
78
|
-
ensure
|
|
79
|
-
stack.pop
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
private
|
|
84
|
-
|
|
85
|
-
def build(model_class, register)
|
|
86
|
-
return nil unless model_class.is_a?(Class) &&
|
|
87
|
-
model_class.include?(::Lutaml::Model::Serialize)
|
|
88
|
-
|
|
89
|
-
mapping = model_class.mappings_for(:xml, register)
|
|
90
|
-
return nil unless compilable_mapping?(mapping)
|
|
91
|
-
|
|
92
|
-
rows = []
|
|
93
|
-
attr_rows = [] # [[rule, attr]] hydration metadata
|
|
94
|
-
plan_attrs = [] # [{name:, kind:}] rows for the engine plan
|
|
95
|
-
compiled = [] # [rule, attr, kind, spelling, delegate_target]
|
|
96
|
-
cdata = false
|
|
97
|
-
mixed_content = false
|
|
98
|
-
needs_nodes = false
|
|
99
|
-
collection_defaults = [] # collection attrs with element rows
|
|
100
|
-
tag = 100 # type_tag echo space for callback-routed rows
|
|
101
|
-
model_ns = plan_namespace(model_class, mapping, register)
|
|
102
|
-
|
|
103
|
-
# Collection rows are NATIVE since leptris 1.9.178 —
|
|
104
|
-
# collection values echo name and type_tag (the #220 gap is
|
|
105
|
-
# closed), so any number routes without callbacks.
|
|
106
|
-
true
|
|
107
|
-
|
|
108
|
-
mapping.mappings(register).each do |rule|
|
|
109
|
-
delegate_target = nil
|
|
110
|
-
attr = if rule.delegate
|
|
111
|
-
delegate_target = model_class.attributes(register)[rule.delegate]
|
|
112
|
-
t = delegate_target&.type(register)
|
|
113
|
-
if t.is_a?(Class) && t.include?(::Lutaml::Model::Serialize)
|
|
114
|
-
t.attributes(register)[rule.to]
|
|
115
|
-
end
|
|
116
|
-
else
|
|
117
|
-
model_class.attributes(register)[rule.to]
|
|
118
|
-
end
|
|
119
|
-
return opt_out!(:attr_nil) if attr.nil?
|
|
120
|
-
return opt_out!(:derived) if attr.derived?
|
|
121
|
-
|
|
122
|
-
# Partition rows (TODO 34 step 2) ride native predicates
|
|
123
|
-
# only in the plain-capture shape; any other when_attribute
|
|
124
|
-
# shape stays interpretive (the suite pins nested-target
|
|
125
|
-
# partitions to the interpretive path).
|
|
126
|
-
unless rule.when_attribute.empty?
|
|
127
|
-
t = attr.type(register)
|
|
128
|
-
plain_partition = !rule.delegate &&
|
|
129
|
-
!rule.has_custom_method_for_deserialization? &&
|
|
130
|
-
!rule.polymorphic_mapping? &&
|
|
131
|
-
!attr.polymorphic? && !attr.union? &&
|
|
132
|
-
!(t.is_a?(Class) &&
|
|
133
|
-
t.include?(::Lutaml::Model::Serialize))
|
|
134
|
-
return opt_out!(:non_plain_partition) unless plain_partition
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Interpretive hydration materializes every mapped
|
|
138
|
-
# collection, present or not; the fast path mirrors with
|
|
139
|
-
# constructor-time empty arrays.
|
|
140
|
-
unless rule.attribute? || !attr.collection?
|
|
141
|
-
collection_defaults << attr.name.to_sym
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Fragment deferrals lose ancestor namespace context —
|
|
145
|
-
# ns-qualified models keep those rules interpretive.
|
|
146
|
-
fragment_needed = rule.has_custom_method_for_deserialization? ||
|
|
147
|
-
rule.polymorphic_mapping? || attr.polymorphic? || attr.union?
|
|
148
|
-
return opt_out!(:fragment_with_ns) if fragment_needed && model_ns
|
|
149
|
-
|
|
150
|
-
if rule.attribute?
|
|
151
|
-
return opt_out!(:non_scalar) unless scalar_type?(attr, register)
|
|
152
|
-
# Attribute plan rows are local-name keyed; a type-level
|
|
153
|
-
# namespace makes the attribute (URI, local)-identified
|
|
154
|
-
# (lutaml-model#744) — the interpretive matcher owns it
|
|
155
|
-
# until plan rows carry namespace identity.
|
|
156
|
-
return opt_out!(:attr_type_ns) if attr.type_namespace_class(register)
|
|
157
|
-
|
|
158
|
-
attr_rows << [rule, attr]
|
|
159
|
-
plan_attrs << { name: rule.name.to_s }
|
|
160
|
-
elsif rule.content_mapping?
|
|
161
|
-
return opt_out!(:multi_content) if content_rows(rows) >= 1
|
|
162
|
-
|
|
163
|
-
mixed_content = true
|
|
164
|
-
compiled << [rule, attr,
|
|
165
|
-
attr.collection? ? :content : :content_deferred,
|
|
166
|
-
nil, delegate_target]
|
|
167
|
-
rows << { name: "__content__#{compiled.size}", kind: :content }
|
|
168
|
-
elsif rule.raw_mapping? || rule.raw == :element
|
|
169
|
-
compiled << [rule, attr, :raw, nil, delegate_target]
|
|
170
|
-
rows << { name: rule.name.to_s, kind: :raw }
|
|
171
|
-
elsif rule.has_custom_method_for_deserialization?
|
|
172
|
-
needs_nodes = true
|
|
173
|
-
compiled << [rule, attr, :custom_method, nil, delegate_target]
|
|
174
|
-
rows << { name: rule.name.to_s, kind: :raw }
|
|
175
|
-
elsif rule.polymorphic_mapping? || attr.polymorphic? || attr.union?
|
|
176
|
-
type = attr.type(register)
|
|
177
|
-
return opt_out!(:non_serializable_poly) unless serializable_type?(type) || attr.union?
|
|
178
|
-
|
|
179
|
-
needs_nodes = true
|
|
180
|
-
compiled << [rule, attr, :polymorphic, nil, delegate_target]
|
|
181
|
-
rows << { name: rule.name.to_s, kind: :raw }
|
|
182
|
-
else
|
|
183
|
-
type = attr.type(register)
|
|
184
|
-
if serializable_type?(type)
|
|
185
|
-
child = compile(type, register)
|
|
186
|
-
return opt_out!(:child_uncompilable) unless child
|
|
187
|
-
|
|
188
|
-
if child[:ordered]
|
|
189
|
-
# Ordered/mixed children need element_order on their
|
|
190
|
-
# instances; the walk hands back plan values, not
|
|
191
|
-
# source nodes, so the subtree defers interpretively
|
|
192
|
-
# (the fragment parse runs the full machinery,
|
|
193
|
-
# order included).
|
|
194
|
-
return opt_out!(:ordered_child_with_ns) if model_ns
|
|
195
|
-
|
|
196
|
-
needs_nodes = true
|
|
197
|
-
compiled << [rule, attr, :ordered_deferred, nil,
|
|
198
|
-
delegate_target]
|
|
199
|
-
rows << { name: rule.name.to_s, kind: :raw }
|
|
200
|
-
else
|
|
201
|
-
compiled << [rule, attr, :nested, nil, delegate_target]
|
|
202
|
-
needs_nodes ||= child[:needs_nodes]
|
|
203
|
-
nested_row = { name: rule.name.to_s, kind: :nested,
|
|
204
|
-
plan: child[:tree] }
|
|
205
|
-
nested_row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
206
|
-
rows << nested_row
|
|
207
|
-
end
|
|
208
|
-
elsif rule.multiple_mappings?
|
|
209
|
-
rule.name.each do |spelling|
|
|
210
|
-
compiled << [rule, attr, :spelling, spelling.to_s,
|
|
211
|
-
delegate_target]
|
|
212
|
-
rows << { name: spelling.to_s, kind: :callback,
|
|
213
|
-
type_tag: (tag += 1) }
|
|
214
|
-
end
|
|
215
|
-
else
|
|
216
|
-
return opt_out!(:non_scalar) unless scalar_type?(attr, register)
|
|
217
|
-
|
|
218
|
-
row = { name: rule.name.to_s }
|
|
219
|
-
row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
220
|
-
# lutaml-model#88: same-name rows partitioned by the
|
|
221
|
-
# rule's discriminator ride the engine's exclusive
|
|
222
|
-
# predicate match (leptris 1.9.221+, #1272).
|
|
223
|
-
row[:when] = rule.when_attribute unless rule.when_attribute.empty?
|
|
224
|
-
|
|
225
|
-
if attr.collection?
|
|
226
|
-
compiled << [rule, attr, :collection_native, nil,
|
|
227
|
-
delegate_target]
|
|
228
|
-
rows << row.merge(kind: :collection)
|
|
229
|
-
else
|
|
230
|
-
cdata ||= rule.cdata
|
|
231
|
-
compiled << [rule, attr, :scalar, nil, delegate_target]
|
|
232
|
-
rows << row.merge(kind: :scalar)
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
row_tags = partition_row_tags!(compiled, rows, tag)
|
|
239
|
-
|
|
240
|
-
# The plan serializer does not emit namespace declarations —
|
|
241
|
-
# namespaced models serialize through the interpretive
|
|
242
|
-
# writer (lutaml-model#847: standalone to_xml under leptris
|
|
243
|
-
# dropped the element xmlns entirely).
|
|
244
|
-
namespaced = !model_ns.nil? || rows.any? { |r| r[:ns] }
|
|
245
|
-
|
|
246
|
-
flags = []
|
|
247
|
-
flags << :cdata if cdata
|
|
248
|
-
flags << :mixed_content if mixed_content
|
|
249
|
-
flags << :ns_lenient if model_ns
|
|
250
|
-
tree = { name: mapping.root_element.to_s,
|
|
251
|
-
attributes: plan_attrs, children: rows }
|
|
252
|
-
tree[:ns] = model_ns if model_ns
|
|
253
|
-
tree[:flags] = flags unless flags.empty?
|
|
254
|
-
begin
|
|
255
|
-
# Lazy: the Opal boot loads this file, and leptris is a
|
|
256
|
-
# native gem there — the require only belongs on the
|
|
257
|
-
# engines-enabled path.
|
|
258
|
-
require "leptris/xml/descriptor"
|
|
259
|
-
descriptor = ::Leptris::XML::Descriptor.build(**tree)
|
|
260
|
-
rescue StandardError
|
|
261
|
-
return nil
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
{ descriptor: descriptor, tree: tree, rows: compiled,
|
|
265
|
-
attr_rows: attr_rows, mapping: mapping,
|
|
266
|
-
row_tags: row_tags, namespaced: namespaced,
|
|
267
|
-
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
268
|
-
needs_nodes: needs_nodes,
|
|
269
|
-
collection_defaults: collection_defaults }
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def compilable_mapping?(mapping)
|
|
273
|
-
mapping.root_element &&
|
|
274
|
-
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings)
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
# Partition bookkeeping for when_attribute rows (#88, TODO
|
|
278
|
-
# 34 step 2): the engine matches same-name rows exclusively —
|
|
279
|
-
# first matching row wins — so predicate rows must PRECEDE any
|
|
280
|
-
# plain sibling on the same name (a plain-first order
|
|
281
|
-
# double-captures: the plain row takes everything and the
|
|
282
|
-
# predicates still claim their matches). Every row of a
|
|
283
|
-
# partitioned name gets a distinct type_tag; plan values echo
|
|
284
|
-
# it back, giving the hydrator row-exact routing with no
|
|
285
|
-
# per-occurrence re-derivation. Returns {compiled_index =>
|
|
286
|
-
# tag} (nil when the model has no partitions). compiled and
|
|
287
|
-
# rows are parallel and stay in lockstep through the reorder.
|
|
288
|
-
def partition_row_tags!(compiled, rows, tag)
|
|
289
|
-
by_name = rows.each_with_index
|
|
290
|
-
.group_by { |(row, _)| row[:name] }
|
|
291
|
-
.select { |_name, pairs| pairs.any? { |(row, _)| row[:when] } }
|
|
292
|
-
return nil if by_name.empty?
|
|
293
|
-
|
|
294
|
-
permutation = by_name.each_value.flat_map do |pairs|
|
|
295
|
-
pairs.sort_by { |(row, i)| [row[:when] ? 0 : 1, i] }
|
|
296
|
-
.map(&:last)
|
|
297
|
-
end
|
|
298
|
-
remaining = (0...rows.length).to_a - permutation
|
|
299
|
-
permutation.concat(remaining)
|
|
300
|
-
|
|
301
|
-
row_tags = {}
|
|
302
|
-
permutation.each_with_index do |old_index, new_index|
|
|
303
|
-
row = rows[old_index]
|
|
304
|
-
next unless by_name.key?(row[:name])
|
|
305
|
-
|
|
306
|
-
tag += 1
|
|
307
|
-
row[:type_tag] = tag
|
|
308
|
-
row_tags[new_index] = tag
|
|
309
|
-
end
|
|
310
|
-
rows.replace(permutation.map { |i| rows[i] })
|
|
311
|
-
compiled.replace(permutation.map { |i| compiled[i] })
|
|
312
|
-
row_tags
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
# Attribute for a rule — delegate rules resolve against their
|
|
316
|
-
# target model's attributes.
|
|
317
|
-
def attr_of(model_class, rule, register)
|
|
318
|
-
attr = model_class.attributes(register)[rule.to]
|
|
319
|
-
return attr if attr || !rule.delegate
|
|
320
|
-
|
|
321
|
-
target = model_class.attributes(register)[rule.delegate]
|
|
322
|
-
t = target&.type(register)
|
|
323
|
-
if t.is_a?(Class) && t.include?(::Lutaml::Model::Serialize)
|
|
324
|
-
t.attributes(register)[rule.to]
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Rule-level namespace → ChildPlan ns form (leptris 1.9.178):
|
|
329
|
-
# the child binds by local name under the rule's URI with any
|
|
330
|
-
# prefix. Blank-namespace rules (xmlns="") match :none.
|
|
331
|
-
def child_ns(rule, _model_ns)
|
|
332
|
-
uri = rule.namespace
|
|
333
|
-
return { exact: uri.to_s } if uri && !uri.to_s.empty?
|
|
334
|
-
|
|
335
|
-
:none
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def content_rows(rows)
|
|
339
|
-
rows.count { |r| r[:kind] == :content }
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
# Model-level namespace: exact URI match with lenient prefixes
|
|
343
|
-
# — children bind by local name under any prefix the document
|
|
344
|
-
# bound to the URI (#754 adoption semantics on the engine).
|
|
345
|
-
def plan_namespace(_model_class, mapping, _register)
|
|
346
|
-
ns_class = mapping.namespace_class if mapping.respond_to?(:namespace_class)
|
|
347
|
-
ns_class&.uri ? { exact: ns_class.uri.to_s } : nil
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
def scalar_type?(attr, register)
|
|
351
|
-
type = attr.type(register)
|
|
352
|
-
type.is_a?(Class) && type < ::Lutaml::Model::Type::Value &&
|
|
353
|
-
!attr.custom_collection?
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
def serializable_type?(type)
|
|
357
|
-
type.is_a?(Class) && type.include?(::Lutaml::Model::Serialize)
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
end
|