lutaml-model 0.8.56 → 0.8.106

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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/scripts/test_installed_gem.rb +58 -0
  3. data/.github/workflows/downstream-performance.yml +28 -3
  4. data/.github/workflows/js-pr-check.yml +4 -0
  5. data/.github/workflows/js-sync.yml +4 -0
  6. data/.github/workflows/opal.yml +9 -0
  7. data/.github/workflows/rake.yml +6 -41
  8. data/.github/workflows/release.yml +41 -0
  9. data/Gemfile +17 -3
  10. data/bench/bench_alloc_audit.rb +56 -0
  11. data/bench/bench_common.rb +11 -0
  12. data/bench/bench_merge_attempts.rb +49 -0
  13. data/docs/_guides/gem-integration.adoc +85 -0
  14. data/docs/_guides/grammar-backed-formats.adoc +69 -0
  15. data/docs/_guides/index.adoc +8 -1
  16. data/docs/_guides/native-engines.adoc +23 -5
  17. data/docs/_guides/schema-generation.adoc +25 -0
  18. data/docs/_guides/type-registers-contexts.adoc +126 -0
  19. data/lib/compat/opal/lutaml_model_boot.rb +7 -5
  20. data/lib/lutaml/json/adapter/yeptris_adapter.rb +24 -1
  21. data/lib/lutaml/key_value/data_model/element.rb +5 -1
  22. data/lib/lutaml/key_value/mapping_rule.rb +7 -0
  23. data/lib/lutaml/key_value/transform.rb +29 -24
  24. data/lib/lutaml/key_value/transformation/collection_serializer.rb +12 -29
  25. data/lib/lutaml/key_value/transformation/value_serializer.rb +29 -6
  26. data/lib/lutaml/key_value/transformation.rb +64 -116
  27. data/lib/lutaml/model/adapter_resolver.rb +7 -3
  28. data/lib/lutaml/model/attribute.rb +74 -15
  29. data/lib/lutaml/model/cached_type_resolver.rb +13 -6
  30. data/lib/lutaml/model/configuration.rb +13 -0
  31. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  32. data/lib/lutaml/model/mapping/mapping_rule.rb +6 -1
  33. data/lib/lutaml/model/render_policy.rb +26 -8
  34. data/lib/lutaml/model/schema/base_schema.rb +15 -0
  35. data/lib/lutaml/model/schema/generator/definition.rb +52 -8
  36. data/lib/lutaml/model/schema/generator/definitions_collection.rb +35 -14
  37. data/lib/lutaml/model/schema.rb +4 -2
  38. data/lib/lutaml/model/serializable.rb +33 -0
  39. data/lib/lutaml/model/serialize/attribute_definition.rb +196 -2
  40. data/lib/lutaml/model/serialize/builder.rb +22 -0
  41. data/lib/lutaml/model/serialize/conversion_caching.rb +5 -0
  42. data/lib/lutaml/model/serialize/format_conversion.rb +49 -41
  43. data/lib/lutaml/model/serialize/initialization.rb +54 -7
  44. data/lib/lutaml/model/serialize/model_import.rb +60 -0
  45. data/lib/lutaml/model/serialize.rb +14 -6
  46. data/lib/lutaml/model/type/time.rb +6 -0
  47. data/lib/lutaml/model/type/value.rb +9 -7
  48. data/lib/lutaml/model/type_resolver.rb +7 -5
  49. data/lib/lutaml/model/utils.rb +18 -5
  50. data/lib/lutaml/model/version.rb +1 -1
  51. data/lib/lutaml/model.rb +7 -0
  52. data/lib/lutaml/xml/adapter/xml_parser.rb +5 -2
  53. data/lib/lutaml/xml/adapter_loader.rb +5 -2
  54. data/lib/lutaml/xml/format.rb +18 -16
  55. data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
  56. data/lib/lutaml/xml/leptris/plan_compiler.rb +650 -0
  57. data/lib/lutaml/xml/leptris/plan_hydrator.rb +748 -0
  58. data/lib/lutaml/xml/leptris/plan_order.rb +55 -0
  59. data/lib/lutaml/xml/leptris/plan_serializer.rb +702 -0
  60. data/lib/lutaml/xml/leptris/plan_walk.rb +36 -0
  61. data/lib/lutaml/xml/leptris.rb +46 -0
  62. data/lib/lutaml/xml/mapping.rb +71 -18
  63. data/lib/lutaml/xml/model_transform.rb +231 -8
  64. data/lib/lutaml/xml/namespace.rb +18 -8
  65. data/lib/lutaml/xml/namespace_collector.rb +17 -1
  66. data/lib/lutaml/xml/namespace_needs.rb +77 -49
  67. data/lib/lutaml/xml/parse_session.rb +7 -19
  68. data/lib/lutaml/xml/schema/xsd/schema.rb +2 -1
  69. data/lib/lutaml/xml/serialization/format_conversion.rb +5 -1
  70. data/lib/lutaml/xml/transformation/ordered_applier.rb +21 -15
  71. data/lib/lutaml/xml/transformation.rb +15 -1
  72. data/lib/lutaml/xml/type/serializers.rb +8 -1
  73. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +34 -2
  74. data/lib/tasks/performance_probe_plan_walk.rb +1 -1
  75. data/lutaml-model.gemspec +7 -4
  76. data/spec/lutaml/json/yeptris_adapter_spec.rb +7 -2
  77. data/spec/lutaml/key_value/transformation/value_serializer_spec.rb +9 -5
  78. data/spec/lutaml/key_value/transformation_spec.rb +87 -21
  79. data/spec/lutaml/model/any_element_mapping_spec.rb +41 -0
  80. data/spec/lutaml/model/collection_validation_spec.rb +265 -0
  81. data/spec/lutaml/model/desc_spec.rb +50 -0
  82. data/spec/lutaml/model/late_attribute_declaration_spec.rb +35 -0
  83. data/spec/lutaml/model/model_import_reentry_spec.rb +46 -0
  84. data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
  85. data/spec/lutaml/model/schema/generate_many_spec.rb +61 -0
  86. data/spec/lutaml/model/schema/generation_regressions_spec.rb +139 -0
  87. data/spec/lutaml/model/schema/json_schema_spec.rb +1 -13
  88. data/spec/lutaml/model/schema/yaml_schema_spec.rb +0 -11
  89. data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
  90. data/spec/lutaml/model/store_spec.rb +6 -2
  91. data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
  92. data/spec/lutaml/model/writer_assigned_attribute_spec.rb +62 -0
  93. data/spec/lutaml/model/yeptris_error_parity_spec.rb +95 -0
  94. data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
  95. data/spec/lutaml/xml/adapter/over_cap_parse_spec.rb +105 -0
  96. data/spec/lutaml/xml/adapter_loader_spec.rb +14 -0
  97. data/spec/lutaml/xml/adapter_metadata_default_spec.rb +16 -0
  98. data/spec/lutaml/xml/attribute_ambiguity_surface_spec.rb +5 -0
  99. data/spec/lutaml/xml/clear_parse_state_spec.rb +6 -1
  100. data/spec/lutaml/xml/compiled_rule_namespace_spec.rb +5 -0
  101. data/spec/lutaml/xml/cross_register_plan_spec.rb +75 -0
  102. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +16 -8
  103. data/spec/lutaml/xml/element_order_tracking_spec.rb +37 -0
  104. data/spec/lutaml/xml/leptris_string_encoding_spec.rb +65 -0
  105. data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
  106. data/spec/lutaml/xml/parse_dom_retention_spec.rb +34 -0
  107. data/spec/lutaml/xml/plan_fast_path_spec.rb +122 -14
  108. data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
  109. data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
  110. data/spec/lutaml/xml/plan_partition_spec.rb +1 -1
  111. data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +131 -0
  112. data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
  113. data/spec/lutaml/xml/plan_walk_spec.rb +6 -6
  114. data/spec/lutaml/xml/plan_wildcard_any_spec.rb +96 -0
  115. data/spec/lutaml/yaml/yeptris_adapter_spec.rb +29 -0
  116. data/spec/person_spec.rb +8 -0
  117. metadata +48 -17
  118. data/lib/lutaml/xml/plan_compiler.rb +0 -332
  119. data/lib/lutaml/xml/plan_hydrator.rb +0 -367
  120. data/lib/lutaml/xml/plan_order.rb +0 -53
  121. data/lib/lutaml/xml/plan_serializer.rb +0 -252
  122. data/lib/lutaml/xml/plan_walk.rb +0 -30
