lutaml-model 0.8.23 → 0.8.28
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/.github/workflows/performance.yml +1 -1
- data/Gemfile +6 -0
- data/README.adoc +16 -7
- data/docs/_guides/index.adoc +4 -0
- data/docs/_guides/native-engines.adoc +61 -0
- data/docs/_pages/configuration.adoc +11 -7
- data/docs/_pages/serialization_adapters.adoc +14 -5
- data/lib/compat/opal/lutaml_model_boot.rb +5 -0
- data/lib/lutaml/json/adapter/multi_json_adapter.rb +12 -1
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +21 -0
- data/lib/lutaml/json/adapter.rb +1 -0
- data/lib/lutaml/json/format.rb +1 -1
- data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +16 -1
- data/lib/lutaml/key_value/adapter/json/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +42 -0
- data/lib/lutaml/key_value/adapter/toml.rb +1 -0
- data/lib/lutaml/key_value/adapter/yaml/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/transform.rb +82 -0
- data/lib/lutaml/key_value/transformation/value_serializer.rb +25 -0
- data/lib/lutaml/key_value/transformation.rb +25 -1
- data/lib/lutaml/model/adapter_resolver.rb +23 -6
- data/lib/lutaml/model/attribute.rb +31 -6
- data/lib/lutaml/model/compiled_rule.rb +16 -0
- data/lib/lutaml/model/configuration.rb +5 -0
- data/lib/lutaml/model/error/unknown_adapter_type_error.rb +4 -4
- data/lib/lutaml/model/serialize/format_conversion.rb +65 -0
- data/lib/lutaml/model/toml.rb +10 -6
- data/lib/lutaml/model/type/date_time.rb +2 -2
- data/lib/lutaml/model/type/value.rb +24 -2
- data/lib/lutaml/model/utils.rb +6 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/toml/adapter/teptris_adapter.rb +14 -0
- data/lib/lutaml/toml/adapter.rb +1 -0
- data/lib/lutaml/toml/format.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +15 -0
- data/lib/lutaml/xml/adapter_element.rb +111 -3
- data/lib/lutaml/xml/builder/base.rb +3 -1
- data/lib/lutaml/xml/builder/leptris.rb +13 -0
- data/lib/lutaml/xml/builder/nokogiri.rb +3 -0
- data/lib/lutaml/xml/builder.rb +1 -0
- data/lib/lutaml/xml/declaration_planner.rb +8 -2
- data/lib/lutaml/xml/format.rb +7 -1
- data/lib/lutaml/xml/leptris/element.rb +34 -0
- data/lib/lutaml/xml/namespace_collector.rb +4 -3
- data/lib/lutaml/xml/plan_compiler.rb +253 -0
- data/lib/lutaml/xml/plan_hydrator.rb +341 -0
- data/lib/lutaml/xml/plan_order.rb +50 -0
- data/lib/lutaml/xml/plan_serializer.rb +241 -0
- data/lib/lutaml/xml/plan_walk.rb +30 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +8 -0
- data/lib/lutaml/xml/xml_element.rb +6 -2
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +46 -0
- data/lib/lutaml/yaml/adapter.rb +5 -0
- data/lib/lutaml/yaml/format.rb +1 -1
- data/lib/tasks/performance.rake +5 -0
- data/lib/tasks/performance_comparator.rb +19 -3
- data/lib/tasks/performance_probe.rake +33 -0
- data/lib/tasks/performance_probe.rb +106 -0
- data/lib/tasks/performance_probe_plan_walk.rb +58 -0
- data/lib/tasks/performance_probe_xml.rb +54 -0
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/json/yeptris_adapter_spec.rb +63 -0
- data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +71 -0
- data/spec/lutaml/model/json_adapter_spec.rb +6 -0
- data/spec/lutaml/model/toml_adapter_spec.rb +8 -0
- data/spec/lutaml/model/toml_spec.rb +36 -1
- data/spec/lutaml/xml/plan_fast_path_spec.rb +547 -0
- data/spec/lutaml/xml/plan_walk_spec.rb +102 -0
- data/spec/lutaml/xml/xml_spec.rb +13 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +63 -0
- data/spec/spec_helper.rb +7 -1
- metadata +28 -4
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
class LeptrisElement < AdapterElement
|
|
6
|
+
def initialize(node, parent: nil, default_namespace: nil)
|
|
7
|
+
@raw_text = node.raw_content if node.is_a?(Moxml::Text)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def adapter_class
|
|
14
|
+
Lutaml::Xml::Adapter::LeptrisAdapter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_text_for_xml
|
|
18
|
+
@raw_text || @text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_element_xml(builder)
|
|
22
|
+
builder.create_and_add_element(name,
|
|
23
|
+
prefix: namespace_prefix,
|
|
24
|
+
attributes: build_attributes(self)) do |xml|
|
|
25
|
+
children.each { |child| child.build_xml(xml) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attribute_value_for_build(attr)
|
|
30
|
+
attr.raw_value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -566,9 +566,10 @@ module Lutaml
|
|
|
566
566
|
# Resolve type using child model's register context
|
|
567
567
|
mapper_class = options[:mapper_class]
|
|
568
568
|
child_register = if mapper_class
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
569
|
+
(@register_for ||= {})[mapper_class] ||=
|
|
570
|
+
Lutaml::Model::Register.resolve_for_child(
|
|
571
|
+
mapper_class, @register
|
|
572
|
+
)
|
|
572
573
|
else
|
|
573
574
|
@register
|
|
574
575
|
end
|
|
@@ -0,0 +1,253 @@
|
|
|
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
|
+
# Isolated holder: suites freeze model classes; a cache on a
|
|
28
|
+
# frozen constant would be immutable (TypeProbeCache precedent).
|
|
29
|
+
PLAN_CACHE = ::Class.new do
|
|
30
|
+
class << self
|
|
31
|
+
def cache
|
|
32
|
+
@cache ||= {}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end.cache
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
def compile(model_class, register)
|
|
39
|
+
key = [model_class, register]
|
|
40
|
+
return PLAN_CACHE[key] if PLAN_CACHE.key?(key)
|
|
41
|
+
|
|
42
|
+
PLAN_CACHE[key] = build(model_class, register)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build(model_class, register)
|
|
48
|
+
return nil unless model_class.is_a?(Class) &&
|
|
49
|
+
model_class.include?(::Lutaml::Model::Serialize)
|
|
50
|
+
|
|
51
|
+
mapping = model_class.mappings_for(:xml, register)
|
|
52
|
+
return nil unless compilable_mapping?(mapping)
|
|
53
|
+
|
|
54
|
+
rows = []
|
|
55
|
+
attr_rows = [] # [[rule, attr]] hydration metadata
|
|
56
|
+
plan_attrs = [] # [{name:, kind:}] rows for the engine plan
|
|
57
|
+
compiled = [] # [rule, attr, kind, spelling, delegate_target]
|
|
58
|
+
cdata = false
|
|
59
|
+
mixed_content = false
|
|
60
|
+
needs_nodes = false
|
|
61
|
+
collection_defaults = [] # collection attrs with element rows
|
|
62
|
+
tag = 100 # type_tag echo space for callback-routed rows
|
|
63
|
+
model_ns = plan_namespace(model_class, mapping, register)
|
|
64
|
+
|
|
65
|
+
# Collection rows are NATIVE since leptris 1.9.178 —
|
|
66
|
+
# collection values echo name and type_tag (the #220 gap is
|
|
67
|
+
# closed), so any number routes without callbacks.
|
|
68
|
+
true
|
|
69
|
+
|
|
70
|
+
mapping.mappings(register).each do |rule|
|
|
71
|
+
delegate_target = nil
|
|
72
|
+
attr = if rule.delegate
|
|
73
|
+
delegate_target = model_class.attributes(register)[rule.delegate]
|
|
74
|
+
t = delegate_target&.type(register)
|
|
75
|
+
if t.is_a?(Class) && t.include?(::Lutaml::Model::Serialize)
|
|
76
|
+
t.attributes(register)[rule.to]
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
model_class.attributes(register)[rule.to]
|
|
80
|
+
end
|
|
81
|
+
return nil if attr.nil?
|
|
82
|
+
return nil if attr.derived?
|
|
83
|
+
|
|
84
|
+
# Interpretive hydration materializes every mapped
|
|
85
|
+
# collection, present or not; the fast path mirrors with
|
|
86
|
+
# constructor-time empty arrays.
|
|
87
|
+
unless rule.attribute? || !attr.collection?
|
|
88
|
+
collection_defaults << attr.name.to_sym
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Fragment deferrals lose ancestor namespace context —
|
|
92
|
+
# ns-qualified models keep those rules interpretive.
|
|
93
|
+
fragment_needed = rule.has_custom_method_for_deserialization? ||
|
|
94
|
+
rule.polymorphic_mapping? || attr.polymorphic? || attr.union?
|
|
95
|
+
return nil if fragment_needed && model_ns
|
|
96
|
+
|
|
97
|
+
if rule.attribute?
|
|
98
|
+
return nil unless scalar_type?(attr, register)
|
|
99
|
+
|
|
100
|
+
attr_rows << [rule, attr]
|
|
101
|
+
plan_attrs << { name: rule.name.to_s }
|
|
102
|
+
elsif rule.content_mapping?
|
|
103
|
+
return nil if content_rows(rows) >= 1
|
|
104
|
+
|
|
105
|
+
mixed_content = true
|
|
106
|
+
compiled << [rule, attr,
|
|
107
|
+
attr.collection? ? :content : :content_deferred,
|
|
108
|
+
nil, delegate_target]
|
|
109
|
+
rows << { name: "__content__#{compiled.size}", kind: :content }
|
|
110
|
+
elsif rule.raw_mapping? || rule.raw == :element
|
|
111
|
+
compiled << [rule, attr, :raw, nil, delegate_target]
|
|
112
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
113
|
+
elsif rule.has_custom_method_for_deserialization?
|
|
114
|
+
needs_nodes = true
|
|
115
|
+
compiled << [rule, attr, :custom_method, nil, delegate_target]
|
|
116
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
117
|
+
elsif rule.polymorphic_mapping? || attr.polymorphic? || attr.union?
|
|
118
|
+
type = attr.type(register)
|
|
119
|
+
return nil unless serializable_type?(type) || attr.union?
|
|
120
|
+
|
|
121
|
+
needs_nodes = true
|
|
122
|
+
compiled << [rule, attr, :polymorphic, nil, delegate_target]
|
|
123
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
124
|
+
else
|
|
125
|
+
type = attr.type(register)
|
|
126
|
+
if serializable_type?(type)
|
|
127
|
+
child = compile(type, register)
|
|
128
|
+
return nil unless child
|
|
129
|
+
|
|
130
|
+
if child[:ordered]
|
|
131
|
+
# Ordered/mixed children need element_order on their
|
|
132
|
+
# instances; the walk hands back plan values, not
|
|
133
|
+
# source nodes, so the subtree defers interpretively
|
|
134
|
+
# (the fragment parse runs the full machinery,
|
|
135
|
+
# order included).
|
|
136
|
+
return nil if model_ns
|
|
137
|
+
|
|
138
|
+
needs_nodes = true
|
|
139
|
+
compiled << [rule, attr, :ordered_deferred, nil,
|
|
140
|
+
delegate_target]
|
|
141
|
+
rows << { name: rule.name.to_s, kind: :raw }
|
|
142
|
+
else
|
|
143
|
+
compiled << [rule, attr, :nested, nil, delegate_target]
|
|
144
|
+
needs_nodes ||= child[:needs_nodes]
|
|
145
|
+
nested_row = { name: rule.name.to_s, kind: :nested,
|
|
146
|
+
plan: child[:tree] }
|
|
147
|
+
nested_row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
148
|
+
rows << nested_row
|
|
149
|
+
end
|
|
150
|
+
elsif rule.multiple_mappings?
|
|
151
|
+
rule.name.each do |spelling|
|
|
152
|
+
compiled << [rule, attr, :spelling, spelling.to_s,
|
|
153
|
+
delegate_target]
|
|
154
|
+
rows << { name: spelling.to_s, kind: :callback,
|
|
155
|
+
type_tag: (tag += 1) }
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
return nil unless scalar_type?(attr, register)
|
|
159
|
+
|
|
160
|
+
row = { name: rule.name.to_s }
|
|
161
|
+
row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
162
|
+
|
|
163
|
+
if attr.collection?
|
|
164
|
+
compiled << [rule, attr, :collection_native, nil,
|
|
165
|
+
delegate_target]
|
|
166
|
+
rows << row.merge(kind: :collection)
|
|
167
|
+
else
|
|
168
|
+
cdata ||= rule.cdata
|
|
169
|
+
compiled << [rule, attr, :scalar, nil, delegate_target]
|
|
170
|
+
rows << row.merge(kind: :scalar)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
flags = []
|
|
177
|
+
flags << :cdata if cdata
|
|
178
|
+
flags << :mixed_content if mixed_content
|
|
179
|
+
flags << :ns_lenient if model_ns
|
|
180
|
+
tree = { name: mapping.root_element.to_s,
|
|
181
|
+
attributes: plan_attrs, children: rows }
|
|
182
|
+
tree[:ns] = model_ns if model_ns
|
|
183
|
+
tree[:flags] = flags unless flags.empty?
|
|
184
|
+
begin
|
|
185
|
+
# Lazy: the Opal boot loads this file, and leptris is a
|
|
186
|
+
# native gem there — the require only belongs on the
|
|
187
|
+
# engines-enabled path.
|
|
188
|
+
require "leptris/xml/descriptor"
|
|
189
|
+
descriptor = ::Leptris::XML::Descriptor.build(**tree)
|
|
190
|
+
rescue StandardError
|
|
191
|
+
return nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
{ descriptor: descriptor, tree: tree, rows: compiled,
|
|
195
|
+
attr_rows: attr_rows, mapping: mapping,
|
|
196
|
+
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
197
|
+
needs_nodes: needs_nodes,
|
|
198
|
+
collection_defaults: collection_defaults }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def compilable_mapping?(mapping)
|
|
202
|
+
mapping.root_element &&
|
|
203
|
+
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Attribute for a rule — delegate rules resolve against their
|
|
207
|
+
# target model's attributes.
|
|
208
|
+
def attr_of(model_class, rule, register)
|
|
209
|
+
attr = model_class.attributes(register)[rule.to]
|
|
210
|
+
return attr if attr || !rule.delegate
|
|
211
|
+
|
|
212
|
+
target = model_class.attributes(register)[rule.delegate]
|
|
213
|
+
t = target&.type(register)
|
|
214
|
+
if t.is_a?(Class) && t.include?(::Lutaml::Model::Serialize)
|
|
215
|
+
t.attributes(register)[rule.to]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Rule-level namespace → ChildPlan ns form (leptris 1.9.178):
|
|
220
|
+
# the child binds by local name under the rule's URI with any
|
|
221
|
+
# prefix. Blank-namespace rules (xmlns="") match :none.
|
|
222
|
+
def child_ns(rule, _model_ns)
|
|
223
|
+
uri = rule.namespace
|
|
224
|
+
return { exact: uri.to_s } if uri && !uri.to_s.empty?
|
|
225
|
+
|
|
226
|
+
:none
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def content_rows(rows)
|
|
230
|
+
rows.count { |r| r[:kind] == :content }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Model-level namespace: exact URI match with lenient prefixes
|
|
234
|
+
# — children bind by local name under any prefix the document
|
|
235
|
+
# bound to the URI (#754 adoption semantics on the engine).
|
|
236
|
+
def plan_namespace(_model_class, mapping, _register)
|
|
237
|
+
ns_class = mapping.namespace_class if mapping.respond_to?(:namespace_class)
|
|
238
|
+
ns_class&.uri ? { exact: ns_class.uri.to_s } : nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def scalar_type?(attr, register)
|
|
242
|
+
type = attr.type(register)
|
|
243
|
+
type.is_a?(Class) && type < ::Lutaml::Model::Type::Value &&
|
|
244
|
+
!attr.custom_collection?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def serializable_type?(type)
|
|
248
|
+
type.is_a?(Class) && type.include?(::Lutaml::Model::Serialize)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# Hydrates model instances from a Descriptor#walk PlanValue tree,
|
|
6
|
+
# keyed by each value's producing row name (never position — rows
|
|
7
|
+
# for missing elements are simply absent). Native rows build
|
|
8
|
+
# constructor kwargs; deferred rows (custom methods, polymorphism,
|
|
9
|
+
# unions) capture their subtree verbatim and interpret it
|
|
10
|
+
# post-walk — the fragment parse runs the existing interpretive
|
|
11
|
+
# machinery on just that island. Delegate rules hydrate
|
|
12
|
+
# post-instance onto their target object. Collection rows route
|
|
13
|
+
# natively when a single one exists, else through callback rows
|
|
14
|
+
# (their values echo name and type_tag; native collection values
|
|
15
|
+
# echo neither — leptris-ruby#220).
|
|
16
|
+
module PlanHydrator
|
|
17
|
+
class << self
|
|
18
|
+
# plan: the compiler's entry for model_class
|
|
19
|
+
# value: the walk root PlanValue (element)
|
|
20
|
+
# node: the parsed source element (leptris); ordered/mixed
|
|
21
|
+
# plans rebuild element_order from it and ordered children
|
|
22
|
+
# hydrate natively against their own nodes
|
|
23
|
+
def call(model_class, plan, value, parent: nil, node: nil)
|
|
24
|
+
buckets = node && plan[:needs_nodes] ? element_buckets(node) : nil
|
|
25
|
+
attr_kwargs = attributes_kwargs(plan, value)
|
|
26
|
+
child_kwargs, children, delegates =
|
|
27
|
+
children_kwargs(model_class, plan, value, buckets)
|
|
28
|
+
plan[:collection_defaults].each do |name|
|
|
29
|
+
next if attr_kwargs.key?(name) || child_kwargs.key?(name)
|
|
30
|
+
|
|
31
|
+
child_kwargs[name] = []
|
|
32
|
+
end
|
|
33
|
+
instance = model_class.new(**attr_kwargs, **child_kwargs)
|
|
34
|
+
instance.lutaml_parent = parent if parent
|
|
35
|
+
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
36
|
+
instance.element_order = PlanOrder.build(node) if node && plan[:ordered]
|
|
37
|
+
children.each do |child|
|
|
38
|
+
child.lutaml_parent = instance
|
|
39
|
+
child.lutaml_root ||= instance.lutaml_root || instance
|
|
40
|
+
end
|
|
41
|
+
interpret_deferred(model_class, plan, value, instance, node, buckets)
|
|
42
|
+
route_delegates(delegates, instance)
|
|
43
|
+
instance
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def register
|
|
49
|
+
Lutaml::Model::Config.default_register
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def attributes_kwargs(plan, value)
|
|
53
|
+
kwargs = {}
|
|
54
|
+
plan[:attr_rows].each do |rule, attr|
|
|
55
|
+
v = value.attribute(rule.name.to_s)
|
|
56
|
+
next if v.nil?
|
|
57
|
+
|
|
58
|
+
v = v.split(rule.delimiter) if rule.delimiter
|
|
59
|
+
if rule.as_list && rule.as_list[:import]
|
|
60
|
+
v = rule.as_list[:import].call(v)
|
|
61
|
+
end
|
|
62
|
+
kwargs[attr.name.to_sym] = v
|
|
63
|
+
end
|
|
64
|
+
kwargs
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns [kwargs, hydrated_child_instances, delegate_values]
|
|
68
|
+
# — child instances come back so the caller can decorate
|
|
69
|
+
# parent/root links once the parent exists; delegate values
|
|
70
|
+
# wait for the instance (their target object must exist).
|
|
71
|
+
def children_kwargs(_model_class, plan, value, buckets = nil)
|
|
72
|
+
grouped = group_children(value)
|
|
73
|
+
buckets = nil unless buckets && plan[:needs_nodes]
|
|
74
|
+
kwargs = {}
|
|
75
|
+
children = []
|
|
76
|
+
delegates = []
|
|
77
|
+
spellings = Hash.new { |h, k| h[k] = [] }
|
|
78
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
79
|
+
case kind
|
|
80
|
+
when :scalar
|
|
81
|
+
# Raw passthrough: the model constructor is the single
|
|
82
|
+
# cast authority — pre-casting here doubled every cast.
|
|
83
|
+
# Class transforms still apply before assignment.
|
|
84
|
+
v = grouped.dig(rule.name.to_s, 0)&.string_value
|
|
85
|
+
unless v.nil?
|
|
86
|
+
v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
|
|
87
|
+
assign(kwargs, delegates, delegate, rule, attr, v)
|
|
88
|
+
end
|
|
89
|
+
when :raw, :custom_method, :polymorphic, :content_deferred
|
|
90
|
+
# interpreted post-instance (interpret_deferred)
|
|
91
|
+
when :content
|
|
92
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
93
|
+
content_runs(value))
|
|
94
|
+
when :ordered_deferred
|
|
95
|
+
# With a source node the child hydrates natively: one
|
|
96
|
+
# walk against its own node + element_order from the
|
|
97
|
+
# node's children. Without one (direct PlanHydrator
|
|
98
|
+
# use), the subtree fragment-parses interpretively.
|
|
99
|
+
if buckets
|
|
100
|
+
child_type = attr.type(register)
|
|
101
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
102
|
+
items = buckets.fetch(rule.name.to_s, []).map do |n|
|
|
103
|
+
call(child_type, child_plan,
|
|
104
|
+
child_plan[:descriptor].walk(n), node: n)
|
|
105
|
+
end
|
|
106
|
+
unless items.empty?
|
|
107
|
+
children.concat(items)
|
|
108
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
109
|
+
attr.collection? ? items : items.first)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
when :collection_cb
|
|
113
|
+
values = grouped[rule.name.to_s].to_a.map(&:string_value)
|
|
114
|
+
if rule.transform.is_a?(Class)
|
|
115
|
+
values = values.map { |v| rule.transform_value(attr, v, :from, :xml) }
|
|
116
|
+
end
|
|
117
|
+
unless values.empty?
|
|
118
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
119
|
+
end
|
|
120
|
+
when :collection_native
|
|
121
|
+
values = native_collection(value, rule.name.to_s)
|
|
122
|
+
unless values.nil?
|
|
123
|
+
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
124
|
+
end
|
|
125
|
+
when :spelling
|
|
126
|
+
spellings[[rule, attr]] << grouped[spelling.to_s].to_a
|
|
127
|
+
when :nested
|
|
128
|
+
child_type = attr.type(register)
|
|
129
|
+
child_plan = PlanCompiler.compile(child_type, register)
|
|
130
|
+
cursor = if child_plan[:needs_nodes] && buckets
|
|
131
|
+
buckets.fetch(rule.name.to_s, [])
|
|
132
|
+
end
|
|
133
|
+
items = grouped.fetch(rule.name.to_s, []).each_with_index
|
|
134
|
+
.map do |v, i|
|
|
135
|
+
call(child_type, child_plan, v,
|
|
136
|
+
node: cursor && cursor[i])
|
|
137
|
+
end
|
|
138
|
+
next if items.empty?
|
|
139
|
+
|
|
140
|
+
children.concat(items)
|
|
141
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
142
|
+
attr.collection? ? items : items.first)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
unless spellings.empty?
|
|
146
|
+
spellings.each do |(rule, attr), groups|
|
|
147
|
+
# Interpretive order for shared-attribute groups is
|
|
148
|
+
# spelling-group order (first spelling's matches, then
|
|
149
|
+
# the next's), not interleaved document order.
|
|
150
|
+
values = groups.compact.flatten.map(&:string_value)
|
|
151
|
+
next if values.empty?
|
|
152
|
+
|
|
153
|
+
delegate = delegate_of(plan, rule)
|
|
154
|
+
assign(kwargs, delegates, delegate, rule, attr,
|
|
155
|
+
attr.collection? ? values : values.first)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
[kwargs, children, delegates]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Deferred islands: raw subtrees become wrapper elements via a
|
|
162
|
+
# fragment parse, then the interpretive machinery runs on just
|
|
163
|
+
# that island — custom method invocation with an
|
|
164
|
+
# element-shaped argument, or the polymorphic/union cast.
|
|
165
|
+
# With the source node available the island wraps the ORIGINAL
|
|
166
|
+
# node (no fragment parse — the reparse dominated deferred
|
|
167
|
+
# hydration). Ordered children only land here without a node.
|
|
168
|
+
def interpret_deferred(model_class, plan, value, instance, node = nil,
|
|
169
|
+
buckets = nil)
|
|
170
|
+
plan[:rows].each do |rule, attr, kind, _spelling, _delegate|
|
|
171
|
+
case kind
|
|
172
|
+
when :content_deferred
|
|
173
|
+
runs = content_runs(value)
|
|
174
|
+
next if runs.empty?
|
|
175
|
+
|
|
176
|
+
# Non-collection content attrs hold the joined text
|
|
177
|
+
# (the interpretive path assigns element text, the runs
|
|
178
|
+
# concatenated).
|
|
179
|
+
instance.public_send(:"#{attr.name}=", runs.join)
|
|
180
|
+
when :raw
|
|
181
|
+
raws = raw_strings(value, rule)
|
|
182
|
+
next if raws.empty?
|
|
183
|
+
|
|
184
|
+
instance.public_send(:"#{attr.name}=",
|
|
185
|
+
attr.collection? ? raws : raws.first)
|
|
186
|
+
when :custom_method
|
|
187
|
+
elements = deferred_elements(value, rule, buckets)
|
|
188
|
+
next if elements.empty?
|
|
189
|
+
|
|
190
|
+
args = attr.collection? ? elements : elements.first
|
|
191
|
+
rule.deserialize(instance, args,
|
|
192
|
+
model_class.attributes(register),
|
|
193
|
+
model_class)
|
|
194
|
+
when :ordered_deferred
|
|
195
|
+
next if node # hydrated natively in children_kwargs
|
|
196
|
+
|
|
197
|
+
results = deferred_elements(value, rule, buckets).map do |element|
|
|
198
|
+
attr.cast(element, :xml, register,
|
|
199
|
+
lutaml_parent: instance,
|
|
200
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
201
|
+
end
|
|
202
|
+
next if results.empty?
|
|
203
|
+
|
|
204
|
+
instance.public_send(:"#{attr.name}=",
|
|
205
|
+
attr.collection? ? results : results.first)
|
|
206
|
+
when :polymorphic
|
|
207
|
+
results = deferred_elements(value, rule, buckets).map do |element|
|
|
208
|
+
attr.cast(element, :xml, register,
|
|
209
|
+
polymorphic: rule.polymorphic,
|
|
210
|
+
lutaml_parent: instance,
|
|
211
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
212
|
+
end
|
|
213
|
+
next if results.empty?
|
|
214
|
+
|
|
215
|
+
instance.public_send(:"#{attr.name}=",
|
|
216
|
+
attr.collection? ? results : results.first)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Wrapper elements for deferred rows: the ORIGINAL source
|
|
222
|
+
# nodes bridged through moxml's wrapper cache (wrapper
|
|
223
|
+
# construction only, no parse), falling back to a fragment
|
|
224
|
+
# parse of the captured subtree when no node is available.
|
|
225
|
+
# Both lists are document-ordered for the row's name.
|
|
226
|
+
def deferred_elements(value, rule, buckets)
|
|
227
|
+
nodes = buckets && buckets[rule.name.to_s]
|
|
228
|
+
return nodes.map { |n| bridge_element(n) } if nodes
|
|
229
|
+
|
|
230
|
+
raw_strings(value, rule).map { |raw| fragment_element(raw) }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def bridge_element(node)
|
|
234
|
+
Lutaml::Xml::LeptrisElement.new(
|
|
235
|
+
Moxml::Node.wrap(node, bridge_context),
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def bridge_context
|
|
240
|
+
@bridge_context ||= Moxml::Context.new(:leptris)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def route_delegates(delegates, instance)
|
|
244
|
+
delegates.each do |rule, delegate_attr, attr, v|
|
|
245
|
+
target = instance.public_send(rule.delegate)
|
|
246
|
+
unless target
|
|
247
|
+
# the interpretive path instantiates absent delegate
|
|
248
|
+
# targets; mirror it
|
|
249
|
+
target = delegate_attr.type(register).new
|
|
250
|
+
instance.public_send(:"#{rule.delegate}=", target)
|
|
251
|
+
end
|
|
252
|
+
target.public_send(:"#{attr.name}=", v)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def delegate_of(plan, rule)
|
|
257
|
+
entry = plan[:rows].find { |r, _, _k, _| r.equal?(rule) }
|
|
258
|
+
entry && entry[4]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Delegate rules hold their values for post-instance routing
|
|
262
|
+
# (the target object must exist first); plain rules land in
|
|
263
|
+
# kwargs directly.
|
|
264
|
+
def assign(kwargs, delegates, delegate, rule, attr, value)
|
|
265
|
+
if delegate
|
|
266
|
+
delegates << [rule, delegate, attr, value]
|
|
267
|
+
else
|
|
268
|
+
kwargs[attr.name.to_sym] = value
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def content_runs(value)
|
|
273
|
+
out = []
|
|
274
|
+
value.count.times do |i|
|
|
275
|
+
c = value.at(i)
|
|
276
|
+
next unless c.name.nil? && c.kind == :collection
|
|
277
|
+
|
|
278
|
+
out.concat(Array.new(c.count) { |j| c.at(j).string_value })
|
|
279
|
+
end
|
|
280
|
+
out
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Native collection rows echo their producing row's name
|
|
284
|
+
# (leptris 1.9.178).
|
|
285
|
+
def native_collection(value, name)
|
|
286
|
+
value.count.times do |i|
|
|
287
|
+
c = value.at(i)
|
|
288
|
+
next unless c.kind == :collection && c.name == name
|
|
289
|
+
|
|
290
|
+
return Array.new(c.count) { |j| c.at(j).string_value }
|
|
291
|
+
end
|
|
292
|
+
nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def raw_strings(value, rule)
|
|
296
|
+
out = []
|
|
297
|
+
value.count.times do |idx|
|
|
298
|
+
child = value.at(idx)
|
|
299
|
+
if child.name == rule.name.to_s && child.kind == :raw
|
|
300
|
+
out << child.string_value
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
out
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Fragment deferral: parse the captured subtree back into a
|
|
307
|
+
# wrapper element. Ancestor namespace context is absent (the
|
|
308
|
+
# compiler only defers on namespace-free model chains).
|
|
309
|
+
def fragment_element(raw)
|
|
310
|
+
Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def group_children(value)
|
|
314
|
+
grouped = {}
|
|
315
|
+
value.count.times do |i|
|
|
316
|
+
child = value.at(i)
|
|
317
|
+
next if child.name.nil? # content runs, read separately
|
|
318
|
+
|
|
319
|
+
(grouped[child.name] ||= []) << child
|
|
320
|
+
end
|
|
321
|
+
grouped
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Element children bucketed by local name, document order
|
|
325
|
+
# preserved — the node-side mirror of group_children for
|
|
326
|
+
# ordered-child hydration. Only built when the plan's subtree
|
|
327
|
+
# needs source nodes; namespace-qualified models never get
|
|
328
|
+
# here (the compiler keeps them interpretive).
|
|
329
|
+
def element_buckets(node)
|
|
330
|
+
buckets = {}
|
|
331
|
+
node.children.each do |child|
|
|
332
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
333
|
+
|
|
334
|
+
(buckets[child.name] ||= []) << child
|
|
335
|
+
end
|
|
336
|
+
buckets
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# Reconstructs element_order for ordered/mixed models from the
|
|
6
|
+
# leptris node surface in one native pass, mirroring the
|
|
7
|
+
# interpretive AdapterElement#order contract: text runs, CDATA,
|
|
8
|
+
# comments, PIs, and element entries (local name + namespace
|
|
9
|
+
# uri/prefix) in document order, frozen like the interpretive
|
|
10
|
+
# product.
|
|
11
|
+
module PlanOrder
|
|
12
|
+
TEXT_MARKER = "text"
|
|
13
|
+
CDATA_MARKER = "#cdata-section"
|
|
14
|
+
COMMENT_MARKER = "comment"
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def build(node)
|
|
18
|
+
node.children.filter_map { |child| entry_for(child) }
|
|
19
|
+
.each(&:freeze).freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def entry_for(child)
|
|
25
|
+
case child
|
|
26
|
+
when ::Leptris::XML::CDATA
|
|
27
|
+
Element.new("Text", CDATA_MARKER,
|
|
28
|
+
text_content: child.content, node_type: :cdata)
|
|
29
|
+
when ::Leptris::XML::Text
|
|
30
|
+
Element.new("Text", TEXT_MARKER,
|
|
31
|
+
text_content: child.text, node_type: :text)
|
|
32
|
+
when ::Leptris::XML::Comment
|
|
33
|
+
Element.new("Comment", COMMENT_MARKER,
|
|
34
|
+
text_content: child.content, node_type: :comment)
|
|
35
|
+
when ::Leptris::XML::ProcessingInstruction
|
|
36
|
+
Element.new("ProcessingInstruction", child.name,
|
|
37
|
+
text_content: child.content.to_s.sub(/\A\s+/, ""),
|
|
38
|
+
node_type: :processing_instruction)
|
|
39
|
+
when ::Leptris::XML::Element
|
|
40
|
+
ns = child.namespace
|
|
41
|
+
Element.new("Element", child.name,
|
|
42
|
+
node_type: :element,
|
|
43
|
+
namespace_uri: ns&.values&.first,
|
|
44
|
+
namespace_prefix: ns&.keys&.first)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|