lutaml-model 0.8.94 → 0.8.96
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/downstream-performance.yml +17 -6
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +2 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +145 -16
- data/lib/lutaml/model/serialize/format_conversion.rb +6 -1
- data/lib/lutaml/model/serialize/initialization.rb +13 -1
- data/lib/lutaml/model/serialize/model_import.rb +30 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/format.rb +8 -10
- data/lib/lutaml/xml/leptris/plan_compiler.rb +110 -21
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +124 -10
- data/lib/lutaml/xml/leptris/plan_serializer.rb +114 -18
- data/lib/lutaml/xml/leptris.rb +38 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/model/writer_assigned_attribute_spec.rb +2 -2
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +6 -4
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +142 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +105 -0
- data/spec/person_spec.rb +8 -8
- metadata +9 -2
|
@@ -99,6 +99,14 @@ module Lutaml
|
|
|
99
99
|
attr_rows = [] # [[rule, attr]] hydration metadata
|
|
100
100
|
plan_attrs = [] # [{name:, kind:}] rows for the engine plan
|
|
101
101
|
compiled = [] # [rule, attr, kind, spelling, delegate_target]
|
|
102
|
+
ns_attr_names = [] # exact-URI attribute rows (ambiguity guard)
|
|
103
|
+
# Local-name claim tallies for attribute rows: a name claimed
|
|
104
|
+
# by both an exact-URI row and a plain (any-namespace) row
|
|
105
|
+
# cannot be dispatched by the walk — the ns form :none
|
|
106
|
+
# matches any qualification, so the plain row captures the
|
|
107
|
+
# qualified spelling too (#758). The interpretive matcher
|
|
108
|
+
# owns shared names.
|
|
109
|
+
attr_local_claims = Hash.new { |h, k| h[k] = { ns: 0, plain: 0 } }
|
|
102
110
|
cdata = false
|
|
103
111
|
mixed_content = false
|
|
104
112
|
needs_nodes = false
|
|
@@ -154,15 +162,10 @@ module Lutaml
|
|
|
154
162
|
return opt_out!(:fragment_with_ns) if fragment_needed && model_ns
|
|
155
163
|
|
|
156
164
|
if rule.attribute?
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
# until plan rows carry namespace identity.
|
|
162
|
-
return opt_out!(:attr_type_ns) if attr.type_namespace_class(register)
|
|
163
|
-
|
|
164
|
-
attr_rows << [rule, attr]
|
|
165
|
-
plan_attrs << { name: rule.name.to_s }
|
|
165
|
+
opt = compile_attribute_row(rule, attr, register,
|
|
166
|
+
attr_rows, plan_attrs,
|
|
167
|
+
ns_attr_names, attr_local_claims)
|
|
168
|
+
return opt_out!(opt) if opt
|
|
166
169
|
elsif rule.content_mapping?
|
|
167
170
|
return opt_out!(:multi_content) if content_rows(rows) >= 1
|
|
168
171
|
|
|
@@ -247,12 +250,23 @@ module Lutaml
|
|
|
247
250
|
# namespaced models serialize through the interpretive
|
|
248
251
|
# writer (lutaml-model#847: standalone to_xml under leptris
|
|
249
252
|
# dropped the element xmlns entirely).
|
|
250
|
-
|
|
253
|
+
# Exact-URI attribute rows include their namespaced spelling
|
|
254
|
+
# here: the plan serializer writes attributes by local name
|
|
255
|
+
# (no prefixed wire spelling, no declaration emission —
|
|
256
|
+
# leptris#1551), so these models serialize interpretively.
|
|
257
|
+
namespaced = !model_ns.nil? || rows.any? { |r| r[:ns] } ||
|
|
258
|
+
plan_attrs.any? { |a| a[:ns] }
|
|
251
259
|
|
|
252
260
|
flags = []
|
|
253
261
|
flags << :cdata if cdata
|
|
254
262
|
flags << :mixed_content if mixed_content
|
|
255
|
-
|
|
263
|
+
# Unconditional: a namespace-less model's children match by
|
|
264
|
+
# local name even when the document declares a default or
|
|
265
|
+
# prefixed namespace (#932) — the interpretive matcher's
|
|
266
|
+
# unprefixed-any-URI behavior; elements (unlike attributes)
|
|
267
|
+
# inherit the default xmlns, so the strict no-URI default
|
|
268
|
+
# dropped every child of a namespaced document.
|
|
269
|
+
flags << :ns_lenient
|
|
256
270
|
tree = { name: mapping.root_element.to_s,
|
|
257
271
|
attributes: plan_attrs, children: rows }
|
|
258
272
|
tree[:ns] = model_ns if model_ns
|
|
@@ -267,23 +281,95 @@ module Lutaml
|
|
|
267
281
|
return nil
|
|
268
282
|
end
|
|
269
283
|
|
|
284
|
+
return opt_out!(:attr_ns_claim_conflict) if attr_local_claims
|
|
285
|
+
.any? { |_, c| c[:ns].positive? && c[:plain].positive? }
|
|
286
|
+
|
|
287
|
+
# Exact-URI attribute rows rely on the sole-claimant lenient
|
|
288
|
+
# recovery for out-of-namespace spellings (the interpretive
|
|
289
|
+
# exact-first-then-any-qualification precedence), and that
|
|
290
|
+
# recovery reads the source node — models carrying such rows
|
|
291
|
+
# must receive one through the buckets chain.
|
|
292
|
+
needs_nodes = true unless ns_attr_names.empty?
|
|
293
|
+
|
|
294
|
+
# A model combining exact-URI attribute rows with nested
|
|
295
|
+
# child rows needs the walk's nested attribute capture
|
|
296
|
+
# (leptris#1563, fixed 1.9.313.0): older engines drop the
|
|
297
|
+
# children's attribute capture under such plans, so they
|
|
298
|
+
# stay interpretive. Leaf models are unaffected.
|
|
299
|
+
return opt_out!(:attr_type_ns_nested) if plan_attrs.any? { |a| a[:ns] } &&
|
|
300
|
+
rows.any? && !Leptris.nested_attr_capture_compatible?
|
|
301
|
+
|
|
302
|
+
# The catch-all row: kind :wildcard with an explicit :any ns
|
|
303
|
+
# form (pad0) — the two-pass walk routes every child no
|
|
304
|
+
# named row claimed here, tagged for hydrator routing.
|
|
305
|
+
# Document order recovery and bridging happen in the
|
|
306
|
+
# hydrator against the source node.
|
|
307
|
+
if (any_rule = mapping.any_element_rule)
|
|
308
|
+
any_attr = model_class.attributes(register)[any_rule.to]
|
|
309
|
+
return opt_out!(:attr_nil) if any_attr.nil?
|
|
310
|
+
|
|
311
|
+
needs_nodes = true
|
|
312
|
+
wildcard_tag = tag += 1
|
|
313
|
+
compiled << [any_rule, any_attr, :wildcard_any, nil, nil]
|
|
314
|
+
rows << { name: "__lutaml_any__", kind: :wildcard, ns: :any,
|
|
315
|
+
type_tag: wildcard_tag }
|
|
316
|
+
end
|
|
317
|
+
|
|
270
318
|
{ descriptor: descriptor, tree: tree, rows: compiled,
|
|
271
319
|
attr_rows: attr_rows, mapping: mapping,
|
|
272
320
|
row_tags: row_tags, namespaced: namespaced,
|
|
273
321
|
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
274
322
|
needs_nodes: needs_nodes,
|
|
275
|
-
collection_defaults: collection_defaults
|
|
323
|
+
collection_defaults: collection_defaults,
|
|
324
|
+
ns_attr_names: ns_attr_names.empty? ? nil : ns_attr_names,
|
|
325
|
+
wildcard_tag: (wildcard_tag if defined?(wildcard_tag)) }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Attribute rows: identity is (URI, local) (lutaml-model#744).
|
|
329
|
+
# A type-level namespace compiles into an exact-URI AttrPlan ns
|
|
330
|
+
# row (libleptris 1.9.289): the walk captures the (URI, local)
|
|
331
|
+
# match and the sole-claimant lenient recovery in PlanHydrator
|
|
332
|
+
# keeps the interpretive exact-first-then-any-qualification
|
|
333
|
+
# precedence for out-of-namespace spellings. Returns the
|
|
334
|
+
# opt-out clause when the row must stay interpretive, nil
|
|
335
|
+
# otherwise. Types declaring alias URI families stay
|
|
336
|
+
# interpretive (a plan row holds a single exact URI and
|
|
337
|
+
# non-sole-claimant alias families have no plan-side
|
|
338
|
+
# resolution order); :blank / :inherit arrive as Symbols —
|
|
339
|
+
# no URI to express in a row.
|
|
340
|
+
def compile_attribute_row(rule, attr, register, attr_rows,
|
|
341
|
+
plan_attrs, ns_attr_names,
|
|
342
|
+
attr_local_claims)
|
|
343
|
+
return :non_scalar unless scalar_type?(attr, register)
|
|
344
|
+
|
|
345
|
+
attr_rows << [rule, attr]
|
|
346
|
+
type_ns = attr.type_namespace_class(register)
|
|
347
|
+
claim = attr_local_claims[rule.name.to_s]
|
|
348
|
+
if type_ns.nil?
|
|
349
|
+
claim[:plain] += 1
|
|
350
|
+
plan_attrs << { name: rule.name.to_s }
|
|
351
|
+
return nil
|
|
352
|
+
end
|
|
353
|
+
return :attr_type_ns_symbol if type_ns.is_a?(Symbol)
|
|
354
|
+
return :attr_type_ns_aliases unless Leptris.attr_ns_rows_compatible? &&
|
|
355
|
+
type_ns.all_uris.size == 1
|
|
356
|
+
|
|
357
|
+
claim[:ns] += 1
|
|
358
|
+
plan_attrs << { name: rule.name.to_s,
|
|
359
|
+
ns: { exact: type_ns.uri.to_s,
|
|
360
|
+
prefix: type_ns.prefix_default&.to_s } }
|
|
361
|
+
ns_attr_names << rule.name.to_s
|
|
362
|
+
nil
|
|
276
363
|
end
|
|
277
364
|
|
|
278
365
|
def compilable_mapping?(mapping)
|
|
279
366
|
# Mapping's uniform interface: root_mappings defaults to
|
|
280
367
|
# false on the base (KeyValue overrides with its own).
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return false if mapping.any_element_rule
|
|
368
|
+
# The map_any_element catch-all rides the engine's wildcard
|
|
369
|
+
# row (leptris#1552, named rows take precedence); engines
|
|
370
|
+
# without the kind keep the interpretive refusal.
|
|
371
|
+
return false if mapping.any_element_rule &&
|
|
372
|
+
!Leptris.wildcard_rows_compatible?
|
|
287
373
|
|
|
288
374
|
mapping.root_element && !mapping.root_mappings?
|
|
289
375
|
end
|
|
@@ -344,9 +430,9 @@ module Lutaml
|
|
|
344
430
|
# prefix. Blank-namespace rules (xmlns="") match :none.
|
|
345
431
|
def child_ns(rule, _model_ns)
|
|
346
432
|
uri = rule.namespace
|
|
347
|
-
return
|
|
433
|
+
return :none if uri.nil? || uri.to_s.empty?
|
|
348
434
|
|
|
349
|
-
:
|
|
435
|
+
{ exact: uri.to_s, prefix: rule.prefix&.to_s }
|
|
350
436
|
end
|
|
351
437
|
|
|
352
438
|
def content_rows(rows)
|
|
@@ -360,7 +446,10 @@ module Lutaml
|
|
|
360
446
|
# Mapping's uniform interface: namespace_class defaults
|
|
361
447
|
# to nil on the base (Xml overrides).
|
|
362
448
|
ns_class = mapping.namespace_class
|
|
363
|
-
|
|
449
|
+
return nil unless ns_class&.uri
|
|
450
|
+
|
|
451
|
+
{ exact: ns_class.uri.to_s,
|
|
452
|
+
prefix: ns_class.prefix_default&.to_s }
|
|
364
453
|
end
|
|
365
454
|
|
|
366
455
|
def scalar_type?(attr, register)
|
|
@@ -72,6 +72,7 @@ module Lutaml
|
|
|
72
72
|
def attributes_kwargs(plan, value, node = nil)
|
|
73
73
|
kwargs = {}
|
|
74
74
|
sole_claimants = sole_claimant_names(plan)
|
|
75
|
+
ns_attr_names = plan[:ns_attr_names]
|
|
75
76
|
plan_attr_rows(plan).each do |rule, attr, name|
|
|
76
77
|
v = value.attribute(name)
|
|
77
78
|
if v.nil? && node && sole_claimants.include?(name)
|
|
@@ -83,6 +84,14 @@ module Lutaml
|
|
|
83
84
|
end
|
|
84
85
|
next if v.nil?
|
|
85
86
|
|
|
87
|
+
# (URI, local) rows mirror the interpretive matcher's
|
|
88
|
+
# ambiguity refusal: a second spelling of the same local
|
|
89
|
+
# name binds nothing (lutaml-model#744).
|
|
90
|
+
if ns_attr_names&.include?(name) &&
|
|
91
|
+
ambiguous_local_spellings?(node, name)
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
|
|
86
95
|
v = v.split(rule.delimiter) if rule.delimiter
|
|
87
96
|
if rule.as_list && rule.as_list[:import]
|
|
88
97
|
v = rule.as_list[:import].call(v)
|
|
@@ -92,6 +101,53 @@ module Lutaml
|
|
|
92
101
|
kwargs
|
|
93
102
|
end
|
|
94
103
|
|
|
104
|
+
# True when the node carries more than one spelling of the
|
|
105
|
+
# local name (any qualification). Only consulted for exact-URI
|
|
106
|
+
# attribute rows.
|
|
107
|
+
def ambiguous_local_spellings?(node, local_name)
|
|
108
|
+
return false unless node.respond_to?(:attributes)
|
|
109
|
+
|
|
110
|
+
seen = false
|
|
111
|
+
node.attributes.each_value do |attr|
|
|
112
|
+
name = attr.name.to_s
|
|
113
|
+
name = name.split(":").last if name.include?(":")
|
|
114
|
+
next unless name == local_name
|
|
115
|
+
|
|
116
|
+
return true if seen
|
|
117
|
+
|
|
118
|
+
seen = true
|
|
119
|
+
end
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A captured-but-empty child marshals as a nil string value.
|
|
124
|
+
# The interpretive pipeline classifies the raw "" through the
|
|
125
|
+
# rule's value map (:empty key) — the :empty directive (the
|
|
126
|
+
# plain-rule default) keeps it as "", :nil binds nil, and any
|
|
127
|
+
# other directive means uninitialized, i.e. no binding.
|
|
128
|
+
def mapped_empty_value(rule)
|
|
129
|
+
case rule.value_map(:from)[:empty]
|
|
130
|
+
when :nil then nil
|
|
131
|
+
when :empty then ""
|
|
132
|
+
else :skip
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def mapped_capture_values(values, rule)
|
|
137
|
+
return values unless values.include?(nil)
|
|
138
|
+
|
|
139
|
+
mapped = mapped_empty_value(rule)
|
|
140
|
+
out = []
|
|
141
|
+
values.each do |item|
|
|
142
|
+
if item.nil?
|
|
143
|
+
out << mapped unless mapped == :skip
|
|
144
|
+
else
|
|
145
|
+
out << item
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
out
|
|
149
|
+
end
|
|
150
|
+
|
|
95
151
|
# Wire-name strings are plan-frozen; materializing them per
|
|
96
152
|
# hydration allocated two or three strings per attribute row
|
|
97
153
|
# per instance.
|
|
@@ -128,6 +184,10 @@ module Lutaml
|
|
|
128
184
|
# Interpretive parity: several captures into a
|
|
129
185
|
# non-collection attribute arrive as the full array.
|
|
130
186
|
v = vals.map(&:string_value)
|
|
187
|
+
if v.include?(nil)
|
|
188
|
+
v = v.map { |item| item.nil? ? mapped_empty_value(rule) : item }
|
|
189
|
+
v.reject! { |item| item == :skip }
|
|
190
|
+
end
|
|
131
191
|
else
|
|
132
192
|
first = if tag
|
|
133
193
|
tagged[tag]&.first
|
|
@@ -135,16 +195,26 @@ module Lutaml
|
|
|
135
195
|
grouped.dig(rule.name.to_s, 0)
|
|
136
196
|
end
|
|
137
197
|
v = first&.string_value
|
|
198
|
+
v = mapped_empty_value(rule) if v.nil? && first
|
|
138
199
|
end
|
|
139
|
-
unless v.nil?
|
|
200
|
+
unless v.nil? || v == :skip
|
|
140
201
|
v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
|
|
141
202
|
assign(kwargs, delegates, delegate, rule, attr, v)
|
|
142
203
|
end
|
|
143
|
-
when :
|
|
204
|
+
when :wildcard_any
|
|
144
205
|
# interpreted post-instance (interpret_deferred)
|
|
145
206
|
when :content
|
|
146
|
-
|
|
147
|
-
|
|
207
|
+
runs = content_runs(value)
|
|
208
|
+
if runs.empty? && !attr.collection?
|
|
209
|
+
# An empty element's content binds through the rule's
|
|
210
|
+
# value map for scalar content attributes: "" for the
|
|
211
|
+
# plain :empty directive, nil for :nil, skipped for
|
|
212
|
+
# anything else (uninitialized).
|
|
213
|
+
bound = mapped_empty_value(rule)
|
|
214
|
+
assign(kwargs, delegates, delegate, rule, attr, bound) unless bound == :skip
|
|
215
|
+
else
|
|
216
|
+
assign(kwargs, delegates, delegate, rule, attr, runs)
|
|
217
|
+
end
|
|
148
218
|
when :ordered_deferred
|
|
149
219
|
# With a source node the child hydrates natively: one
|
|
150
220
|
# walk against its own node + element_order from the
|
|
@@ -165,7 +235,10 @@ module Lutaml
|
|
|
165
235
|
end
|
|
166
236
|
end
|
|
167
237
|
when :collection_cb
|
|
168
|
-
values = grouped[rule.name.to_s].to_a.map
|
|
238
|
+
values = grouped[rule.name.to_s].to_a.map do |item|
|
|
239
|
+
item&.string_value
|
|
240
|
+
end
|
|
241
|
+
values = mapped_capture_values(values, rule)
|
|
169
242
|
if rule.transform.is_a?(Class)
|
|
170
243
|
values = values.map { |v| rule.transform_value(attr, v, :from, :xml) }
|
|
171
244
|
end
|
|
@@ -178,6 +251,7 @@ module Lutaml
|
|
|
178
251
|
else
|
|
179
252
|
native_collection(value, rule.name.to_s)
|
|
180
253
|
end
|
|
254
|
+
values = mapped_capture_values(values, rule) unless values.nil?
|
|
181
255
|
unless values.nil?
|
|
182
256
|
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
183
257
|
end
|
|
@@ -232,12 +306,19 @@ module Lutaml
|
|
|
232
306
|
when :content_deferred
|
|
233
307
|
runs = content_runs(value)
|
|
234
308
|
if runs.empty?
|
|
235
|
-
# The interpretive pipeline
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
309
|
+
# The interpretive pipeline classifies the raw ""
|
|
310
|
+
# through the rule's value map: the :empty directive
|
|
311
|
+
# (the plain-rule default) binds "" — the attribute is
|
|
312
|
+
# still marked set, which is what the #856 mark is
|
|
313
|
+
# about; :nil binds nil; other directives leave the
|
|
314
|
+
# binding unset.
|
|
240
315
|
instance.value_set_for(attr.name)
|
|
316
|
+
case rule.value_map(:from)[:empty]
|
|
317
|
+
when :nil
|
|
318
|
+
instance.public_send(:"#{attr.name}=", nil)
|
|
319
|
+
when :empty
|
|
320
|
+
instance.public_send(:"#{attr.name}=", "")
|
|
321
|
+
end
|
|
241
322
|
next
|
|
242
323
|
end
|
|
243
324
|
|
|
@@ -271,6 +352,39 @@ module Lutaml
|
|
|
271
352
|
|
|
272
353
|
instance.public_send(:"#{attr.name}=",
|
|
273
354
|
attr.collection? ? results : results.first)
|
|
355
|
+
when :wildcard_any
|
|
356
|
+
# map_any_element: every child element no named row
|
|
357
|
+
# claims, in document order, bridged as wrappers — the
|
|
358
|
+
# interpretive pass's contract. The wildcard row's
|
|
359
|
+
# type_tag marks its captures in the walk.
|
|
360
|
+
next unless node
|
|
361
|
+
|
|
362
|
+
claimed = plan[:rows].filter_map do |wr, _wa, _wk, spelling, _wd|
|
|
363
|
+
next if wr.equal?(rule) || wr.name.nil?
|
|
364
|
+
|
|
365
|
+
spelling ? spelling.to_s : wr.name.to_s
|
|
366
|
+
end.uniq
|
|
367
|
+
elements = []
|
|
368
|
+
node.children.each do |child|
|
|
369
|
+
next unless child.is_a?(::Leptris::XML::Element)
|
|
370
|
+
next if claimed.include?(child.name)
|
|
371
|
+
|
|
372
|
+
elements << bridge_element(child)
|
|
373
|
+
end
|
|
374
|
+
next if elements.empty?
|
|
375
|
+
|
|
376
|
+
# The interpretive pass casts each collected element
|
|
377
|
+
# through the attribute: String keeps the wrapper,
|
|
378
|
+
# Serializable types build model instances.
|
|
379
|
+
results = elements.map do |element|
|
|
380
|
+
attr.cast(element, :xml, register,
|
|
381
|
+
lutaml_parent: instance,
|
|
382
|
+
lutaml_root: instance.lutaml_root || instance)
|
|
383
|
+
end
|
|
384
|
+
instance.public_send(
|
|
385
|
+
:"#{attr.name}=",
|
|
386
|
+
attr.collection? ? results : results.first,
|
|
387
|
+
)
|
|
274
388
|
when :polymorphic
|
|
275
389
|
results = deferred_elements(value, rule, buckets).map do |element|
|
|
276
390
|
attr.cast(element, :xml, register,
|
|
@@ -38,16 +38,84 @@ module Lutaml
|
|
|
38
38
|
return nil if plan[:ordered] && instance.element_order.nil?
|
|
39
39
|
|
|
40
40
|
doc = ::Leptris::XML::Document.create
|
|
41
|
-
|
|
41
|
+
ns_ctx = Namespaces.new(plan)
|
|
42
|
+
return nil if ns_ctx.unspellable?
|
|
43
|
+
|
|
44
|
+
root = doc.create_element(
|
|
45
|
+
ns_ctx.spell(plan[:tree][:name], plan[:tree][:ns]),
|
|
46
|
+
)
|
|
42
47
|
doc.root = root
|
|
48
|
+
ns_ctx.declare_on(root)
|
|
43
49
|
if plan[:ordered]
|
|
44
50
|
build_ordered(root, instance, plan, doc, register)
|
|
45
51
|
else
|
|
46
|
-
build(root, instance, plan, doc, register)
|
|
52
|
+
build(root, instance, plan, doc, register, ns_ctx)
|
|
47
53
|
end
|
|
48
54
|
doc.to_xml(indent: 2, no_decl: true)
|
|
49
55
|
end
|
|
50
56
|
|
|
57
|
+
# Namespace spelling for namespaced plans (#847): every ns
|
|
58
|
+
# form resolves to a wire spelling (prefix:local, or local
|
|
59
|
+
# under a default xmlns), and every distinct (prefix, uri)
|
|
60
|
+
# pair is declared exactly once on the output root in
|
|
61
|
+
# first-encounter order — the #1558 contract on the DOM
|
|
62
|
+
# path. A nested plan's own tree ns overrides the inherited
|
|
63
|
+
# one; rows without an ns form inherit.
|
|
64
|
+
class Namespaces
|
|
65
|
+
attr_reader :unspellable
|
|
66
|
+
|
|
67
|
+
def initialize(plan)
|
|
68
|
+
@forms = {}
|
|
69
|
+
@declared = {}
|
|
70
|
+
@unspellable = false
|
|
71
|
+
collect(plan[:tree])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def unspellable?
|
|
75
|
+
@unspellable
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def collect(tree)
|
|
79
|
+
register(tree[:ns])
|
|
80
|
+
(tree[:attributes] || []).each { |attr| register(attr[:ns]) }
|
|
81
|
+
(tree[:children] || []).each do |row|
|
|
82
|
+
register(row[:ns]) if row[:ns].is_a?(Hash)
|
|
83
|
+
collect(row[:plan]) if row[:kind] == :nested && row[:plan]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def register(ns_form)
|
|
88
|
+
return if ns_form.nil? || ns_form == :none || ns_form == :any
|
|
89
|
+
|
|
90
|
+
form = { prefix: ns_form[:prefix], uri: ns_form[:exact] }
|
|
91
|
+
@forms[ns_form[:exact]] ||= form
|
|
92
|
+
@unspellable = true if ns_form[:exact] && !ns_form.key?(:prefix)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The wire spelling for a local name under this form.
|
|
96
|
+
def spell(local, ns_form = nil)
|
|
97
|
+
form = ns_form.is_a?(Hash) ? ns_form : @forms[ns_form]
|
|
98
|
+
prefix = form && form[:prefix]
|
|
99
|
+
prefix ? "#{prefix}:#{local}" : local
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def form_for(ns_form)
|
|
103
|
+
return nil if ns_form.nil? || ns_form == :none
|
|
104
|
+
|
|
105
|
+
ns_form.is_a?(Hash) ? ns_form : @forms[ns_form]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def declare_on(root)
|
|
109
|
+
@forms.each_value do |form|
|
|
110
|
+
key = form[:uri]
|
|
111
|
+
next if key.nil? || @declared[key]
|
|
112
|
+
|
|
113
|
+
@declared[key] = true
|
|
114
|
+
root.add_namespace_definition(form[:prefix], form[:uri])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
51
119
|
# Whether the compiled plan is serialize-shaped. Ordered/mixed
|
|
52
120
|
# models serialize through build_ordered from element_order;
|
|
53
121
|
# plain models through plan rows.
|
|
@@ -63,9 +131,10 @@ module Lutaml
|
|
|
63
131
|
Lutaml::Model::Config.default_register
|
|
64
132
|
end
|
|
65
133
|
|
|
66
|
-
def build(element, instance, plan, doc, reg = nil
|
|
134
|
+
def build(element, instance, plan, doc, reg = nil, ns_ctx = nil,
|
|
135
|
+
inherited_ns = nil)
|
|
67
136
|
reg ||= register
|
|
68
|
-
write_attributes(element, instance, plan, reg)
|
|
137
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
69
138
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
70
139
|
value = value_of(instance, attr, delegate)
|
|
71
140
|
next unless rule.render?(value, instance)
|
|
@@ -76,33 +145,46 @@ module Lutaml
|
|
|
76
145
|
# several occurrences serializes one element per item,
|
|
77
146
|
# as the interpretive writer does. A plain scalar skips
|
|
78
147
|
# the Array() wrap — the common case.
|
|
148
|
+
row_ns = plan[:tree][:children]&.find do |r|
|
|
149
|
+
r[:name] == rule.name.to_s
|
|
150
|
+
end&.[](:ns) || inherited_ns
|
|
79
151
|
if value.is_a?(::Array)
|
|
80
152
|
value.each do |item|
|
|
81
|
-
add_leaf(element,
|
|
153
|
+
add_leaf(element,
|
|
154
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
82
155
|
attr.serialize(item, :xml, reg), doc,
|
|
83
156
|
attrs: rule.when_attribute)
|
|
84
157
|
end
|
|
85
158
|
else
|
|
86
|
-
add_leaf(element,
|
|
159
|
+
add_leaf(element,
|
|
160
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
87
161
|
attr.serialize(value, :xml, reg), doc,
|
|
88
162
|
attrs: rule.when_attribute)
|
|
89
163
|
end
|
|
90
164
|
when :collection_native, :collection_cb
|
|
165
|
+
row_ns = plan[:tree][:children]&.find do |r|
|
|
166
|
+
r[:name] == rule.name.to_s
|
|
167
|
+
end&.[](:ns) || inherited_ns
|
|
91
168
|
Array(value).each do |item|
|
|
92
|
-
add_leaf(element,
|
|
169
|
+
add_leaf(element,
|
|
170
|
+
ns_ctx ? ns_ctx.spell(rule.name.to_s, row_ns) : rule.name.to_s,
|
|
93
171
|
attr.serialize(item, :xml, reg), doc,
|
|
94
172
|
attrs: rule.when_attribute)
|
|
95
173
|
end
|
|
96
174
|
when :nested, :ordered_deferred
|
|
97
175
|
Array(value).each do |item|
|
|
98
176
|
child_plan = PlanCompiler.compile(item.class, reg)
|
|
99
|
-
|
|
177
|
+
child_ns = child_plan[:tree][:ns] || inherited_ns
|
|
178
|
+
child = element.create_child(
|
|
179
|
+
ns_ctx ? ns_ctx.spell(child_plan[:tree][:name], child_ns) : child_plan[:tree][:name],
|
|
180
|
+
)
|
|
181
|
+
ns_ctx&.collect(child_plan[:tree])
|
|
100
182
|
if child_plan[:ordered]
|
|
101
|
-
|
|
102
|
-
|
|
183
|
+
next unless build_ordered(child, item, child_plan,
|
|
184
|
+
doc, reg, ns_ctx, child_ns)
|
|
103
185
|
|
|
104
186
|
else
|
|
105
|
-
build(child, item, child_plan, doc, reg)
|
|
187
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns)
|
|
106
188
|
end
|
|
107
189
|
end
|
|
108
190
|
when :raw
|
|
@@ -121,9 +203,11 @@ module Lutaml
|
|
|
121
203
|
# the model are reflected; element_order text is the fallback.
|
|
122
204
|
# PIs drop (interpretive parity). Returns nil when a nested
|
|
123
205
|
# ordered child lacks element_order (caller falls back).
|
|
124
|
-
def build_ordered(element, instance, plan, doc, reg = nil
|
|
206
|
+
def build_ordered(element, instance, plan, doc, reg = nil,
|
|
207
|
+
ns_ctx = nil, _inherited_ns = nil)
|
|
125
208
|
reg ||= register
|
|
126
|
-
|
|
209
|
+
ns_ctx&.collect(plan[:tree])
|
|
210
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
127
211
|
rows_by_name = {}
|
|
128
212
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
129
213
|
next unless rule.name
|
|
@@ -213,8 +297,16 @@ module Lutaml
|
|
|
213
297
|
|
|
214
298
|
# Attributes in document order when the instance recorded it
|
|
215
299
|
# (parsed models), else plan row order.
|
|
216
|
-
def write_attributes(element, instance, plan,
|
|
300
|
+
def write_attributes(element, instance, plan, reg = nil,
|
|
301
|
+
ns_ctx = nil)
|
|
217
302
|
recorded = instance.attribute_order
|
|
303
|
+
attr_prefixes = {}
|
|
304
|
+
if ns_ctx
|
|
305
|
+
(plan[:tree][:attributes] || []).each do |a|
|
|
306
|
+
form = a[:ns].is_a?(Hash) ? a[:ns] : nil
|
|
307
|
+
attr_prefixes[a[:name]] = form && form[:prefix]
|
|
308
|
+
end
|
|
309
|
+
end
|
|
218
310
|
if recorded && !recorded.empty?
|
|
219
311
|
by_name = {}
|
|
220
312
|
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
@@ -223,24 +315,28 @@ module Lutaml
|
|
|
223
315
|
names = recorded |
|
|
224
316
|
plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
|
|
225
317
|
names.each do |name|
|
|
226
|
-
write_attribute(element, instance, name, by_name[name]
|
|
318
|
+
write_attribute(element, instance, name, by_name[name],
|
|
319
|
+
reg, attr_prefixes[name])
|
|
227
320
|
end
|
|
228
321
|
else
|
|
229
322
|
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
230
323
|
write_attribute(element, instance, rule.name.to_s,
|
|
231
|
-
[rule, attr, delegate]
|
|
324
|
+
[rule, attr, delegate], reg,
|
|
325
|
+
attr_prefixes[rule.name.to_s])
|
|
232
326
|
end
|
|
233
327
|
end
|
|
234
328
|
end
|
|
235
329
|
|
|
236
|
-
def write_attribute(element, instance, name, entry, reg = nil
|
|
330
|
+
def write_attribute(element, instance, name, entry, reg = nil,
|
|
331
|
+
prefix = nil)
|
|
237
332
|
return unless entry
|
|
238
333
|
|
|
239
334
|
_rule, attr, delegate = entry
|
|
240
335
|
value = value_of(instance, attr, delegate)
|
|
241
336
|
return if value.nil?
|
|
242
337
|
|
|
243
|
-
|
|
338
|
+
spelled = prefix ? "#{prefix}:#{name}" : name
|
|
339
|
+
element.set_attribute(spelled,
|
|
244
340
|
attr.serialize(value, :xml, reg || register).to_s)
|
|
245
341
|
end
|
|
246
342
|
|
data/lib/lutaml/xml/leptris.rb
CHANGED
|
@@ -16,6 +16,23 @@ module Lutaml
|
|
|
16
16
|
# existing opt-out protocol.
|
|
17
17
|
MIN_LEPTRIS_VERSION = "1.9.273.0"
|
|
18
18
|
|
|
19
|
+
# AttrPlan rows carrying an ns form (exact-URI attribute matching,
|
|
20
|
+
# leptris#1486) shipped in libleptris 1.9.289.0. Older gems keep
|
|
21
|
+
# type-namespaced attributes on the interpretive path.
|
|
22
|
+
MIN_LEPTRIS_ATTR_NS_VERSION = "1.9.289.0"
|
|
23
|
+
|
|
24
|
+
# Nested child plans capture their declared attribute rows
|
|
25
|
+
# correctly again as of 1.9.313.0 (leptris#1563: the walk dropped
|
|
26
|
+
# children's attribute capture when the parent plan carried an
|
|
27
|
+
# exact-URI attribute row). Older gems keep those models
|
|
28
|
+
# interpretive.
|
|
29
|
+
MIN_LEPTRIS_NESTED_ATTR_VERSION = "1.9.313.0"
|
|
30
|
+
|
|
31
|
+
# Wildcard child rows (leptris#1552: named-rows-take-precedence
|
|
32
|
+
# two-pass walk, ns-form aware, type_tag echo) — the plan path's
|
|
33
|
+
# map_any_element catch-all. Ships with the 1.9.313 binding.
|
|
34
|
+
MIN_LEPTRIS_WILDCARD_VERSION = "1.9.313.0"
|
|
35
|
+
|
|
19
36
|
def self.plan_path_compatible?
|
|
20
37
|
return false unless defined?(Gem)
|
|
21
38
|
|
|
@@ -23,6 +40,27 @@ module Lutaml
|
|
|
23
40
|
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
|
|
24
41
|
end
|
|
25
42
|
|
|
43
|
+
def self.attr_ns_rows_compatible?
|
|
44
|
+
return false unless defined?(Gem)
|
|
45
|
+
|
|
46
|
+
spec = Gem.loaded_specs["leptris"]
|
|
47
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_ATTR_NS_VERSION)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.nested_attr_capture_compatible?
|
|
51
|
+
return false unless defined?(Gem)
|
|
52
|
+
|
|
53
|
+
spec = Gem.loaded_specs["leptris"]
|
|
54
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_NESTED_ATTR_VERSION)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.wildcard_rows_compatible?
|
|
58
|
+
return false unless defined?(Gem)
|
|
59
|
+
|
|
60
|
+
spec = Gem.loaded_specs["leptris"]
|
|
61
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WILDCARD_VERSION)
|
|
62
|
+
end
|
|
63
|
+
|
|
26
64
|
autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
|
|
27
65
|
autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
|
|
28
66
|
autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
|