lutaml-model 0.8.100 → 0.8.119
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/Gemfile +6 -5
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +28 -0
- data/lib/lutaml/key_value/transform.rb +4 -0
- data/lib/lutaml/key_value/transformation.rb +10 -0
- data/lib/lutaml/model/adapter_resolver.rb +13 -5
- data/lib/lutaml/model/attribute.rb +77 -3
- data/lib/lutaml/model/cached_type_resolver.rb +13 -6
- data/lib/lutaml/model/configuration.rb +13 -0
- data/lib/lutaml/model/serialize/builder.rb +22 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +21 -3
- data/lib/lutaml/model/serialize/initialization.rb +14 -1
- data/lib/lutaml/model/type_resolver.rb +7 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +1 -0
- data/lib/lutaml/xml/adapter/xml_parser.rb +5 -45
- data/lib/lutaml/xml/leptris/native_emission.rb +187 -0
- data/lib/lutaml/xml/leptris/plan_compiler.rb +161 -102
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +276 -58
- data/lib/lutaml/xml/leptris/plan_serializer.rb +309 -47
- data/lib/lutaml/xml/leptris/plan_walk.rb +6 -3
- data/lib/lutaml/xml/leptris.rb +25 -84
- data/lib/lutaml/xml/model_transform.rb +5 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +15 -6
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +29 -0
- data/lutaml-model.gemspec +3 -3
- data/spec/lutaml/json/yeptris_adapter_spec.rb +3 -10
- data/spec/lutaml/model/adapter_resolver_configured_spec.rb +32 -8
- data/spec/lutaml/model/cast_element_form_spec.rb +54 -0
- data/spec/lutaml/model/collection_strict_assignment_spec.rb +55 -0
- data/spec/lutaml/model/from_json_scalar_cast_spec.rb +70 -0
- data/spec/lutaml/model/singular_tolerance_spec.rb +159 -0
- data/spec/lutaml/model/unregistered_context_resolution_spec.rb +34 -0
- data/spec/lutaml/model/yeptris_native_detection_spec.rb +83 -0
- data/spec/lutaml/xml/adapter/empty_document_diagnostics_spec.rb +50 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +34 -6
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +0 -11
- data/spec/lutaml/xml/plan_uri_exact_dispatch_spec.rb +287 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +0 -9
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +3 -10
- metadata +19 -10
|
@@ -18,12 +18,30 @@ module Lutaml
|
|
|
18
18
|
# order applier — including its content-attr-first text reads
|
|
19
19
|
# (mutations to the model show up) and CDATA flattening. Models
|
|
20
20
|
# without element_order fall back interpretively (return nil).
|
|
21
|
+
# Writer-added values the order snapshot does not cover ride the
|
|
22
|
+
# OrderReconciler: the walk consumes the reconciled order and
|
|
23
|
+
# unplaceable rules emit appended (the interpretive
|
|
24
|
+
# apply_remaining_rules contract).
|
|
21
25
|
module PlanSerializer
|
|
22
26
|
SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
|
|
23
27
|
nested ordered_deferred raw content
|
|
24
28
|
content_deferred].freeze
|
|
29
|
+
PLAN_SERIALIZABLE_KINDS = SERIALIZABLE_KINDS
|
|
25
30
|
CONTENT_KINDS = %i[content content_deferred].freeze
|
|
26
31
|
|
|
32
|
+
# Reconciler host: the interpretive support modules are written
|
|
33
|
+
# against the Transformation's combined surface (the reconciler
|
|
34
|
+
# consumes applier helpers such as find_rule_for_element and
|
|
35
|
+
# rule_discriminator), so the plan lane drives them through a
|
|
36
|
+
# dedicated host holding both. The helpers are pure functions
|
|
37
|
+
# of their arguments — the frozen host carries no state.
|
|
38
|
+
class ReconcilerHost
|
|
39
|
+
include(TransformationSupport::OrderedApplier)
|
|
40
|
+
include(TransformationSupport::OrderReconciler)
|
|
41
|
+
include(TransformationSupport::SkipLogic)
|
|
42
|
+
end
|
|
43
|
+
RECONCILER = ReconcilerHost.new.freeze
|
|
44
|
+
|
|
27
45
|
class << self
|
|
28
46
|
# plan: the compiler's entry for instance's class
|
|
29
47
|
def call(instance, plan, register = nil)
|
|
@@ -37,6 +55,20 @@ module Lutaml
|
|
|
37
55
|
)
|
|
38
56
|
return nil if plan[:ordered] && instance.element_order.nil?
|
|
39
57
|
|
|
58
|
+
# The native lane first: purely namespace-less trees emit
|
|
59
|
+
# through the engine's result builder and writers, all-C
|
|
60
|
+
# after the ops list — 2.4x the DOM-mint loop on the
|
|
61
|
+
# audit corpus. Everything else keeps the DOM lane below
|
|
62
|
+
# (namespaced spellings are instance-dependent; ordered,
|
|
63
|
+
# wildcard, raw, and callback shapes are not
|
|
64
|
+
# ops-expressible).
|
|
65
|
+
# element_order is the order contract: a model carrying it —
|
|
66
|
+
# parse-captured or programmatically tracked (#977) — must
|
|
67
|
+
# serialize through the ordered walk, not the flat
|
|
68
|
+
# schema-order native ops layout.
|
|
69
|
+
return NativeEmission.emit(instance, plan, register) if
|
|
70
|
+
NativeEmission.compatible?(plan) && instance.element_order.nil?
|
|
71
|
+
|
|
40
72
|
doc = ::Leptris::XML::Document.create
|
|
41
73
|
ns_ctx = Namespaces.new(instance, plan, register)
|
|
42
74
|
return nil if ns_ctx.unspellable?
|
|
@@ -46,11 +78,25 @@ module Lutaml
|
|
|
46
78
|
)
|
|
47
79
|
doc.root = root
|
|
48
80
|
ns_ctx.declare_on(root)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
# The interpretive writer's raw schemaLocation re-emission
|
|
82
|
+
# (handle_schema_location): the parse-captured string rides
|
|
83
|
+
# back as xsi:schemaLocation with its declaration.
|
|
84
|
+
if instance.is_a?(::Lutaml::Model::Serialize) &&
|
|
85
|
+
instance.raw_schema_location &&
|
|
86
|
+
!instance.raw_schema_location.empty?
|
|
87
|
+
root.set_attribute(
|
|
88
|
+
"xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"
|
|
89
|
+
)
|
|
90
|
+
root.set_attribute("xsi:schemaLocation",
|
|
91
|
+
instance.raw_schema_location)
|
|
53
92
|
end
|
|
93
|
+
built = if plan[:ordered] || !instance.element_order.nil?
|
|
94
|
+
build_ordered(root, instance, plan, doc, register, ns_ctx)
|
|
95
|
+
else
|
|
96
|
+
build(root, instance, plan, doc, register, ns_ctx)
|
|
97
|
+
end
|
|
98
|
+
return nil unless built
|
|
99
|
+
|
|
54
100
|
doc.to_xml(indent: 2, no_decl: true)
|
|
55
101
|
end
|
|
56
102
|
|
|
@@ -106,6 +152,38 @@ module Lutaml
|
|
|
106
152
|
@hoisted[uri] = prefix
|
|
107
153
|
@forms[uri] = { prefix: prefix, uri: uri }
|
|
108
154
|
end
|
|
155
|
+
# Format preservation (the interpretive writer's
|
|
156
|
+
# rule): an instance parsed from a PREFIXED source keeps
|
|
157
|
+
# its prefixes — the model's own namespace hoists with
|
|
158
|
+
# the instance's recorded prefix even when the fresh
|
|
159
|
+
# planner preferred the default-ns format.
|
|
160
|
+
model_uri = plan[:tree][:ns] && plan[:tree][:ns][:exact]
|
|
161
|
+
if ns_prefix && !ns_prefix.empty? && model_uri &&
|
|
162
|
+
@hoisted.key?(model_uri)
|
|
163
|
+
@hoisted[model_uri] = ns_prefix
|
|
164
|
+
@forms[model_uri] = { prefix: ns_prefix, uri: model_uri }
|
|
165
|
+
end
|
|
166
|
+
# Source declaration preservation (the interpretive
|
|
167
|
+
# writer's stored-plan merge): prefixes the parsed source
|
|
168
|
+
# declared are kept for URIs the document emits. The
|
|
169
|
+
# planner's class-analysis path has no stored-plan
|
|
170
|
+
# access, so the merge reads the instance's recorded plan
|
|
171
|
+
# directly.
|
|
172
|
+
stored = if instance.is_a?(::Lutaml::Model::Serialize)
|
|
173
|
+
instance.import_declaration_plan
|
|
174
|
+
end
|
|
175
|
+
if stored.is_a?(Lutaml::Xml::DeclarationPlan) &&
|
|
176
|
+
stored.root_node.is_a?(
|
|
177
|
+
Lutaml::Xml::DeclarationPlan::ElementNode,
|
|
178
|
+
) && (ns_prefix.nil? || ns_prefix.empty?)
|
|
179
|
+
original_uris = stored.original_namespace_uris || {}
|
|
180
|
+
stored.root_node.hoisted_declarations.each do |prefix, uri|
|
|
181
|
+
effective_uri = original_uris[uri] || uri
|
|
182
|
+
already = @hoisted.value?(effective_uri) ||
|
|
183
|
+
@hoisted.value?(uri)
|
|
184
|
+
@hoisted[prefix] = uri if already && !@hoisted.key?(prefix)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
109
187
|
collect(plan[:tree])
|
|
110
188
|
end
|
|
111
189
|
|
|
@@ -156,14 +234,18 @@ module Lutaml
|
|
|
156
234
|
ns_form.is_a?(Hash) ? { prefix: @hoisted[ns_form[:exact]], uri: ns_form[:exact] } : @forms[ns_form]
|
|
157
235
|
end
|
|
158
236
|
|
|
237
|
+
# Declaration order matches the interpretive writer: the
|
|
238
|
+
# prefixed namespaces sorted by prefix, the default
|
|
239
|
+
# namespace last.
|
|
159
240
|
def declare_on(root, _attr_form_prefix = nil)
|
|
160
|
-
@hoisted.
|
|
161
|
-
|
|
162
|
-
|
|
241
|
+
@hoisted.sort_by { |_uri, prefix| [prefix ? 0 : 1, prefix.to_s] }
|
|
242
|
+
.each do |uri, prefix|
|
|
243
|
+
key = uri
|
|
244
|
+
next if key.nil? || @declared[key]
|
|
163
245
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
246
|
+
@declared[key] = true
|
|
247
|
+
root.add_namespace_definition(prefix, uri)
|
|
248
|
+
end
|
|
167
249
|
end
|
|
168
250
|
end
|
|
169
251
|
|
|
@@ -172,7 +254,7 @@ module Lutaml
|
|
|
172
254
|
# plain models through plan rows.
|
|
173
255
|
def serializable?(plan)
|
|
174
256
|
plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
|
|
175
|
-
|
|
257
|
+
PLAN_SERIALIZABLE_KINDS.include?(kind)
|
|
176
258
|
end
|
|
177
259
|
end
|
|
178
260
|
|
|
@@ -222,25 +304,35 @@ module Lutaml
|
|
|
222
304
|
end
|
|
223
305
|
when :nested, :ordered_deferred
|
|
224
306
|
Array(value).each do |item|
|
|
225
|
-
|
|
226
|
-
|
|
307
|
+
# Keyed by [class, row name]: distinct rows (sz, szCs)
|
|
308
|
+
# can share one class with different root names.
|
|
309
|
+
child_plan = nested_plans[[item.class, rule.name.to_s]] ||
|
|
310
|
+
(nested_plans[[item.class, rule.name.to_s]] = PlanCompiler.compile(item.class, reg))
|
|
227
311
|
child_ns = child_plan[:tree][:ns] || inherited_ns
|
|
312
|
+
# The ROW's wire name wins over the child plan's
|
|
313
|
+
# root — distinct rows (sz, szCs) share one class.
|
|
228
314
|
child = element.create_child(
|
|
229
|
-
ns_ctx ? ns_ctx.spell(
|
|
315
|
+
ns_ctx ? ns_ctx.spell(row_name(rule, spelling), child_ns) : row_name(rule, spelling),
|
|
230
316
|
)
|
|
231
317
|
ns_ctx&.collect(child_plan[:tree])
|
|
232
|
-
if child_plan[:ordered]
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
318
|
+
if child_plan[:ordered] || !item.element_order.nil?
|
|
319
|
+
build_ordered(child, item, child_plan,
|
|
320
|
+
doc, reg, ns_ctx, child_ns) || return
|
|
236
321
|
else
|
|
237
|
-
build(child, item, child_plan, doc, reg, ns_ctx, child_ns)
|
|
322
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
238
323
|
end
|
|
239
324
|
end
|
|
240
325
|
when :raw
|
|
241
326
|
Array(value).each { |raw| element.add_child(raw.to_s) }
|
|
327
|
+
when :child_deferred
|
|
328
|
+
# A plan-less child carrying a value (a cycle point
|
|
329
|
+
# with data, e.g. bibliography's Author → author):
|
|
330
|
+
# the interpretive serializer owns the model — the
|
|
331
|
+
# plan lane has no faithful spelling for it. Empty
|
|
332
|
+
# deferred rows ride the fast lane untouched.
|
|
333
|
+
return nil unless Array(value).empty?
|
|
242
334
|
when :content, :content_deferred
|
|
243
|
-
Array(value).each { |run| element
|
|
335
|
+
Array(value).each { |run| append_text(element, run.to_s, doc) }
|
|
244
336
|
end
|
|
245
337
|
end
|
|
246
338
|
end
|
|
@@ -264,30 +356,96 @@ module Lutaml
|
|
|
264
356
|
plan[:nested_plans] ||= {}
|
|
265
357
|
end
|
|
266
358
|
|
|
359
|
+
# The row's exact element URI — the child type's namespace,
|
|
360
|
+
# the same source the interpretive matcher dispatches by for
|
|
361
|
+
# (URI, local) element identity. Nil for scalar/raw/content
|
|
362
|
+
# rows (any-qualification binding). Memoized per plan (rules
|
|
363
|
+
# are stable per compiled plan).
|
|
364
|
+
def row_exact_uri(rule, attr, kind, type, reg, plan)
|
|
365
|
+
return nil unless %i[nested ordered_deferred].include?(kind)
|
|
366
|
+
|
|
367
|
+
cache = plan[:row_exact_uris] ||= {}.compare_by_identity
|
|
368
|
+
return cache[rule] if cache.key?(rule)
|
|
369
|
+
|
|
370
|
+
child_type = type || attr.type(reg)
|
|
371
|
+
child_plan = child_type && PlanCompiler.compile(child_type, reg)
|
|
372
|
+
tree_ns = child_plan && child_plan[:tree][:ns]
|
|
373
|
+
cache[rule] = tree_ns.is_a?(Hash) ? tree_ns[:exact] : tree_ns
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def ordered_layout_for(plan, inherited_ns, ns_index, reg)
|
|
377
|
+
cache = plan[:ordered_layouts] ||= {}.compare_by_identity
|
|
378
|
+
return cache[inherited_ns] if cache.key?(inherited_ns)
|
|
379
|
+
|
|
380
|
+
rows_by_name = {}
|
|
381
|
+
name_keys = ::Hash.new { |h, k| h[k] = [] }
|
|
382
|
+
plan[:rows].each do |rule, attr, kind, spelling, delegate, type|
|
|
383
|
+
next unless rule.name
|
|
384
|
+
|
|
385
|
+
exact = row_exact_uri(rule, attr, kind, type, reg, plan)
|
|
386
|
+
name = rule.name.to_s
|
|
387
|
+
key = exact ? [exact, name] : [nil, name]
|
|
388
|
+
rows_by_name[key] ||= [rule, attr, kind, spelling,
|
|
389
|
+
delegate,
|
|
390
|
+
ns_index ? ns_index[name] || inherited_ns : inherited_ns]
|
|
391
|
+
name_keys[name] << key
|
|
392
|
+
end
|
|
393
|
+
name_keys.default = nil
|
|
394
|
+
cache[inherited_ns] = [rows_by_name, name_keys]
|
|
395
|
+
end
|
|
396
|
+
|
|
267
397
|
# Element_order-driven emission (the interpretive order
|
|
268
398
|
# applier's contract): text runs and comments verbatim, each
|
|
269
399
|
# element entry consuming the next item of its collection
|
|
270
400
|
# (document order). Text reads prefer the content attribute
|
|
271
401
|
# when it lines up with the text-node count, so mutations to
|
|
272
402
|
# the model are reflected; element_order text is the fallback.
|
|
273
|
-
# PIs drop (interpretive parity).
|
|
274
|
-
#
|
|
403
|
+
# PIs drop (interpretive parity). A nested ordered instance
|
|
404
|
+
# without element_order (built by Ruby, not parsed) falls
|
|
405
|
+
# back to declaration order — the interpretive behavior.
|
|
275
406
|
def build_ordered(element, instance, plan, doc, reg = nil,
|
|
276
|
-
ns_ctx = nil,
|
|
407
|
+
ns_ctx = nil, inherited_ns = nil)
|
|
408
|
+
if instance.element_order.nil?
|
|
409
|
+
return build(element, instance, plan, doc, reg, ns_ctx,
|
|
410
|
+
inherited_ns)
|
|
411
|
+
end
|
|
412
|
+
|
|
277
413
|
reg ||= register
|
|
278
414
|
ns_ctx&.collect(plan[:tree])
|
|
279
415
|
write_attributes(element, instance, plan, reg, ns_ctx)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
416
|
+
ns_index = ns_ctx ? ns_index_for(plan) : nil
|
|
417
|
+
# (URI, local) row keys — the interpretive matcher's
|
|
418
|
+
# typed-element identity: same-name rows in different
|
|
419
|
+
# namespaces (w14:docId vs w15:docId) must not share one
|
|
420
|
+
# dispatch slot. Name-keyed rows (no exact URI) bind any
|
|
421
|
+
# qualification. The layout is static per (plan, inherited
|
|
422
|
+
# ns) — the reconciliation corpus re-serializes thousands
|
|
423
|
+
# of same-shaped paragraphs, so it is memoized.
|
|
424
|
+
rows_by_name, name_keys =
|
|
425
|
+
ordered_layout_for(plan, inherited_ns, ns_index, reg)
|
|
287
426
|
content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
|
|
288
427
|
CONTENT_KINDS.include?(kind) && rule.name.nil?
|
|
289
428
|
end
|
|
290
|
-
order
|
|
429
|
+
# Writer-added values sit outside the order snapshot — the
|
|
430
|
+
# reconciler inserts their entries at the interpretive
|
|
431
|
+
# anchors and returns the rules it could not place for
|
|
432
|
+
# appended emission (silent drops are data loss). The
|
|
433
|
+
# reconciler speaks CompiledRule — the interpretive
|
|
434
|
+
# transformation's memoized set; the plan's MappingRule
|
|
435
|
+
# rows cannot drive it. Pristine parsed instances skip the
|
|
436
|
+
# scan entirely: the parse-built order covers every set
|
|
437
|
+
# value, and the writer-set flag is the only flow that can
|
|
438
|
+
# produce deficits.
|
|
439
|
+
rules = if instance.mutated_after_parse?
|
|
440
|
+
compiled_rules_for(instance.class, reg)
|
|
441
|
+
end
|
|
442
|
+
if rules
|
|
443
|
+
order, fallback_rules =
|
|
444
|
+
RECONCILER.reconciled_element_order(instance, rules, {})
|
|
445
|
+
else
|
|
446
|
+
order = instance.element_order
|
|
447
|
+
fallback_rules = []
|
|
448
|
+
end
|
|
291
449
|
element_indices = ::Hash.new(0)
|
|
292
450
|
content_attr = content_entry && content_entry[1]
|
|
293
451
|
content_value = content_attr &&
|
|
@@ -321,50 +479,136 @@ module Lutaml
|
|
|
321
479
|
when "Comment"
|
|
322
480
|
element.add_child(doc.create_comment(object.text_content))
|
|
323
481
|
when "Element"
|
|
324
|
-
entry
|
|
482
|
+
# A uniquely-named row absorbs the entry at any
|
|
483
|
+
# qualification (the interpretive matcher's lenient
|
|
484
|
+
# binding — order entries written by consumers carry no
|
|
485
|
+
# namespace). Contested names dispatch strictly by
|
|
486
|
+
# (URI, local), falling back to URI-less rows.
|
|
487
|
+
name = object.name
|
|
488
|
+
# Order entries may carry names with no mapped row (the
|
|
489
|
+
# parse records every child in document order); the
|
|
490
|
+
# layout cache kills its default proc to stay
|
|
491
|
+
# mutation-free, so a miss reads nil and drops below.
|
|
492
|
+
keys = name_keys[name]
|
|
493
|
+
if keys&.length == 1
|
|
494
|
+
dispatch_key = keys.first
|
|
495
|
+
entry = rows_by_name[dispatch_key]
|
|
496
|
+
else
|
|
497
|
+
dispatch_key = [object.namespace_uri, name]
|
|
498
|
+
entry = rows_by_name[dispatch_key]
|
|
499
|
+
unless entry
|
|
500
|
+
dispatch_key = [nil, name]
|
|
501
|
+
entry = rows_by_name[dispatch_key]
|
|
502
|
+
end
|
|
503
|
+
end
|
|
325
504
|
next unless entry
|
|
326
505
|
|
|
327
|
-
_, attr, kind, _spelling, delegate = entry
|
|
506
|
+
_, attr, kind, _spelling, delegate, row_ns = entry
|
|
328
507
|
value = value_of(instance, attr, delegate)
|
|
329
508
|
case kind
|
|
330
509
|
when :collection_native, :collection_cb
|
|
331
|
-
index = element_indices[
|
|
510
|
+
index = element_indices[dispatch_key]
|
|
332
511
|
items = Array(value)
|
|
333
512
|
next unless index < items.length
|
|
334
513
|
|
|
335
|
-
element_indices[
|
|
336
|
-
add_leaf(element,
|
|
514
|
+
element_indices[dispatch_key] += 1
|
|
515
|
+
add_leaf(element,
|
|
516
|
+
ns_ctx ? ns_ctx.spell(object.name, row_ns) : object.name,
|
|
337
517
|
attr.serialize(items[index], :xml, reg), doc)
|
|
338
518
|
when :nested, :ordered_deferred
|
|
339
519
|
item = if attr.collection?
|
|
340
|
-
index = element_indices[
|
|
341
|
-
element_indices[
|
|
520
|
+
index = element_indices[dispatch_key]
|
|
521
|
+
element_indices[dispatch_key] += 1
|
|
342
522
|
Array(value)[index]
|
|
343
523
|
else
|
|
344
524
|
value
|
|
345
525
|
end
|
|
346
526
|
next unless item
|
|
347
527
|
|
|
348
|
-
child_plan = nested_plans_for(plan)[item.class] ||
|
|
349
|
-
(nested_plans_for(plan)[item.class] = PlanCompiler.compile(item.class, reg))
|
|
350
|
-
|
|
351
|
-
|
|
528
|
+
child_plan = nested_plans_for(plan)[[item.class, object.name]] ||
|
|
529
|
+
(nested_plans_for(plan)[[item.class, object.name]] = PlanCompiler.compile(item.class, reg))
|
|
530
|
+
child_ns = child_plan[:tree][:ns] || row_ns || inherited_ns
|
|
531
|
+
child = element.create_child(
|
|
532
|
+
ns_ctx ? ns_ctx.spell(object.name, child_ns) : object.name,
|
|
533
|
+
)
|
|
534
|
+
if child_plan[:ordered] || !item.element_order.nil?
|
|
352
535
|
return nil unless build_ordered(child, item, child_plan,
|
|
353
|
-
doc, reg)
|
|
536
|
+
doc, reg, ns_ctx, child_ns)
|
|
354
537
|
else
|
|
355
|
-
build(child, item, child_plan, doc, reg)
|
|
538
|
+
build(child, item, child_plan, doc, reg, ns_ctx, child_ns) || return
|
|
356
539
|
end
|
|
357
540
|
when :raw
|
|
358
541
|
element.add_child(value.to_s) unless value.nil?
|
|
542
|
+
when :child_deferred
|
|
543
|
+
# Valued cycle-point children: interpretive lane (see
|
|
544
|
+
# build).
|
|
545
|
+
return nil unless Array(value).empty?
|
|
359
546
|
when :scalar
|
|
360
|
-
add_leaf(element,
|
|
547
|
+
add_leaf(element,
|
|
548
|
+
ns_ctx ? ns_ctx.spell(object.name, row_ns) : object.name,
|
|
361
549
|
attr.serialize(value, :xml, reg), doc)
|
|
362
550
|
end
|
|
363
551
|
end
|
|
364
552
|
end
|
|
553
|
+
return nil if !fallback_rules.empty? &&
|
|
554
|
+
!emit_uncovered_values(element, instance, plan, fallback_rules,
|
|
555
|
+
doc, reg, ns_ctx, inherited_ns)
|
|
556
|
+
|
|
557
|
+
true
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# OrderReconciler fallback: element rules the reconciled order
|
|
561
|
+
# could not place but that still hold a value — emit them
|
|
562
|
+
# appended, the ordinary way (the interpretive
|
|
563
|
+
# apply_remaining_rules contract for the plan row model).
|
|
564
|
+
def emit_uncovered_values(element, instance, plan, fallback, doc,
|
|
565
|
+
reg, ns_ctx, inherited_ns)
|
|
566
|
+
by_attr = {}
|
|
567
|
+
plan[:rows].each { |row| by_attr[row[1].name] ||= row if row[1] }
|
|
568
|
+
fallback.each do |rule|
|
|
569
|
+
entry = by_attr[rule.attribute_name]
|
|
570
|
+
next unless entry
|
|
571
|
+
|
|
572
|
+
_rule, attr, kind, _spelling, delegate, row_ns = entry
|
|
573
|
+
value = value_of(instance, attr, delegate)
|
|
574
|
+
name = rule.name.to_s
|
|
575
|
+
Array(value).each do |item|
|
|
576
|
+
case kind
|
|
577
|
+
when :collection_native, :collection_cb, :scalar
|
|
578
|
+
add_leaf(element,
|
|
579
|
+
ns_ctx ? ns_ctx.spell(name, row_ns) : name,
|
|
580
|
+
attr.serialize(item, :xml, reg), doc)
|
|
581
|
+
when :nested, :ordered_deferred
|
|
582
|
+
next if item.nil?
|
|
583
|
+
|
|
584
|
+
child_plan =
|
|
585
|
+
nested_plans_for(plan)[[item.class, name]] ||
|
|
586
|
+
(nested_plans_for(plan)[[item.class, name]] =
|
|
587
|
+
PlanCompiler.compile(item.class, reg))
|
|
588
|
+
child_ns = child_plan[:tree][:ns] || row_ns || inherited_ns
|
|
589
|
+
child = element.create_child(
|
|
590
|
+
ns_ctx ? ns_ctx.spell(name, child_ns) : name,
|
|
591
|
+
)
|
|
592
|
+
if child_plan[:ordered] || !item.element_order.nil?
|
|
593
|
+
return nil unless build_ordered(child, item, child_plan,
|
|
594
|
+
doc, reg, ns_ctx, child_ns)
|
|
595
|
+
else
|
|
596
|
+
built = build(child, item, child_plan, doc, reg, ns_ctx,
|
|
597
|
+
child_ns)
|
|
598
|
+
return nil unless built
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
# Valued cycle-point rows keep the walk's contract.
|
|
603
|
+
return nil if kind == :child_deferred && !Array(value).empty?
|
|
604
|
+
end
|
|
365
605
|
true
|
|
366
606
|
end
|
|
367
607
|
|
|
608
|
+
def compiled_rules_for(klass, reg)
|
|
609
|
+
klass.transformation_for(:xml, reg)&.compiled_rules
|
|
610
|
+
end
|
|
611
|
+
|
|
368
612
|
# Attributes in document order when the instance recorded it
|
|
369
613
|
# (parsed models), else plan row order.
|
|
370
614
|
def write_attributes(element, instance, plan, reg = nil,
|
|
@@ -411,9 +655,10 @@ module Lutaml
|
|
|
411
655
|
prefix = nil)
|
|
412
656
|
return unless entry
|
|
413
657
|
|
|
414
|
-
|
|
658
|
+
rule, attr, delegate = entry
|
|
415
659
|
value = value_of(instance, attr, delegate)
|
|
416
660
|
return if value.nil?
|
|
661
|
+
return unless rule.render?(value, instance)
|
|
417
662
|
|
|
418
663
|
spelled = prefix ? "#{prefix}:#{name}" : name
|
|
419
664
|
element.set_attribute(spelled,
|
|
@@ -426,10 +671,27 @@ module Lutaml
|
|
|
426
671
|
def add_leaf(element, name, value, doc, attrs: nil)
|
|
427
672
|
child = element.create_child(name)
|
|
428
673
|
attrs&.each { |k, v| child[k.to_s] = v.to_s }
|
|
429
|
-
child
|
|
674
|
+
append_text(child, value.to_s, doc) unless value.nil?
|
|
430
675
|
child
|
|
431
676
|
end
|
|
432
677
|
|
|
678
|
+
# Text attach: the address face (leptris#408 adoption)
|
|
679
|
+
# passes raw node addresses — the pointer face mints an
|
|
680
|
+
# FFI::Pointer wrapper per fresh node, the last per-row
|
|
681
|
+
# Ruby-side allocation on the DOM build.
|
|
682
|
+
def append_text(parent, text, doc)
|
|
683
|
+
node = doc.create_text_node(text)
|
|
684
|
+
status = ::Leptris::XML::FFI.leptris_element_add_child_addr(
|
|
685
|
+
parent.c_address, node.c_address
|
|
686
|
+
)
|
|
687
|
+
unless status.zero?
|
|
688
|
+
raise ::Leptris::XML::Error,
|
|
689
|
+
"text attach failed (status=#{status})"
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
node
|
|
693
|
+
end
|
|
694
|
+
|
|
433
695
|
def value_of(instance, attr, delegate)
|
|
434
696
|
if delegate
|
|
435
697
|
target = instance.public_send(delegate)
|
|
@@ -12,11 +12,10 @@ module Lutaml
|
|
|
12
12
|
class << self
|
|
13
13
|
# Build the plan, walk the document, and hydrate the mapped
|
|
14
14
|
# root. Returns nil when the model is not plan-compilable.
|
|
15
|
-
def call(model_class, xml,
|
|
15
|
+
def call(model_class, xml, options = {})
|
|
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)
|
|
20
19
|
|
|
21
20
|
document = ::Leptris::XML.parse(xml)
|
|
22
21
|
root = document.root
|
|
@@ -24,7 +23,11 @@ module Lutaml
|
|
|
24
23
|
|
|
25
24
|
PlanHydrator.call(model_class, plan,
|
|
26
25
|
plan[:descriptor].walk(root),
|
|
27
|
-
node: root
|
|
26
|
+
node: root,
|
|
27
|
+
declaration_plan_mode:
|
|
28
|
+
options.fetch(
|
|
29
|
+
:import_declaration_plan, :eager
|
|
30
|
+
))
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
end
|
data/lib/lutaml/xml/leptris.rb
CHANGED
|
@@ -8,95 +8,36 @@ module Lutaml
|
|
|
8
8
|
# reference leptris types directly (the moxml adapters and the
|
|
9
9
|
# interpretive path never enter here).
|
|
10
10
|
module Leptris
|
|
11
|
-
#
|
|
12
|
-
# (
|
|
13
|
-
#
|
|
14
|
-
# 1.9.
|
|
11
|
+
# Single capability floor (leptris 1.9.351.1): the child-handles
|
|
12
|
+
# snapshot (1.9.273), exact-URI attribute rows (#1486, 1.9.289),
|
|
13
|
+
# nested-attribute capture (#1563, 1.9.313), wildcard rows
|
|
14
|
+
# (#1552, 1.9.313), child-row ns_uri retention and qualified
|
|
15
|
+
# attribute leniency (#1585/#1586, 1.9.320), the :unqualified ns
|
|
16
|
+
# form (#1560, 1.9.323), build_result with the address-based
|
|
17
|
+
# text attach (#408, 1.9.334/1.9.336), and the plan-value
|
|
18
|
+
# attributes batch face (PlanValue#attributes_snapshot,
|
|
19
|
+
# leptris-ruby#447). Bundles resolving an older gem keep the
|
|
15
20
|
# interpretive path — PlanCompiler.compile answers nil — the
|
|
16
|
-
#
|
|
17
|
-
MIN_LEPTRIS_VERSION = "1.9.
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
# The :unqualified child-row ns form (leptris#1560: matches only
|
|
46
|
-
# the UNWRITTEN spelling — bare or inherited-default-ns — and
|
|
47
|
-
# refuses prefixed spellings, the interpretive matcher's exact
|
|
48
|
-
# no-namespace semantic) ships in binding 1.9.323.0. Older
|
|
49
|
-
# engines keep namespace-less model rows on the ns_lenient
|
|
50
|
-
# superset (local-name matching under any qualification).
|
|
51
|
-
MIN_LEPTRIS_NS_UNQUALIFIED_VERSION = "1.9.323.0"
|
|
21
|
+
# opt-out protocol.
|
|
22
|
+
MIN_LEPTRIS_VERSION = "1.9.366.2"
|
|
52
23
|
|
|
24
|
+
# Memoized: the compiler consults this on every cache hit (per
|
|
25
|
+
# child row per node), and the comparison minted a Gem::Version
|
|
26
|
+
# per call — visible in the niso ordered-lane allocation profile.
|
|
27
|
+
# The loaded gem cannot change mid-process.
|
|
53
28
|
def self.plan_path_compatible?
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
spec = Gem.loaded_specs["leptris"]
|
|
64
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_ATTR_NS_VERSION)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.nested_attr_capture_compatible?
|
|
68
|
-
return false unless defined?(Gem)
|
|
69
|
-
|
|
70
|
-
spec = Gem.loaded_specs["leptris"]
|
|
71
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_NESTED_ATTR_VERSION)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def self.wildcard_rows_compatible?
|
|
75
|
-
return false unless defined?(Gem)
|
|
76
|
-
|
|
77
|
-
spec = Gem.loaded_specs["leptris"]
|
|
78
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WILDCARD_VERSION)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Memoized: consulted per parse on the walk gate hot path, and
|
|
82
|
-
# the loaded leptris spec cannot change within a process.
|
|
83
|
-
@walk_ns_safety = nil
|
|
84
|
-
def self.walk_ns_safety_compatible?
|
|
85
|
-
return @walk_ns_safety unless @walk_ns_safety.nil?
|
|
86
|
-
return (@walk_ns_safety = false) unless defined?(Gem)
|
|
87
|
-
|
|
88
|
-
spec = Gem.loaded_specs["leptris"]
|
|
89
|
-
@walk_ns_safety =
|
|
90
|
-
!!(spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_WALK_NS_SAFETY_VERSION))
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def self.ns_unqualified_rows_compatible?
|
|
94
|
-
return false unless defined?(Gem)
|
|
95
|
-
|
|
96
|
-
spec = Gem.loaded_specs["leptris"]
|
|
97
|
-
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_NS_UNQUALIFIED_VERSION)
|
|
29
|
+
return @plan_path_compatible if defined?(@plan_path_compatible)
|
|
30
|
+
|
|
31
|
+
@plan_path_compatible =
|
|
32
|
+
if defined?(Gem)
|
|
33
|
+
spec = Gem.loaded_specs["leptris"]
|
|
34
|
+
spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
|
|
35
|
+
else
|
|
36
|
+
false
|
|
37
|
+
end
|
|
98
38
|
end
|
|
99
39
|
|
|
40
|
+
autoload :NativeEmission, "lutaml/xml/leptris/native_emission"
|
|
100
41
|
autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
|
|
101
42
|
autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
|
|
102
43
|
autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
|
|
@@ -479,6 +479,11 @@ module Lutaml
|
|
|
479
479
|
# violation entirely — check it while there is still someone to tell.
|
|
480
480
|
if !instance_is_serialize && attr && !attr.collection? &&
|
|
481
481
|
!rule.content_mapping?
|
|
482
|
+
# singular_tolerance: :first collapses an over-ingested
|
|
483
|
+
# collection to its leading value before the writer sees it
|
|
484
|
+
# (#971) — the reduced scalar then flows through
|
|
485
|
+
# deserialize and serializes as a single element.
|
|
486
|
+
value = value.first if attr.tolerates_singular_overflow?(value)
|
|
482
487
|
attr.valid_collection!(value, context)
|
|
483
488
|
end
|
|
484
489
|
rule.deserialize(instance, value, deserialize_attributes, context,
|