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.
@@ -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
@@ -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