lutaml-model 0.8.95 → 0.8.97
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/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +2 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +133 -7
- data/lib/lutaml/model/serialize/format_conversion.rb +13 -2
- data/lib/lutaml/model/serialize/initialization.rb +25 -7
- data/lib/lutaml/model/serialize/model_import.rb +39 -4
- 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 +137 -22
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +34 -1
- data/lib/lutaml/xml/leptris/plan_serializer.rb +174 -18
- data/lib/lutaml/xml/leptris/plan_walk.rb +1 -0
- data/lib/lutaml/xml/leptris.rb +47 -0
- data/lib/lutaml/xml/model_transform.rb +38 -14
- data/spec/lutaml/model/model_import_reentry_spec.rb +46 -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/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +2 -1
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +105 -0
- metadata +7 -2
|
@@ -86,8 +86,49 @@ module Lutaml
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Two engine walk defects gate walk-side consumers here; both
|
|
90
|
+
# bail models to the interpretive matcher, whose exact-first-
|
|
91
|
+
# then-any-qualification precedence binds what the walk drops.
|
|
92
|
+
# The serialize side (PlanSerializer) reads the Ruby tree and
|
|
93
|
+
# stays on the fast path. leptris 1.9.320.0 (#1587) fixes
|
|
94
|
+
# both defects — the scan is skipped there and every model
|
|
95
|
+
# rides the walk again.
|
|
96
|
+
#
|
|
97
|
+
# leptris#1585: exact-URI CHILD-row ns_uri strings are not
|
|
98
|
+
# retained engine-side — once the build anchors are GC'd (a
|
|
99
|
+
# warm walk plus one GC suffices), those rows stop matching
|
|
100
|
+
# and hydration silently answers nil.
|
|
101
|
+
#
|
|
102
|
+
# leptris#1586: plain attribute rows do not leniently match
|
|
103
|
+
# namespace-qualified wire attributes (uniword fontTable:
|
|
104
|
+
# every w:name/w:val against a plain attr row hydrates nil).
|
|
105
|
+
# A plan level carrying plain attr rows under a namespaced
|
|
106
|
+
# chain therefore cannot be trusted on the walk; attribute
|
|
107
|
+
# rows with an exact URI (or :any) and plans without any
|
|
108
|
+
# namespace form are unaffected.
|
|
109
|
+
def engine_walk_safe?(plan)
|
|
110
|
+
return true if Leptris.walk_ns_safety_compatible?
|
|
111
|
+
|
|
112
|
+
tree_safe?(plan[:tree], false)
|
|
113
|
+
end
|
|
114
|
+
|
|
89
115
|
private
|
|
90
116
|
|
|
117
|
+
def tree_safe?(tree, ancestor_ns)
|
|
118
|
+
namespaced = ancestor_ns || tree[:ns].is_a?(Hash)
|
|
119
|
+
attrs = tree[:attributes] || []
|
|
120
|
+
return false if namespaced &&
|
|
121
|
+
attrs.any? { |a| !a[:ns].is_a?(Hash) && a[:ns] != :any }
|
|
122
|
+
|
|
123
|
+
(tree[:children] || []).all? do |row|
|
|
124
|
+
return false if row[:ns].is_a?(Hash)
|
|
125
|
+
|
|
126
|
+
next true unless row[:kind] == :nested && row[:plan]
|
|
127
|
+
|
|
128
|
+
tree_safe?(row[:plan], namespaced)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
91
132
|
def build(model_class, register)
|
|
92
133
|
return nil unless model_class.is_a?(Class) &&
|
|
93
134
|
model_class.include?(::Lutaml::Model::Serialize)
|
|
@@ -162,7 +203,7 @@ module Lutaml
|
|
|
162
203
|
return opt_out!(:fragment_with_ns) if fragment_needed && model_ns
|
|
163
204
|
|
|
164
205
|
if rule.attribute?
|
|
165
|
-
opt = compile_attribute_row(rule, attr, register,
|
|
206
|
+
opt = compile_attribute_row(rule, attr, register, mapping,
|
|
166
207
|
attr_rows, plan_attrs,
|
|
167
208
|
ns_attr_names, attr_local_claims)
|
|
168
209
|
return opt_out!(opt) if opt
|
|
@@ -225,7 +266,22 @@ module Lutaml
|
|
|
225
266
|
return opt_out!(:non_scalar) unless scalar_type?(attr, register)
|
|
226
267
|
|
|
227
268
|
row = { name: rule.name.to_s }
|
|
228
|
-
|
|
269
|
+
if rule.namespace_set?
|
|
270
|
+
row[:ns] = child_ns(rule, model_ns)
|
|
271
|
+
else
|
|
272
|
+
# The interpretive writer qualifies an element row
|
|
273
|
+
# by its attribute's type namespace when the rule
|
|
274
|
+
# declares none (resolve step 4). Rows without the
|
|
275
|
+
# type form inherit the tree namespace and land
|
|
276
|
+
# under the wrong default xmlns (uniword
|
|
277
|
+
# CoreProperties: <dc:title> spelled <title>).
|
|
278
|
+
type_ns_form = element_type_ns_form(rule, attr,
|
|
279
|
+
register, model_ns)
|
|
280
|
+
return opt_out!(:elem_type_ns_symbol) if type_ns_form == :bail_symbol
|
|
281
|
+
return opt_out!(:elem_type_ns_aliases) if type_ns_form == :bail_aliases
|
|
282
|
+
|
|
283
|
+
row[:ns] = type_ns_form if type_ns_form
|
|
284
|
+
end
|
|
229
285
|
# lutaml-model#88: same-name rows partitioned by the
|
|
230
286
|
# rule's discriminator ride the engine's exclusive
|
|
231
287
|
# predicate match (leptris 1.9.221+, #1272).
|
|
@@ -292,14 +348,28 @@ module Lutaml
|
|
|
292
348
|
needs_nodes = true unless ns_attr_names.empty?
|
|
293
349
|
|
|
294
350
|
# A model combining exact-URI attribute rows with nested
|
|
295
|
-
# child rows
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
# lift — that is the measured win (uniword's deep property
|
|
300
|
-
# models are leaves).
|
|
351
|
+
# child rows needs the walk's nested attribute capture
|
|
352
|
+
# (leptris#1563, fixed 1.9.313.0): older engines drop the
|
|
353
|
+
# children's attribute capture under such plans, so they
|
|
354
|
+
# stay interpretive. Leaf models are unaffected.
|
|
301
355
|
return opt_out!(:attr_type_ns_nested) if plan_attrs.any? { |a| a[:ns] } &&
|
|
302
|
-
rows.any?
|
|
356
|
+
rows.any? && !Leptris.nested_attr_capture_compatible?
|
|
357
|
+
|
|
358
|
+
# The catch-all row: kind :wildcard with an explicit :any ns
|
|
359
|
+
# form (pad0) — the two-pass walk routes every child no
|
|
360
|
+
# named row claimed here, tagged for hydrator routing.
|
|
361
|
+
# Document order recovery and bridging happen in the
|
|
362
|
+
# hydrator against the source node.
|
|
363
|
+
if (any_rule = mapping.any_element_rule)
|
|
364
|
+
any_attr = model_class.attributes(register)[any_rule.to]
|
|
365
|
+
return opt_out!(:attr_nil) if any_attr.nil?
|
|
366
|
+
|
|
367
|
+
needs_nodes = true
|
|
368
|
+
wildcard_tag = tag += 1
|
|
369
|
+
compiled << [any_rule, any_attr, :wildcard_any, nil, nil]
|
|
370
|
+
rows << { name: "__lutaml_any__", kind: :wildcard, ns: :any,
|
|
371
|
+
type_tag: wildcard_tag }
|
|
372
|
+
end
|
|
303
373
|
|
|
304
374
|
{ descriptor: descriptor, tree: tree, rows: compiled,
|
|
305
375
|
attr_rows: attr_rows, mapping: mapping,
|
|
@@ -307,7 +377,8 @@ module Lutaml
|
|
|
307
377
|
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
308
378
|
needs_nodes: needs_nodes,
|
|
309
379
|
collection_defaults: collection_defaults,
|
|
310
|
-
ns_attr_names: ns_attr_names.empty? ? nil : ns_attr_names
|
|
380
|
+
ns_attr_names: ns_attr_names.empty? ? nil : ns_attr_names,
|
|
381
|
+
wildcard_tag: (wildcard_tag if defined?(wildcard_tag)) }
|
|
311
382
|
end
|
|
312
383
|
|
|
313
384
|
# Attribute rows: identity is (URI, local) (lutaml-model#744).
|
|
@@ -322,8 +393,8 @@ module Lutaml
|
|
|
322
393
|
# non-sole-claimant alias families have no plan-side
|
|
323
394
|
# resolution order); :blank / :inherit arrive as Symbols —
|
|
324
395
|
# no URI to express in a row.
|
|
325
|
-
def compile_attribute_row(rule, attr, register,
|
|
326
|
-
plan_attrs, ns_attr_names,
|
|
396
|
+
def compile_attribute_row(rule, attr, register, mapping,
|
|
397
|
+
attr_rows, plan_attrs, ns_attr_names,
|
|
327
398
|
attr_local_claims)
|
|
328
399
|
return :non_scalar unless scalar_type?(attr, register)
|
|
329
400
|
|
|
@@ -331,6 +402,24 @@ module Lutaml
|
|
|
331
402
|
type_ns = attr.type_namespace_class(register)
|
|
332
403
|
claim = attr_local_claims[rule.name.to_s]
|
|
333
404
|
if type_ns.nil?
|
|
405
|
+
# attribute_form_default :qualified on the model's
|
|
406
|
+
# namespace qualifies locally-declared attributes under
|
|
407
|
+
# the parent URI — the interpretive
|
|
408
|
+
# resolve_attribute_namespace contract (WML: updateFields
|
|
409
|
+
# w:val under xmlns="..."). The W3C default is
|
|
410
|
+
# :unqualified, so == :qualified means explicitly set.
|
|
411
|
+
# Rules with an explicit :blank stay unprefixed.
|
|
412
|
+
ns_class = mapping.namespace_class
|
|
413
|
+
if rule.namespace_param != :blank && ns_class &&
|
|
414
|
+
ns_class.attribute_form_default == :qualified
|
|
415
|
+
claim[:ns] += 1
|
|
416
|
+
plan_attrs << { name: rule.name.to_s,
|
|
417
|
+
ns: { exact: ns_class.uri.to_s,
|
|
418
|
+
prefix: ns_class.prefix_default&.to_s } }
|
|
419
|
+
ns_attr_names << rule.name.to_s
|
|
420
|
+
return nil
|
|
421
|
+
end
|
|
422
|
+
|
|
334
423
|
claim[:plain] += 1
|
|
335
424
|
plan_attrs << { name: rule.name.to_s }
|
|
336
425
|
return nil
|
|
@@ -341,7 +430,8 @@ module Lutaml
|
|
|
341
430
|
|
|
342
431
|
claim[:ns] += 1
|
|
343
432
|
plan_attrs << { name: rule.name.to_s,
|
|
344
|
-
ns: { exact: type_ns.uri.to_s
|
|
433
|
+
ns: { exact: type_ns.uri.to_s,
|
|
434
|
+
prefix: type_ns.prefix_default&.to_s } }
|
|
345
435
|
ns_attr_names << rule.name.to_s
|
|
346
436
|
nil
|
|
347
437
|
end
|
|
@@ -349,12 +439,11 @@ module Lutaml
|
|
|
349
439
|
def compilable_mapping?(mapping)
|
|
350
440
|
# Mapping's uniform interface: root_mappings defaults to
|
|
351
441
|
# false on the base (KeyValue overrides with its own).
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return false if mapping.any_element_rule
|
|
442
|
+
# The map_any_element catch-all rides the engine's wildcard
|
|
443
|
+
# row (leptris#1552, named rows take precedence); engines
|
|
444
|
+
# without the kind keep the interpretive refusal.
|
|
445
|
+
return false if mapping.any_element_rule &&
|
|
446
|
+
!Leptris.wildcard_rows_compatible?
|
|
358
447
|
|
|
359
448
|
mapping.root_element && !mapping.root_mappings?
|
|
360
449
|
end
|
|
@@ -415,9 +504,32 @@ module Lutaml
|
|
|
415
504
|
# prefix. Blank-namespace rules (xmlns="") match :none.
|
|
416
505
|
def child_ns(rule, _model_ns)
|
|
417
506
|
uri = rule.namespace
|
|
418
|
-
return
|
|
507
|
+
return :none if uri.nil? || uri.to_s.empty?
|
|
508
|
+
|
|
509
|
+
{ exact: uri.to_s, prefix: rule.prefix&.to_s }
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
# Type-namespace ns form for a scalar element row, mirroring
|
|
513
|
+
# the interpretive resolve_element_namespace step 4: the
|
|
514
|
+
# attribute's type namespace qualifies the row when the rule
|
|
515
|
+
# declares none. Rows inheriting the tree namespace (type ns
|
|
516
|
+
# nil, :inherit, or equal to the model's own URI) get no form.
|
|
517
|
+
# :blank and alias families have no row-expressible form —
|
|
518
|
+
# the model stays interpretive.
|
|
519
|
+
def element_type_ns_form(rule, attr, register, model_ns)
|
|
520
|
+
return nil if rule.namespace_set?
|
|
419
521
|
|
|
420
|
-
|
|
522
|
+
type_ns = attr.type_namespace_class(register)
|
|
523
|
+
return nil if type_ns.nil? || type_ns == :inherit
|
|
524
|
+
return :bail_symbol if type_ns.is_a?(Symbol)
|
|
525
|
+
return :bail_aliases unless Leptris.attr_ns_rows_compatible? &&
|
|
526
|
+
type_ns.all_uris.size == 1
|
|
527
|
+
|
|
528
|
+
form = { exact: type_ns.uri.to_s,
|
|
529
|
+
prefix: type_ns.prefix_default&.to_s }
|
|
530
|
+
return nil if form[:exact] == model_ns&.[](:exact)
|
|
531
|
+
|
|
532
|
+
form
|
|
421
533
|
end
|
|
422
534
|
|
|
423
535
|
def content_rows(rows)
|
|
@@ -431,7 +543,10 @@ module Lutaml
|
|
|
431
543
|
# Mapping's uniform interface: namespace_class defaults
|
|
432
544
|
# to nil on the base (Xml overrides).
|
|
433
545
|
ns_class = mapping.namespace_class
|
|
434
|
-
|
|
546
|
+
return nil unless ns_class&.uri
|
|
547
|
+
|
|
548
|
+
{ exact: ns_class.uri.to_s,
|
|
549
|
+
prefix: ns_class.prefix_default&.to_s }
|
|
435
550
|
end
|
|
436
551
|
|
|
437
552
|
def scalar_type?(attr, register)
|
|
@@ -201,7 +201,7 @@ module Lutaml
|
|
|
201
201
|
v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
|
|
202
202
|
assign(kwargs, delegates, delegate, rule, attr, v)
|
|
203
203
|
end
|
|
204
|
-
when :
|
|
204
|
+
when :wildcard_any
|
|
205
205
|
# interpreted post-instance (interpret_deferred)
|
|
206
206
|
when :content
|
|
207
207
|
runs = content_runs(value)
|
|
@@ -352,6 +352,39 @@ module Lutaml
|
|
|
352
352
|
|
|
353
353
|
instance.public_send(:"#{attr.name}=",
|
|
354
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
|
+
)
|
|
355
388
|
when :polymorphic
|
|
356
389
|
results = deferred_elements(value, rule, buckets).map do |element|
|
|
357
390
|
attr.cast(element, :xml, register,
|
|
@@ -38,16 +38,135 @@ 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(instance, plan, register)
|
|
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
|
+
# Namespace spelling sourced from the interpretive writer's
|
|
65
|
+
# own machinery: NamespaceCollector + DeclarationPlanner
|
|
66
|
+
# decide prefix-vs-default per namespace exactly as the
|
|
67
|
+
# interpretive path does — no heuristic divergence (the
|
|
68
|
+
# uniword FontScheme/Image regressions). Option-bearing
|
|
69
|
+
# calls (prefix: true) never reach here; the serializer's
|
|
70
|
+
# plain spelling is the interpretive writer's plain
|
|
71
|
+
# spelling.
|
|
72
|
+
class Namespaces
|
|
73
|
+
attr_reader :unspellable
|
|
74
|
+
|
|
75
|
+
def initialize(instance, plan, register)
|
|
76
|
+
@forms = {}
|
|
77
|
+
@hoisted = {}
|
|
78
|
+
@declared = {}
|
|
79
|
+
@unspellable = false
|
|
80
|
+
mapping = plan[:mapping]
|
|
81
|
+
# The interpretive writer plans from the BUILT element
|
|
82
|
+
# tree: absent attributes (nil values) and absent
|
|
83
|
+
# children contribute no namespace needs, so an element
|
|
84
|
+
# whose own namespace would otherwise be prefix-hoisted
|
|
85
|
+
# (W3C: prefixed attributes need a prefix declaration)
|
|
86
|
+
# falls back to default-ns format. Collecting from the
|
|
87
|
+
# instance reproduces that — collecting from nil (type
|
|
88
|
+
# analysis) hoists every declared need and diverges
|
|
89
|
+
# (`<w:b xmlns:w>` vs the interpretive `<b xmlns>` when
|
|
90
|
+
# val=true omits the only prefixed attribute).
|
|
91
|
+
ns_prefix = if instance.is_a?(::Lutaml::Model::Serialize)
|
|
92
|
+
instance.xml_namespace_prefix
|
|
93
|
+
end
|
|
94
|
+
collect_options = { mapper_class: instance.class }
|
|
95
|
+
if ns_prefix && !ns_prefix.empty?
|
|
96
|
+
collect_options[:__xml_namespace_prefix] = ns_prefix
|
|
97
|
+
end
|
|
98
|
+
needs = Lutaml::Xml::NamespaceCollector.new(register).collect(
|
|
99
|
+
instance, mapping, **collect_options
|
|
100
|
+
)
|
|
101
|
+
planner = Lutaml::Xml::DeclarationPlanner.new(register)
|
|
102
|
+
declared_plan = planner.plan(
|
|
103
|
+
instance.class, mapping, needs, options: {}
|
|
104
|
+
)
|
|
105
|
+
declared_plan.root_node&.hoisted_declarations&.each do |prefix, uri|
|
|
106
|
+
@hoisted[uri] = prefix
|
|
107
|
+
@forms[uri] = { prefix: prefix, uri: uri }
|
|
108
|
+
end
|
|
109
|
+
collect(plan[:tree])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def unspellable?
|
|
113
|
+
@unspellable
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def collect(tree)
|
|
117
|
+
check(tree[:ns])
|
|
118
|
+
(tree[:attributes] || []).each { |a| check(a[:ns]) }
|
|
119
|
+
(tree[:children] || []).each do |row|
|
|
120
|
+
check(row[:ns]) if row[:ns].is_a?(Hash)
|
|
121
|
+
collect(row[:plan]) if row[:kind] == :nested && row[:plan]
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# An exact form the planner did not declare cannot be
|
|
126
|
+
# spelled — the serializer falls back interpretively.
|
|
127
|
+
def check(ns_form)
|
|
128
|
+
return if ns_form.nil? || ns_form == :none || ns_form == :any
|
|
129
|
+
|
|
130
|
+
uri = ns_form[:exact]
|
|
131
|
+
@unspellable = true if uri && !@forms.key?(uri)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The planner's hoisted prefix for a URI (nil when hoisted
|
|
135
|
+
# as the default namespace).
|
|
136
|
+
def prefix_for(uri)
|
|
137
|
+
@hoisted[uri]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The wire spelling for a local name under this form: the
|
|
141
|
+
# planner's hoisted prefix for the URI, or unprefixed when
|
|
142
|
+
# hoisted as the default namespace.
|
|
143
|
+
def spell(local, ns_form = nil)
|
|
144
|
+
uri = if ns_form.is_a?(Hash)
|
|
145
|
+
ns_form[:exact]
|
|
146
|
+
else
|
|
147
|
+
@forms.key?(ns_form) ? @forms[ns_form][:uri] : nil
|
|
148
|
+
end
|
|
149
|
+
prefix = uri ? @hoisted[uri] : nil
|
|
150
|
+
prefix ? "#{prefix}:#{local}" : local
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def form_for(ns_form)
|
|
154
|
+
return nil if ns_form.nil? || ns_form == :none
|
|
155
|
+
|
|
156
|
+
ns_form.is_a?(Hash) ? { prefix: @hoisted[ns_form[:exact]], uri: ns_form[:exact] } : @forms[ns_form]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def declare_on(root, _attr_form_prefix = nil)
|
|
160
|
+
@hoisted.each do |uri, prefix|
|
|
161
|
+
key = uri
|
|
162
|
+
next if key.nil? || @declared[key]
|
|
163
|
+
|
|
164
|
+
@declared[key] = true
|
|
165
|
+
root.add_namespace_definition(prefix, uri)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
51
170
|
# Whether the compiled plan is serialize-shaped. Ordered/mixed
|
|
52
171
|
# models serialize through build_ordered from element_order;
|
|
53
172
|
# plain models through plan rows.
|
|
@@ -63,9 +182,10 @@ module Lutaml
|
|
|
63
182
|
Lutaml::Model::Config.default_register
|
|
64
183
|
end
|
|
65
184
|
|
|
66
|
-
def build(element, instance, plan, doc, reg = nil
|
|
185
|
+
def build(element, instance, plan, doc, reg = nil, ns_ctx = nil,
|
|
186
|
+
inherited_ns = nil)
|
|
67
187
|
reg ||= register
|
|
68
|
-
write_attributes(element, instance, plan, reg)
|
|
188
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
69
189
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
70
190
|
value = value_of(instance, attr, delegate)
|
|
71
191
|
next unless rule.render?(value, instance)
|
|
@@ -76,33 +196,46 @@ module Lutaml
|
|
|
76
196
|
# several occurrences serializes one element per item,
|
|
77
197
|
# as the interpretive writer does. A plain scalar skips
|
|
78
198
|
# the Array() wrap — the common case.
|
|
199
|
+
row_ns = plan[:tree][:children]&.find do |r|
|
|
200
|
+
r[:name] == rule.name.to_s
|
|
201
|
+
end&.[](:ns) || inherited_ns
|
|
79
202
|
if value.is_a?(::Array)
|
|
80
203
|
value.each do |item|
|
|
81
|
-
add_leaf(element,
|
|
204
|
+
add_leaf(element,
|
|
205
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
82
206
|
attr.serialize(item, :xml, reg), doc,
|
|
83
207
|
attrs: rule.when_attribute)
|
|
84
208
|
end
|
|
85
209
|
else
|
|
86
|
-
add_leaf(element,
|
|
210
|
+
add_leaf(element,
|
|
211
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), row_ns) : row_name(rule, spelling),
|
|
87
212
|
attr.serialize(value, :xml, reg), doc,
|
|
88
213
|
attrs: rule.when_attribute)
|
|
89
214
|
end
|
|
90
215
|
when :collection_native, :collection_cb
|
|
216
|
+
row_ns = plan[:tree][:children]&.find do |r|
|
|
217
|
+
r[:name] == rule.name.to_s
|
|
218
|
+
end&.[](:ns) || inherited_ns
|
|
91
219
|
Array(value).each do |item|
|
|
92
|
-
add_leaf(element,
|
|
220
|
+
add_leaf(element,
|
|
221
|
+
ns_ctx ? ns_ctx.spell(rule.name.to_s, row_ns) : rule.name.to_s,
|
|
93
222
|
attr.serialize(item, :xml, reg), doc,
|
|
94
223
|
attrs: rule.when_attribute)
|
|
95
224
|
end
|
|
96
225
|
when :nested, :ordered_deferred
|
|
97
226
|
Array(value).each do |item|
|
|
98
227
|
child_plan = PlanCompiler.compile(item.class, reg)
|
|
99
|
-
|
|
228
|
+
child_ns = child_plan[:tree][:ns] || inherited_ns
|
|
229
|
+
child = element.create_child(
|
|
230
|
+
ns_ctx ? ns_ctx.spell(child_plan[:tree][:name], child_ns) : child_plan[:tree][:name],
|
|
231
|
+
)
|
|
232
|
+
ns_ctx&.collect(child_plan[:tree])
|
|
100
233
|
if child_plan[:ordered]
|
|
101
|
-
|
|
102
|
-
|
|
234
|
+
next unless build_ordered(child, item, child_plan,
|
|
235
|
+
doc, reg, ns_ctx, child_ns)
|
|
103
236
|
|
|
104
237
|
else
|
|
105
|
-
build(child, item, child_plan, doc, reg)
|
|
238
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns)
|
|
106
239
|
end
|
|
107
240
|
end
|
|
108
241
|
when :raw
|
|
@@ -121,9 +254,11 @@ module Lutaml
|
|
|
121
254
|
# the model are reflected; element_order text is the fallback.
|
|
122
255
|
# PIs drop (interpretive parity). Returns nil when a nested
|
|
123
256
|
# ordered child lacks element_order (caller falls back).
|
|
124
|
-
def build_ordered(element, instance, plan, doc, reg = nil
|
|
257
|
+
def build_ordered(element, instance, plan, doc, reg = nil,
|
|
258
|
+
ns_ctx = nil, _inherited_ns = nil)
|
|
125
259
|
reg ||= register
|
|
126
|
-
|
|
260
|
+
ns_ctx&.collect(plan[:tree])
|
|
261
|
+
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
127
262
|
rows_by_name = {}
|
|
128
263
|
plan[:rows].each do |rule, attr, kind, spelling, delegate|
|
|
129
264
|
next unless rule.name
|
|
@@ -213,8 +348,25 @@ module Lutaml
|
|
|
213
348
|
|
|
214
349
|
# Attributes in document order when the instance recorded it
|
|
215
350
|
# (parsed models), else plan row order.
|
|
216
|
-
def write_attributes(element, instance, plan,
|
|
351
|
+
def write_attributes(element, instance, plan, reg = nil,
|
|
352
|
+
ns_ctx = nil)
|
|
217
353
|
recorded = instance.attribute_order
|
|
354
|
+
attr_prefixes = {}
|
|
355
|
+
if ns_ctx
|
|
356
|
+
(plan[:tree][:attributes] || []).each do |a|
|
|
357
|
+
form = a[:ns].is_a?(Hash) ? a[:ns] : nil
|
|
358
|
+
# Namespaced attributes always spell prefixed (W3C:
|
|
359
|
+
# attributes cannot use the default namespace) — the
|
|
360
|
+
# interpretive writer emits the type namespace's own
|
|
361
|
+
# prefix even when the element hoists the same URI as
|
|
362
|
+
# the default namespace (updateFields w:val under
|
|
363
|
+
# xmlns="...").
|
|
364
|
+
attr_prefixes[a[:name]] =
|
|
365
|
+
if form
|
|
366
|
+
form[:prefix] || ns_ctx.prefix_for(form[:exact])
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
218
370
|
if recorded && !recorded.empty?
|
|
219
371
|
by_name = {}
|
|
220
372
|
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
@@ -223,24 +375,28 @@ module Lutaml
|
|
|
223
375
|
names = recorded |
|
|
224
376
|
plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
|
|
225
377
|
names.each do |name|
|
|
226
|
-
write_attribute(element, instance, name, by_name[name]
|
|
378
|
+
write_attribute(element, instance, name, by_name[name],
|
|
379
|
+
reg, attr_prefixes[name])
|
|
227
380
|
end
|
|
228
381
|
else
|
|
229
382
|
plan[:attr_rows].each do |rule, attr, _sp, delegate|
|
|
230
383
|
write_attribute(element, instance, rule.name.to_s,
|
|
231
|
-
[rule, attr, delegate]
|
|
384
|
+
[rule, attr, delegate], reg,
|
|
385
|
+
attr_prefixes[rule.name.to_s])
|
|
232
386
|
end
|
|
233
387
|
end
|
|
234
388
|
end
|
|
235
389
|
|
|
236
|
-
def write_attribute(element, instance, name, entry, reg = nil
|
|
390
|
+
def write_attribute(element, instance, name, entry, reg = nil,
|
|
391
|
+
prefix = nil)
|
|
237
392
|
return unless entry
|
|
238
393
|
|
|
239
394
|
_rule, attr, delegate = entry
|
|
240
395
|
value = value_of(instance, attr, delegate)
|
|
241
396
|
return if value.nil?
|
|
242
397
|
|
|
243
|
-
|
|
398
|
+
spelled = prefix ? "#{prefix}:#{name}" : name
|
|
399
|
+
element.set_attribute(spelled,
|
|
244
400
|
attr.serialize(value, :xml, reg || register).to_s)
|
|
245
401
|
end
|
|
246
402
|
|
|
@@ -16,6 +16,7 @@ module Lutaml
|
|
|
16
16
|
register = Lutaml::Model::Config.default_register
|
|
17
17
|
plan = PlanCompiler.compile(model_class, register)
|
|
18
18
|
return nil unless plan
|
|
19
|
+
return nil unless PlanCompiler.engine_walk_safe?(plan)
|
|
19
20
|
|
|
20
21
|
document = ::Leptris::XML.parse(xml)
|
|
21
22
|
root = document.root
|
data/lib/lutaml/xml/leptris.rb
CHANGED
|
@@ -21,6 +21,27 @@ module Lutaml
|
|
|
21
21
|
# type-namespaced attributes on the interpretive path.
|
|
22
22
|
MIN_LEPTRIS_ATTR_NS_VERSION = "1.9.289.0"
|
|
23
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
|
+
|
|
36
|
+
# Walk-side namespace safety (leptris#1587, closing #1585 and
|
|
37
|
+
# #1586): child-row exact-URI ns_uri is retained engine-side
|
|
38
|
+
# (deep-copied at build, freed with the plan) and plain attribute
|
|
39
|
+
# rows leniently match namespace-qualified wire attributes
|
|
40
|
+
# (exact-first-then-any, top level and nested). Both shipped in
|
|
41
|
+
# 1.9.320.0; older engines keep the affected models interpretive
|
|
42
|
+
# through PlanCompiler.engine_walk_safe?.
|
|
43
|
+
MIN_LEPTRIS_WALK_NS_SAFETY_VERSION = "1.9.320.0"
|
|
44
|
+
|
|
24
45
|
def self.plan_path_compatible?
|
|
25
46
|
return false unless defined?(Gem)
|
|
26
47
|
|
|
@@ -35,6 +56,32 @@ module Lutaml
|
|
|
35
56
|
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_ATTR_NS_VERSION)
|
|
36
57
|
end
|
|
37
58
|
|
|
59
|
+
def self.nested_attr_capture_compatible?
|
|
60
|
+
return false unless defined?(Gem)
|
|
61
|
+
|
|
62
|
+
spec = Gem.loaded_specs["leptris"]
|
|
63
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_NESTED_ATTR_VERSION)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.wildcard_rows_compatible?
|
|
67
|
+
return false unless defined?(Gem)
|
|
68
|
+
|
|
69
|
+
spec = Gem.loaded_specs["leptris"]
|
|
70
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WILDCARD_VERSION)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Memoized: consulted per parse on the walk gate hot path, and
|
|
74
|
+
# the loaded leptris spec cannot change within a process.
|
|
75
|
+
@walk_ns_safety = nil
|
|
76
|
+
def self.walk_ns_safety_compatible?
|
|
77
|
+
return @walk_ns_safety unless @walk_ns_safety.nil?
|
|
78
|
+
return (@walk_ns_safety = false) unless defined?(Gem)
|
|
79
|
+
|
|
80
|
+
spec = Gem.loaded_specs["leptris"]
|
|
81
|
+
@walk_ns_safety =
|
|
82
|
+
!!(spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WALK_NS_SAFETY_VERSION))
|
|
83
|
+
end
|
|
84
|
+
|
|
38
85
|
autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
|
|
39
86
|
autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
|
|
40
87
|
autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
|