@@ -1,367 +0,0 @@
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
- # TODO.max-perf/33: the bulk constructor — allocate +
34
- # compiled writers, from_hash semantics — measured ~30x on
35
- # hydration-heavy corpora versus the generic constructor.
36
- instance = model_class.instantiate(attr_kwargs.merge(child_kwargs),
37
- register)
38
- instance.lutaml_parent = parent if parent
39
- instance.lutaml_root ||= parent&.lutaml_root || parent
40
- instance.element_order = PlanOrder.build(node) if node && plan[:ordered]
41
- children.each do |child|
42
- child.lutaml_parent = instance
43
- child.lutaml_root ||= instance.lutaml_root || instance
44
- end
45
- interpret_deferred(model_class, plan, value, instance, node, buckets)
46
- route_delegates(delegates, instance)
47
- instance
48
- end
49
-
50
- private
51
-
52
- def register
53
- Lutaml::Model::Config.default_register
54
- end
55
-
56
- def attributes_kwargs(plan, value)
57
- kwargs = {}
58
- plan[:attr_rows].each do |rule, attr|
59
- v = value.attribute(rule.name.to_s)
60
- next if v.nil?
61
-
62
- v = v.split(rule.delimiter) if rule.delimiter
63
- if rule.as_list && rule.as_list[:import]
64
- v = rule.as_list[:import].call(v)
65
- end
66
- kwargs[attr.name.to_sym] = v
67
- end
68
- kwargs
69
- end
70
-
71
- # Returns [kwargs, hydrated_child_instances, delegate_values]
72
- # — child instances come back so the caller can decorate
73
- # parent/root links once the parent exists; delegate values
74
- # wait for the instance (their target object must exist).
75
- def children_kwargs(_model_class, plan, value, buckets = nil)
76
- grouped, tagged = group_children(value, plan[:row_tags])
77
- buckets = nil unless buckets && plan[:needs_nodes]
78
- kwargs = {}
79
- children = []
80
- delegates = []
81
- spellings = Hash.new { |h, k| h[k] = [] }
82
- row_tags = plan[:row_tags]
83
- plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate), idx|
84
- tag = row_tags&.[](idx)
85
- case kind
86
- when :scalar
87
- # Raw passthrough: the model constructor is the single
88
- # cast authority — pre-casting here doubled every cast.
89
- # Class transforms still apply before assignment.
90
- if tag && (vals = tagged[tag]) && vals.size > 1
91
- # Interpretive parity: several captures into a
92
- # non-collection attribute arrive as the full array.
93
- v = vals.map(&:string_value)
94
- else
95
- first = if tag
96
- tagged[tag]&.first
97
- else
98
- grouped.dig(rule.name.to_s, 0)
99
- end
100
- v = first&.string_value
101
- end
102
- unless v.nil?
103
- v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
104
- assign(kwargs, delegates, delegate, rule, attr, v)
105
- end
106
- when :raw, :custom_method, :polymorphic, :content_deferred
107
- # interpreted post-instance (interpret_deferred)
108
- when :content
109
- assign(kwargs, delegates, delegate, rule, attr,
110
- content_runs(value))
111
- when :ordered_deferred
112
- # With a source node the child hydrates natively: one
113
- # walk against its own node + element_order from the
114
- # node's children. Without one (direct PlanHydrator
115
- # use), the subtree fragment-parses interpretively.
116
- if buckets
117
- child_type = attr.type(register)
118
- child_plan = PlanCompiler.compile(child_type, register)
119
- items = buckets.fetch(rule.name.to_s, []).map do |n|
120
- call(child_type, child_plan,
121
- child_plan[:descriptor].walk(n), node: n)
122
- end
123
- unless items.empty?
124
- children.concat(items)
125
- assign(kwargs, delegates, delegate, rule, attr,
126
- attr.collection? ? items : items.first)
127
- end
128
- end
129
- when :collection_cb
130
- values = grouped[rule.name.to_s].to_a.map(&:string_value)
131
- if rule.transform.is_a?(Class)
132
- values = values.map { |v| rule.transform_value(attr, v, :from, :xml) }
133
- end
134
- unless values.empty?
135
- assign(kwargs, delegates, delegate, rule, attr, values)
136
- end
137
- when :collection_native
138
- values = if tag
139
- tagged[tag].to_a.map(&:string_value)
140
- else
141
- native_collection(value, rule.name.to_s)
142
- end
143
- unless values.nil?
144
- assign(kwargs, delegates, delegate, rule, attr, values)
145
- end
146
- when :spelling
147
- spellings[[rule, attr]] << grouped[spelling.to_s].to_a
148
- when :nested
149
- child_type = attr.type(register)
150
- child_plan = PlanCompiler.compile(child_type, register)
151
- cursor = if child_plan[:needs_nodes] && buckets
152
- buckets.fetch(rule.name.to_s, [])
153
- end
154
- items = grouped.fetch(rule.name.to_s, []).each_with_index
155
- .map do |v, i|
156
- call(child_type, child_plan, v,
157
- node: cursor && cursor[i])
158
- end
159
- next if items.empty?
160
-
161
- children.concat(items)
162
- assign(kwargs, delegates, delegate, rule, attr,
163
- attr.collection? ? items : items.first)
164
- end
165
- end
166
- unless spellings.empty?
167
- spellings.each do |(rule, attr), groups|
168
- # Interpretive order for shared-attribute groups is
169
- # spelling-group order (first spelling's matches, then
170
- # the next's), not interleaved document order.
171
- values = groups.compact.flatten.map(&:string_value)
172
- next if values.empty?
173
-
174
- delegate = delegate_of(plan, rule)
175
- assign(kwargs, delegates, delegate, rule, attr,
176
- attr.collection? ? values : values.first)
177
- end
178
- end
179
- [kwargs, children, delegates]
180
- end
181
-
182
- # Deferred islands: raw subtrees become wrapper elements via a
183
- # fragment parse, then the interpretive machinery runs on just
184
- # that island — custom method invocation with an
185
- # element-shaped argument, or the polymorphic/union cast.
186
- # With the source node available the island wraps the ORIGINAL
187
- # node (no fragment parse — the reparse dominated deferred
188
- # hydration). Ordered children only land here without a node.
189
- def interpret_deferred(model_class, plan, value, instance, node = nil,
190
- buckets = nil)
191
- plan[:rows].each do |rule, attr, kind, _spelling, _delegate|
192
- case kind
193
- when :content_deferred
194
- runs = content_runs(value)
195
- next if runs.empty?
196
-
197
- # Non-collection content attrs hold the joined text
198
- # (the interpretive path assigns element text, the runs
199
- # concatenated).
200
- instance.public_send(:"#{attr.name}=", runs.join)
201
- when :raw
202
- raws = raw_strings(value, rule)
203
- next if raws.empty?
204
-
205
- instance.public_send(:"#{attr.name}=",
206
- attr.collection? ? raws : raws.first)
207
- when :custom_method
208
- elements = deferred_elements(value, rule, buckets)
209
- next if elements.empty?
210
-
211
- args = attr.collection? ? elements : elements.first
212
- rule.deserialize(instance, args,
213
- model_class.attributes(register),
214
- model_class)
215
- when :ordered_deferred
216
- next if node # hydrated natively in children_kwargs
217
-
218
- results = deferred_elements(value, rule, buckets).map do |element|
219
- attr.cast(element, :xml, register,
220
- lutaml_parent: instance,
221
- lutaml_root: instance.lutaml_root || instance)
222
- end
223
- next if results.empty?
224
-
225
- instance.public_send(:"#{attr.name}=",
226
- attr.collection? ? results : results.first)
227
- when :polymorphic
228
- results = deferred_elements(value, rule, buckets).map do |element|
229
- attr.cast(element, :xml, register,
230
- polymorphic: rule.polymorphic,
231
- lutaml_parent: instance,
232
- lutaml_root: instance.lutaml_root || instance)
233
- end
234
- next if results.empty?
235
-
236
- instance.public_send(:"#{attr.name}=",
237
- attr.collection? ? results : results.first)
238
- end
239
- end
240
- end
241
-
242
- # Wrapper elements for deferred rows: the ORIGINAL source
243
- # nodes bridged through moxml's wrapper cache (wrapper
244
- # construction only, no parse), falling back to a fragment
245
- # parse of the captured subtree when no node is available.
246
- # Both lists are document-ordered for the row's name.
247
- def deferred_elements(value, rule, buckets)
248
- nodes = buckets && buckets[rule.name.to_s]
249
- return nodes.map { |n| bridge_element(n) } if nodes
250
-
251
- raw_strings(value, rule).map { |raw| fragment_element(raw) }
252
- end
253
-
254
- def bridge_element(node)
255
- Lutaml::Xml::LeptrisElement.new(
256
- Moxml::Node.wrap(node, bridge_context),
257
- )
258
- end
259
-
260
- def bridge_context
261
- @bridge_context ||= Moxml::Context.new(:leptris)
262
- end
263
-
264
- def route_delegates(delegates, instance)
265
- delegates.each do |rule, delegate_attr, attr, v|
266
- target = instance.public_send(rule.delegate)
267
- unless target
268
- # the interpretive path instantiates absent delegate
269
- # targets; mirror it
270
- target = delegate_attr.type(register).new
271
- instance.public_send(:"#{rule.delegate}=", target)
272
- end
273
- target.public_send(:"#{attr.name}=", v)
274
- end
275
- end
276
-
277
- def delegate_of(plan, rule)
278
- entry = plan[:rows].find { |r, _, _k, _| r.equal?(rule) }
279
- entry && entry[4]
280
- end
281
-
282
- # Delegate rules hold their values for post-instance routing
283
- # (the target object must exist first); plain rules land in
284
- # kwargs directly.
285
- def assign(kwargs, delegates, delegate, rule, attr, value)
286
- if delegate
287
- delegates << [rule, delegate, attr, value]
288
- else
289
- kwargs[attr.name.to_sym] = value
290
- end
291
- end
292
-
293
- def content_runs(value)
294
- out = []
295
- value.count.times do |i|
296
- c = value.at(i)
297
- next unless c.name.nil? && c.kind == :collection
298
-
299
- out.concat(Array.new(c.count) { |j| c.at(j).string_value })
300
- end
301
- out
302
- end
303
-
304
- # Native collection rows echo their producing row's name
305
- # (leptris 1.9.178).
306
- def native_collection(value, name)
307
- value.count.times do |i|
308
- c = value.at(i)
309
- next unless c.kind == :collection && c.name == name
310
-
311
- return Array.new(c.count) { |j| c.at(j).string_value }
312
- end
313
- nil
314
- end
315
-
316
- def raw_strings(value, rule)
317
- out = []
318
- value.count.times do |idx|
319
- child = value.at(idx)
320
- if child.name == rule.name.to_s && child.kind == :raw
321
- out << child.string_value
322
- end
323
- end
324
- out
325
- end
326
-
327
- # Fragment deferral: parse the captured subtree back into a
328
- # wrapper element. Ancestor namespace context is absent (the
329
- # compiler only defers on namespace-free model chains).
330
- def fragment_element(raw)
331
- Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
332
- end
333
-
334
- def group_children(value, row_tags = nil)
335
- grouped = {}
336
- tagged = {}
337
- want_tags = !row_tags.nil?
338
- value.count.times do |i|
339
- child = value.at(i)
340
- next if child.name.nil? # content runs, read separately
341
-
342
- (grouped[child.name] ||= []) << child
343
- if want_tags && child.type_tag != 0
344
- (tagged[child.type_tag] ||= []) << child
345
- end
346
- end
347
- [grouped, tagged]
348
- end
349
-
350
- # Element children bucketed by local name, document order
351
- # preserved — the node-side mirror of group_children for
352
- # ordered-child hydration. Only built when the plan's subtree
353
- # needs source nodes; namespace-qualified models never get
354
- # here (the compiler keeps them interpretive).
355
- def element_buckets(node)
356
- buckets = {}
357
- node.children.each do |child|
358
- next unless child.is_a?(::Leptris::XML::Element)
359
-
360
- (buckets[child.name] ||= []) << child
361
- end
362
- buckets
363
- end
364
- end
365
- end
366
- end
367
- end
@@ -1,53 +0,0 @@
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
- # Leptris::XML::Namespace is a single object (href/prefix),
42
- # not the Nokogiri-style prefix hash — hash accessors blow
43
- # up ordered+namespaced plan parses.
44
- Element.new("Element", child.name,
45
- node_type: :element,
46
- namespace_uri: ns&.href,
47
- namespace_prefix: ns&.prefix)
48
- end
49
- end
50
- end
51
- end
52
- end
53
- end
@@ -1,252 +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)
29
- return nil if plan[:ordered] && instance.element_order.nil?
30
-
31
- doc = ::Leptris::XML::Document.create
32
- root = doc.create_element(plan[:tree][:name])
33
- doc.root = root
34
- if plan[:ordered]
35
- build_ordered(root, instance, plan, doc)
36
- else
37
- build(root, instance, plan, doc)
38
- end
39
- doc.to_xml(indent: 2, no_decl: true)
40
- end
41
-
42
- # Whether the compiled plan is serialize-shaped. Ordered/mixed
43
- # models serialize through build_ordered from element_order;
44
- # plain models through plan rows.
45
- def serializable?(plan)
46
- plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
47
- SERIALIZABLE_KINDS.include?(kind)
48
- end
49
- end
50
-
51
- private
52
-
53
- def register
54
- Lutaml::Model::Config.default_register
55
- end
56
-
57
- def build(element, instance, plan, doc)
58
- write_attributes(element, instance, plan)
59
- plan[:rows].each do |rule, attr, kind, spelling, delegate|
60
- value = value_of(instance, attr, delegate)
61
- next unless rule.render?(value, instance)
62
-
63
- case kind
64
- when :scalar
65
- # Multi-capture parity: an attribute that collected
66
- # several occurrences serializes one element per item,
67
- # as the interpretive writer does.
68
- Array(value).each do |item|
69
- add_leaf(element, row_name(rule, spelling),
70
- attr.serialize(item, :xml, register), doc,
71
- attrs: rule.when_attribute)
72
- end
73
- when :collection_native, :collection_cb
74
- Array(value).each do |item|
75
- add_leaf(element, rule.name.to_s,
76
- attr.serialize(item, :xml, register), doc,
77
- attrs: rule.when_attribute)
78
- end
79
- when :nested, :ordered_deferred
80
- Array(value).each do |item|
81
- child_plan = PlanCompiler.compile(item.class, register)
82
- child = element.create_child(child_plan[:tree][:name])
83
- if child_plan[:ordered]
84
- return nil unless build_ordered(child, item, child_plan, doc)
85
-
86
- else
87
- build(child, item, child_plan, doc)
88
- end
89
- end
90
- when :raw
91
- Array(value).each { |raw| element.add_child(raw.to_s) }
92
- when :content, :content_deferred
93
- Array(value).each { |run| element.add_child(doc.create_text_node(run.to_s)) }
94
- end
95
- end
96
- end
97
-
98
- # Element_order-driven emission (the interpretive order
99
- # applier's contract): text runs and comments verbatim, each
100
- # element entry consuming the next item of its collection
101
- # (document order). Text reads prefer the content attribute
102
- # when it lines up with the text-node count, so mutations to
103
- # the model are reflected; element_order text is the fallback.
104
- # PIs drop (interpretive parity). Returns nil when a nested
105
- # ordered child lacks element_order (caller falls back).
106
- def build_ordered(element, instance, plan, doc)
107
- write_attributes(element, instance, plan)
108
- rows_by_name = {}
109
- plan[:rows].each do |rule, attr, kind, spelling, delegate|
110
- next unless rule.name
111
-
112
- rows_by_name[rule.name.to_s] ||= [rule, attr, kind, spelling,
113
- delegate]
114
- end
115
- content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
116
- CONTENT_KINDS.include?(kind) && rule.name.nil?
117
- end
118
- order = instance.element_order
119
- element_indices = ::Hash.new(0)
120
- content_attr = content_entry && content_entry[1]
121
- content_value = content_attr &&
122
- value_of(instance, content_attr, content_entry[4])
123
- text_node_count = order.count { |o| o.type == "Text" }
124
- use_content_index = content_value.is_a?(Array) &&
125
- content_value.length == text_node_count
126
- content_cdata = content_entry && content_entry[0].cdata
127
- text_node_index = 0
128
-
129
- order.each do |object|
130
- case object.type
131
- when "Text"
132
- text = if content_attr && !content_value.nil?
133
- if use_content_index
134
- content_value[text_node_index].to_s
135
- elsif !content_value.is_a?(Array) && text_node_count <= 1
136
- content_value.to_s
137
- else
138
- object.text_content || object.name
139
- end
140
- else
141
- object.text_content || object.name
142
- end
143
- text_node_index += 1
144
- next if text.nil?
145
- # Mixed content keeps whitespace; ordered-only skips it
146
- next if content_attr.nil? && text.strip.empty?
147
-
148
- element.add_child(content_cdata ? doc.create_cdata(text) : doc.create_text_node(text))
149
- when "Comment"
150
- element.add_child(doc.create_comment(object.text_content))
151
- when "Element"
152
- entry = rows_by_name[object.name]
153
- next unless entry
154
-
155
- _, attr, kind, _spelling, delegate = entry
156
- value = value_of(instance, attr, delegate)
157
- case kind
158
- when :collection_native, :collection_cb
159
- index = element_indices[object.name]
160
- items = Array(value)
161
- next unless index < items.length
162
-
163
- element_indices[object.name] += 1
164
- add_leaf(element, object.name,
165
- attr.serialize(items[index], :xml, register), doc)
166
- when :nested, :ordered_deferred
167
- item = if attr.collection?
168
- index = element_indices[object.name]
169
- element_indices[object.name] += 1
170
- Array(value)[index]
171
- else
172
- value
173
- end
174
- next unless item
175
-
176
- child_plan = PlanCompiler.compile(item.class, register)
177
- child = element.create_child(child_plan[:tree][:name])
178
- if child_plan[:ordered]
179
- return nil unless build_ordered(child, item, child_plan, doc)
180
- else
181
- build(child, item, child_plan, doc)
182
- end
183
- when :raw
184
- element.add_child(value.to_s) unless value.nil?
185
- when :scalar
186
- add_leaf(element, object.name,
187
- attr.serialize(value, :xml, register), doc)
188
- end
189
- end
190
- end
191
- true
192
- end
193
-
194
- # Attributes in document order when the instance recorded it
195
- # (parsed models), else plan row order.
196
- def write_attributes(element, instance, plan)
197
- recorded = instance.attribute_order
198
- if recorded && !recorded.empty?
199
- by_name = {}
200
- plan[:attr_rows].each do |rule, attr, _sp, delegate|
201
- by_name[rule.name.to_s] = [rule, attr, delegate]
202
- end
203
- names = recorded |
204
- plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
205
- names.each do |name|
206
- write_attribute(element, instance, name, by_name[name])
207
- end
208
- else
209
- plan[:attr_rows].each do |rule, attr, _sp, delegate|
210
- write_attribute(element, instance, rule.name.to_s,
211
- [rule, attr, delegate])
212
- end
213
- end
214
- end
215
-
216
- def write_attribute(element, instance, name, entry)
217
- return unless entry
218
-
219
- _rule, attr, delegate = entry
220
- value = value_of(instance, attr, delegate)
221
- return if value.nil?
222
-
223
- element.set_attribute(name,
224
- attr.serialize(value, :xml, register).to_s)
225
- end
226
-
227
- # Partition rows (#88) re-emit their discriminator: the wire
228
- # element carries the when_attribute pairs so the round trip
229
- # re-routes to the same attribute on reparse.
230
- def add_leaf(element, name, value, doc, attrs: nil)
231
- child = element.create_child(name)
232
- attrs&.each { |k, v| child[k.to_s] = v.to_s }
233
- child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
234
- child
235
- end
236
-
237
- def value_of(instance, attr, delegate)
238
- if delegate
239
- target = instance.public_send(delegate)
240
- target&.public_send(attr.name)
241
- else
242
- instance.public_send(attr.name)
243
- end
244
- end
245
-
246
- def row_name(rule, spelling)
247
- (spelling || rule.name).to_s
248
- end
249
- end
250
- end
251
- end
252
- end