lutaml-model 0.8.54 → 0.8.55
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/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/plan_compiler.rb +61 -8
- data/lib/lutaml/xml/plan_hydrator.rb +28 -6
- data/lib/lutaml/xml/plan_serializer.rb +15 -4
- data/spec/lutaml/xml/plan_partition_spec.rb +67 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c16452c0371017c525543d5fcd948d4eaf9680dcff5ec2b193d2e9fabe43f86a
|
|
4
|
+
data.tar.gz: 1853acc8790ddc2e1b53cfc8c1a68b750f695fc01f474d608e5d29cfbe269a57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ac74cd346e28f14990ae94ef0921bce2f75bc92252e82a6d7a16059683b25a5e2645e4227a3a72479880921c9a831d69faa951cb6eb20600f29a41ba010e9c8
|
|
7
|
+
data.tar.gz: 9f30ab7f585db43c6d0e94425c9e4fe3e9895a02c28c7a7f3f5888ef5dc8a6ea4dee287cfb58c2300edcd23e0d6f92a9a792f602a0696f0a76c35334362add79
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -95,6 +95,21 @@ module Lutaml
|
|
|
95
95
|
return nil if attr.nil?
|
|
96
96
|
return nil if attr.derived?
|
|
97
97
|
|
|
98
|
+
# Partition rows (TODO 34 step 2) ride native predicates
|
|
99
|
+
# only in the plain-capture shape; any other when_attribute
|
|
100
|
+
# shape stays interpretive (the suite pins nested-target
|
|
101
|
+
# partitions to the interpretive path).
|
|
102
|
+
unless rule.when_attribute.empty?
|
|
103
|
+
t = attr.type(register)
|
|
104
|
+
plain_partition = !rule.delegate &&
|
|
105
|
+
!rule.has_custom_method_for_deserialization? &&
|
|
106
|
+
!rule.polymorphic_mapping? &&
|
|
107
|
+
!attr.polymorphic? && !attr.union? &&
|
|
108
|
+
!(t.is_a?(Class) &&
|
|
109
|
+
t.include?(::Lutaml::Model::Serialize))
|
|
110
|
+
return nil unless plain_partition
|
|
111
|
+
end
|
|
112
|
+
|
|
98
113
|
# Interpretive hydration materializes every mapped
|
|
99
114
|
# collection, present or not; the fast path mirrors with
|
|
100
115
|
# constructor-time empty arrays.
|
|
@@ -178,6 +193,10 @@ module Lutaml
|
|
|
178
193
|
|
|
179
194
|
row = { name: rule.name.to_s }
|
|
180
195
|
row[:ns] = child_ns(rule, model_ns) if rule.namespace_set?
|
|
196
|
+
# lutaml-model#88: same-name rows partitioned by the
|
|
197
|
+
# rule's discriminator ride the engine's exclusive
|
|
198
|
+
# predicate match (leptris 1.9.221+, #1272).
|
|
199
|
+
row[:when] = rule.when_attribute unless rule.when_attribute.empty?
|
|
181
200
|
|
|
182
201
|
if attr.collection?
|
|
183
202
|
compiled << [rule, attr, :collection_native, nil,
|
|
@@ -192,6 +211,8 @@ module Lutaml
|
|
|
192
211
|
end
|
|
193
212
|
end
|
|
194
213
|
|
|
214
|
+
row_tags = partition_row_tags!(compiled, rows, tag)
|
|
215
|
+
|
|
195
216
|
flags = []
|
|
196
217
|
flags << :cdata if cdata
|
|
197
218
|
flags << :mixed_content if mixed_content
|
|
@@ -212,6 +233,7 @@ module Lutaml
|
|
|
212
233
|
|
|
213
234
|
{ descriptor: descriptor, tree: tree, rows: compiled,
|
|
214
235
|
attr_rows: attr_rows, mapping: mapping,
|
|
236
|
+
row_tags: row_tags,
|
|
215
237
|
ordered: mapping.ordered? || mapping.mixed_content?,
|
|
216
238
|
needs_nodes: needs_nodes,
|
|
217
239
|
collection_defaults: collection_defaults }
|
|
@@ -219,14 +241,45 @@ module Lutaml
|
|
|
219
241
|
|
|
220
242
|
def compilable_mapping?(mapping)
|
|
221
243
|
mapping.root_element &&
|
|
222
|
-
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Partition bookkeeping for when_attribute rows (#88, TODO
|
|
248
|
+
# 34 step 2): the engine matches same-name rows exclusively —
|
|
249
|
+
# first matching row wins — so predicate rows must PRECEDE any
|
|
250
|
+
# plain sibling on the same name (a plain-first order
|
|
251
|
+
# double-captures: the plain row takes everything and the
|
|
252
|
+
# predicates still claim their matches). Every row of a
|
|
253
|
+
# partitioned name gets a distinct type_tag; plan values echo
|
|
254
|
+
# it back, giving the hydrator row-exact routing with no
|
|
255
|
+
# per-occurrence re-derivation. Returns {compiled_index =>
|
|
256
|
+
# tag} (nil when the model has no partitions). compiled and
|
|
257
|
+
# rows are parallel and stay in lockstep through the reorder.
|
|
258
|
+
def partition_row_tags!(compiled, rows, tag)
|
|
259
|
+
by_name = rows.each_with_index
|
|
260
|
+
.group_by { |(row, _)| row[:name] }
|
|
261
|
+
.select { |_name, pairs| pairs.any? { |(row, _)| row[:when] } }
|
|
262
|
+
return nil if by_name.empty?
|
|
263
|
+
|
|
264
|
+
permutation = by_name.each_value.flat_map do |pairs|
|
|
265
|
+
pairs.sort_by { |(row, i)| [row[:when] ? 0 : 1, i] }
|
|
266
|
+
.map(&:last)
|
|
267
|
+
end
|
|
268
|
+
remaining = (0...rows.length).to_a - permutation
|
|
269
|
+
permutation.concat(remaining)
|
|
270
|
+
|
|
271
|
+
row_tags = {}
|
|
272
|
+
permutation.each_with_index do |old_index, new_index|
|
|
273
|
+
row = rows[old_index]
|
|
274
|
+
next unless by_name.key?(row[:name])
|
|
275
|
+
|
|
276
|
+
tag += 1
|
|
277
|
+
row[:type_tag] = tag
|
|
278
|
+
row_tags[new_index] = tag
|
|
279
|
+
end
|
|
280
|
+
rows.replace(permutation.map { |i| rows[i] })
|
|
281
|
+
compiled.replace(permutation.map { |i| compiled[i] })
|
|
282
|
+
row_tags
|
|
230
283
|
end
|
|
231
284
|
|
|
232
285
|
# Attribute for a rule — delegate rules resolve against their
|
|
@@ -73,19 +73,32 @@ module Lutaml
|
|
|
73
73
|
# parent/root links once the parent exists; delegate values
|
|
74
74
|
# wait for the instance (their target object must exist).
|
|
75
75
|
def children_kwargs(_model_class, plan, value, buckets = nil)
|
|
76
|
-
grouped = group_children(value)
|
|
76
|
+
grouped, tagged = group_children(value, plan[:row_tags])
|
|
77
77
|
buckets = nil unless buckets && plan[:needs_nodes]
|
|
78
78
|
kwargs = {}
|
|
79
79
|
children = []
|
|
80
80
|
delegates = []
|
|
81
81
|
spellings = Hash.new { |h, k| h[k] = [] }
|
|
82
|
-
plan[:
|
|
82
|
+
row_tags = plan[:row_tags]
|
|
83
|
+
plan[:rows].each_with_index do |(rule, attr, kind, spelling, delegate), idx|
|
|
84
|
+
tag = row_tags&.[](idx)
|
|
83
85
|
case kind
|
|
84
86
|
when :scalar
|
|
85
87
|
# Raw passthrough: the model constructor is the single
|
|
86
88
|
# cast authority — pre-casting here doubled every cast.
|
|
87
89
|
# Class transforms still apply before assignment.
|
|
88
|
-
|
|
90
|
+
if tag && (vals = tagged[tag]) && vals.size > 1
|
|
91
|
+
# Interpretive parity: several captures into a
|
|
92
|
+
# non-collection attribute arrive as the full array.
|
|
93
|
+
v = vals.map(&:string_value)
|
|
94
|
+
else
|
|
95
|
+
first = if tag
|
|
96
|
+
tagged[tag]&.first
|
|
97
|
+
else
|
|
98
|
+
grouped.dig(rule.name.to_s, 0)
|
|
99
|
+
end
|
|
100
|
+
v = first&.string_value
|
|
101
|
+
end
|
|
89
102
|
unless v.nil?
|
|
90
103
|
v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
|
|
91
104
|
assign(kwargs, delegates, delegate, rule, attr, v)
|
|
@@ -122,7 +135,11 @@ module Lutaml
|
|
|
122
135
|
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
123
136
|
end
|
|
124
137
|
when :collection_native
|
|
125
|
-
values =
|
|
138
|
+
values = if tag
|
|
139
|
+
tagged[tag].to_a.map(&:string_value)
|
|
140
|
+
else
|
|
141
|
+
native_collection(value, rule.name.to_s)
|
|
142
|
+
end
|
|
126
143
|
unless values.nil?
|
|
127
144
|
assign(kwargs, delegates, delegate, rule, attr, values)
|
|
128
145
|
end
|
|
@@ -314,15 +331,20 @@ module Lutaml
|
|
|
314
331
|
Lutaml::Xml::Adapter::LeptrisAdapter.parse(raw).root
|
|
315
332
|
end
|
|
316
333
|
|
|
317
|
-
def group_children(value)
|
|
334
|
+
def group_children(value, row_tags = nil)
|
|
318
335
|
grouped = {}
|
|
336
|
+
tagged = {}
|
|
337
|
+
want_tags = !row_tags.nil?
|
|
319
338
|
value.count.times do |i|
|
|
320
339
|
child = value.at(i)
|
|
321
340
|
next if child.name.nil? # content runs, read separately
|
|
322
341
|
|
|
323
342
|
(grouped[child.name] ||= []) << child
|
|
343
|
+
if want_tags && child.type_tag != 0
|
|
344
|
+
(tagged[child.type_tag] ||= []) << child
|
|
345
|
+
end
|
|
324
346
|
end
|
|
325
|
-
grouped
|
|
347
|
+
[grouped, tagged]
|
|
326
348
|
end
|
|
327
349
|
|
|
328
350
|
# Element children bucketed by local name, document order
|
|
@@ -62,12 +62,19 @@ module Lutaml
|
|
|
62
62
|
|
|
63
63
|
case kind
|
|
64
64
|
when :scalar
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
# Multi-capture parity: an attribute that collected
|
|
66
|
+
# several occurrences serializes one element per item,
|
|
67
|
+
# as the interpretive writer does.
|
|
68
|
+
Array(value).each do |item|
|
|
69
|
+
add_leaf(element, row_name(rule, spelling),
|
|
70
|
+
attr.serialize(item, :xml, register), doc,
|
|
71
|
+
attrs: rule.when_attribute)
|
|
72
|
+
end
|
|
67
73
|
when :collection_native, :collection_cb
|
|
68
74
|
Array(value).each do |item|
|
|
69
75
|
add_leaf(element, rule.name.to_s,
|
|
70
|
-
attr.serialize(item, :xml, register), doc
|
|
76
|
+
attr.serialize(item, :xml, register), doc,
|
|
77
|
+
attrs: rule.when_attribute)
|
|
71
78
|
end
|
|
72
79
|
when :nested, :ordered_deferred
|
|
73
80
|
Array(value).each do |item|
|
|
@@ -217,8 +224,12 @@ module Lutaml
|
|
|
217
224
|
attr.serialize(value, :xml, register).to_s)
|
|
218
225
|
end
|
|
219
226
|
|
|
220
|
-
|
|
227
|
+
# Partition rows (#88) re-emit their discriminator: the wire
|
|
228
|
+
# element carries the when_attribute pairs so the round trip
|
|
229
|
+
# re-routes to the same attribute on reparse.
|
|
230
|
+
def add_leaf(element, name, value, doc, attrs: nil)
|
|
221
231
|
child = element.create_child(name)
|
|
232
|
+
attrs&.each { |k, v| child[k.to_s] = v.to_s }
|
|
222
233
|
child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
|
|
223
234
|
child
|
|
224
235
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/xml"
|
|
5
|
+
|
|
6
|
+
# The plan fast path rides when_attribute partitions (TODO 34 step 2,
|
|
7
|
+
# leptris 1.9.221+ #1272 predicate rows): same-name rows claim their
|
|
8
|
+
# occurrences exclusively, the plain sibling takes the unclaimed, and
|
|
9
|
+
# serialization re-stamps the discriminators. All lanes must agree —
|
|
10
|
+
# plan and interpretive — on parse AND round trip, including the
|
|
11
|
+
# multi-capture edge (several unclaimed occurrences into a scalar
|
|
12
|
+
# attribute arrive as the full array, interpretive parity).
|
|
13
|
+
RSpec.describe "Plan fast path with when_attribute partitions" do
|
|
14
|
+
before do
|
|
15
|
+
require "leptris/xml"
|
|
16
|
+
rescue LoadError
|
|
17
|
+
skip "leptris not available"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:model) do
|
|
21
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
22
|
+
attribute :a_val, :string
|
|
23
|
+
attribute :b_val, :string
|
|
24
|
+
attribute :other, :string
|
|
25
|
+
|
|
26
|
+
xml do
|
|
27
|
+
root "part"
|
|
28
|
+
map_element "item", to: :a_val, when_attribute: { "type" => "a" }
|
|
29
|
+
map_element "item", to: :b_val, when_attribute: { "type" => "b" }
|
|
30
|
+
map_element "item", to: :other
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
let(:doc) do
|
|
36
|
+
%(<part><item type="a">A</item><item type="b">B</item><item>plain</item><item type="x">X</item></part>)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "compiles the partitioned model with row tags" do
|
|
40
|
+
plan = Lutaml::Xml::PlanCompiler.compile(
|
|
41
|
+
model, Lutaml::Model::Config.default_register
|
|
42
|
+
)
|
|
43
|
+
expect(plan).not_to be_nil
|
|
44
|
+
expect(plan[:row_tags].length).to eq(3)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "routes occurrences exclusively and matches the interpretive parse" do
|
|
48
|
+
fast = model.from_xml(doc)
|
|
49
|
+
slow = Lutaml::Model::Config.instance.tap { |c| c.xml_plan_fast_path = false }
|
|
50
|
+
.then { model.from_xml(doc) }
|
|
51
|
+
Lutaml::Model::Config.instance.xml_plan_fast_path = true
|
|
52
|
+
|
|
53
|
+
expect(fast.a_val).to eq("A")
|
|
54
|
+
expect(fast.b_val).to eq("B")
|
|
55
|
+
expect(fast.other).to eq(["plain", "X"])
|
|
56
|
+
expect(fast.a_val).to eq(slow.a_val)
|
|
57
|
+
expect(fast.b_val).to eq(slow.b_val)
|
|
58
|
+
expect(fast.other).to eq(slow.other)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "round-trips through the serializer with discriminator stamps" do
|
|
62
|
+
round = model.from_xml(model.from_xml(doc).to_xml)
|
|
63
|
+
expect(round.a_val).to eq("A")
|
|
64
|
+
expect(round.b_val).to eq("B")
|
|
65
|
+
expect(round.other).to eq(["plain", "X"])
|
|
66
|
+
end
|
|
67
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.55
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -1970,6 +1970,7 @@ files:
|
|
|
1970
1970
|
- spec/lutaml/xml/pipeline_integration_spec.rb
|
|
1971
1971
|
- spec/lutaml/xml/plain_model_accessors_spec.rb
|
|
1972
1972
|
- spec/lutaml/xml/plan_fast_path_spec.rb
|
|
1973
|
+
- spec/lutaml/xml/plan_partition_spec.rb
|
|
1973
1974
|
- spec/lutaml/xml/plan_walk_spec.rb
|
|
1974
1975
|
- spec/lutaml/xml/prefix_control_spec.rb
|
|
1975
1976
|
- spec/lutaml/xml/register_spec.rb
|