lutaml-model 0.8.137 → 0.8.139
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/attribute.rb +8 -1
- data/lib/lutaml/model/serialize/initialization.rb +15 -3
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/leptris/native_emission.rb +145 -37
- data/lib/lutaml/xml/leptris/plan_compiler.rb +2 -0
- data/lib/lutaml/xml/mapping.rb +38 -1
- data/lib/lutaml/xml/model_transform.rb +39 -0
- data/lib/lutaml/xml/transformation.rb +24 -0
- data/spec/lutaml/model/map_all_attributes_spec.rb +89 -0
- data/spec/lutaml/model/walk_constructor_spec.rb +48 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +61 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a027b25146b1db5759062241ef72d4e6cfb7122f4dff800d5683535bf4767c5b
|
|
4
|
+
data.tar.gz: 2b49a35df1f800d47d18b2e79737fe58fccc79385a2f38e5e5c9169acae84063
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e66ba7f1fff3188bbe477aa108681b53cfbacaec36f7dfd652a54c6305f083a28fc4992e6586312fc22da9bded0864f5336f848538b755a315f8c4fbdde3d427
|
|
7
|
+
data.tar.gz: 8f1039a43171dea42d3bf5f3db295ae4bc44e15d087fc531a9910515d307634410d3a6b102adfff2b5ee30764ff0e19dbefdeb9e1c84618332981bc95f4c67ab
|
|
@@ -379,6 +379,13 @@ module Lutaml
|
|
|
379
379
|
# Performance: Frozen empty hash to reduce allocations
|
|
380
380
|
EMPTY_TRANSFORM_HASH = {}.freeze
|
|
381
381
|
|
|
382
|
+
# Default-argument options for the serialize lane. The per-call
|
|
383
|
+
# `options = {}` literal allocated one Hash per serialize call
|
|
384
|
+
# (two per mapped XML row); every consumer of the argument only
|
|
385
|
+
# reads it or derives a new hash (merge), so a shared frozen
|
|
386
|
+
# empty is safe.
|
|
387
|
+
EMPTY_OPTIONS = {}.freeze
|
|
388
|
+
|
|
382
389
|
def transform
|
|
383
390
|
# Options are fixed after definition (value_policy memoizes the
|
|
384
391
|
# same way) — the per-call @options[:transform] Hash#[] was hit
|
|
@@ -918,7 +925,7 @@ instance_object = nil)
|
|
|
918
925
|
true
|
|
919
926
|
end
|
|
920
927
|
|
|
921
|
-
def serialize(value, format, register, options =
|
|
928
|
+
def serialize(value, format, register, options = EMPTY_OPTIONS)
|
|
922
929
|
value ||= build_collection if collection? && initialize_empty?
|
|
923
930
|
return value if value.nil? || Utils.uninitialized?(value)
|
|
924
931
|
|
|
@@ -376,7 +376,11 @@ module Lutaml
|
|
|
376
376
|
# through the real constructor for them, exactly as the walk
|
|
377
377
|
# did before the compiled seed existed. The seed stays the
|
|
378
378
|
# fast path for classes using the standard initializer.
|
|
379
|
-
return new if
|
|
379
|
+
return new(lutaml_register: register) if constructs_through_initialize?
|
|
380
|
+
# did before the compiled seed existed, register included. The
|
|
381
|
+
# seed stays the fast path for classes using the standard
|
|
382
|
+
# initializer.
|
|
383
|
+
return new(lutaml_register: register) if constructs_through_initialize?
|
|
380
384
|
|
|
381
385
|
instance = allocate
|
|
382
386
|
instance.init_walk_state(register_id)
|
|
@@ -388,8 +392,8 @@ module Lutaml
|
|
|
388
392
|
# optional setup, and the compiled walk seed would skip it.
|
|
389
393
|
# (The standard initializer's owner is Serialize::Builder; a
|
|
390
394
|
# class- or parent-defined initialize makes the owner the class.)
|
|
391
|
-
def
|
|
392
|
-
@
|
|
395
|
+
def constructs_through_initialize?
|
|
396
|
+
@constructs_through_initialize ||=
|
|
393
397
|
instance_method(:initialize).owner != Lutaml::Model::Serialize::Builder
|
|
394
398
|
end
|
|
395
399
|
|
|
@@ -407,6 +411,14 @@ module Lutaml
|
|
|
407
411
|
# stack limit — models without pending deferred imports skip
|
|
408
412
|
# it entirely.
|
|
409
413
|
ensure_imports!(register_id) if finalized? && deferred_imports_pending?(register_id)
|
|
414
|
+
# Custom-initialize classes (#1032's twin): their initialize is
|
|
415
|
+
# constructor contract — internal state the compiled seeding
|
|
416
|
+
# cannot know about — so they construct through the real
|
|
417
|
+
# constructor, then carry the parse's register context.
|
|
418
|
+
if constructs_through_initialize?
|
|
419
|
+
instance = new(lutaml_register: register_id)
|
|
420
|
+
return instance
|
|
421
|
+
end
|
|
410
422
|
instance = allocate
|
|
411
423
|
instance.finalize_deserialization(register_id)
|
|
412
424
|
instance
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -31,6 +31,17 @@ module Lutaml
|
|
|
31
31
|
ROOT_ROW = 0xFFFF_FFFF
|
|
32
32
|
EMITTABLE_KINDS = %i[scalar collection nested].freeze
|
|
33
33
|
|
|
34
|
+
# Fiber-local buffer of reusable op Hash slots. The engine's
|
|
35
|
+
# build_result copies every op into FFI memory before it
|
|
36
|
+
# returns, so the Ruby hashes are dead weight past the call;
|
|
37
|
+
# recycling them positionally removes one Hash allocation per
|
|
38
|
+
# emitted op (four per mapped row on the emission lane) at no
|
|
39
|
+
# semantic cost. Slots are cleared before every reuse, so
|
|
40
|
+
# stale keys from a prior kind cannot leak into an op.
|
|
41
|
+
OP_BUFFER_KEY = :lutaml_model_leptris_native_emission_op_buffer
|
|
42
|
+
OP_BUFFER_BASE_KEY =
|
|
43
|
+
:lutaml_model_leptris_native_emission_op_buffer_base
|
|
44
|
+
|
|
34
45
|
class << self
|
|
35
46
|
def compatible?(plan)
|
|
36
47
|
!plan[:ordered] && tree_emittable?(plan[:tree])
|
|
@@ -49,28 +60,59 @@ module Lutaml
|
|
|
49
60
|
end
|
|
50
61
|
end
|
|
51
62
|
|
|
63
|
+
# len is the logical length of the buffer: slots [base, len) hold
|
|
64
|
+
# the ops of the emission in progress. base is nonzero when a
|
|
65
|
+
# user hook re-enters to_xml mid-emission — the inner
|
|
66
|
+
# emission appends after the outer's written slots instead of
|
|
67
|
+
# overwriting them. The tail is trimmed after the engine
|
|
68
|
+
# consumed the result so a pathological document cannot grow
|
|
69
|
+
# the buffer without bound.
|
|
52
70
|
def emit(instance, plan, register)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
71
|
+
buf = (Thread.current[OP_BUFFER_KEY] ||= [])
|
|
72
|
+
base = (Thread.current[OP_BUFFER_BASE_KEY] ||= 0)
|
|
73
|
+
len = base
|
|
74
|
+
begin
|
|
75
|
+
op_slot(buf, len)
|
|
76
|
+
buf[len][:kind] = :element
|
|
77
|
+
buf[len][:row_index] = ROOT_ROW
|
|
78
|
+
len = emit_element(buf, len + 1, instance, plan, register, 0)
|
|
79
|
+
op_slot(buf, len)
|
|
80
|
+
buf[len][:kind] = :end
|
|
81
|
+
len += 1
|
|
82
|
+
compact = plan[:descriptor].serialize(
|
|
83
|
+
plan[:descriptor].build_result(buf[base, len - base]),
|
|
84
|
+
)
|
|
85
|
+
::Leptris::XML.parse(compact).to_xml(indent: 2, no_decl: true)
|
|
86
|
+
ensure
|
|
87
|
+
buf.slice!(len..) if buf.size > len
|
|
88
|
+
Thread.current[OP_BUFFER_BASE_KEY] = base
|
|
89
|
+
end
|
|
60
90
|
end
|
|
61
91
|
|
|
62
92
|
private
|
|
63
93
|
|
|
94
|
+
# Cleared slot len of the buffer, reusing the Hash object a
|
|
95
|
+
# previous emission left there when present. Also publishes
|
|
96
|
+
# this emission's exclusive end as the fiber-local base: the
|
|
97
|
+
# walk evaluates user code (model getters, default procs,
|
|
98
|
+
# registered serializers) between slots, and a re-entrant
|
|
99
|
+
# to_xml from that code must append after every written op,
|
|
100
|
+
# never overlap this emission's slots.
|
|
101
|
+
def op_slot(buf, len)
|
|
102
|
+
Thread.current[OP_BUFFER_BASE_KEY] = len + 1
|
|
103
|
+
(buf[len] ||= {}).clear
|
|
104
|
+
end
|
|
105
|
+
|
|
64
106
|
# plan_index: this plan's flattened index. The engine's
|
|
65
107
|
# flatten_plan pushes each nested row's child plan exactly
|
|
66
108
|
# once — collection members share that slot — so child flat
|
|
67
109
|
# indices come from a static per-plan layout (relative to
|
|
68
110
|
# this plan's index), never from a runtime counter.
|
|
69
|
-
def emit_element(
|
|
111
|
+
def emit_element(buf, len, instance, plan, register, plan_index)
|
|
70
112
|
my_index = plan_index
|
|
71
113
|
layout = flat_layout_for(plan)
|
|
72
114
|
|
|
73
|
-
emit_attributes(
|
|
115
|
+
len = emit_attributes(buf, len, instance, plan, my_index, register)
|
|
74
116
|
|
|
75
117
|
plan[:rows].each_with_index do |(rule, attr, kind, _spelling, delegate), row_index|
|
|
76
118
|
value = value_of(instance, attr, delegate)
|
|
@@ -83,31 +125,41 @@ module Lutaml
|
|
|
83
125
|
# as the DOM lane does.
|
|
84
126
|
if value.is_a?(::Array)
|
|
85
127
|
value.each do |item|
|
|
86
|
-
|
|
128
|
+
len = scalar_op(buf, len, my_index, row_index, attr, item,
|
|
129
|
+
register)
|
|
87
130
|
end
|
|
88
131
|
else
|
|
89
|
-
|
|
132
|
+
len = scalar_op(buf, len, my_index, row_index, attr, value,
|
|
133
|
+
register)
|
|
90
134
|
end
|
|
91
135
|
when :collection_native, :collection_cb
|
|
92
136
|
Array(value).each do |item|
|
|
93
|
-
|
|
137
|
+
len = scalar_op(buf, len, my_index, row_index, attr, item,
|
|
138
|
+
register)
|
|
94
139
|
end
|
|
95
140
|
when :nested
|
|
96
141
|
_tree, child_rel = layout[row_index]
|
|
97
142
|
next unless child_rel
|
|
98
143
|
|
|
144
|
+
child_plans = child_plans_for(plan, row_index)
|
|
99
145
|
Array(value).each do |item|
|
|
100
|
-
child_plan =
|
|
101
|
-
(
|
|
146
|
+
child_plan = child_plans[item.class] ||
|
|
147
|
+
(child_plans[item.class] =
|
|
102
148
|
PlanCompiler.compile(item.class, register))
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
149
|
+
op_slot(buf, len)
|
|
150
|
+
buf[len][:kind] = :element
|
|
151
|
+
buf[len][:plan_index] = my_index
|
|
152
|
+
buf[len][:row_index] = row_index
|
|
153
|
+
len += 1
|
|
154
|
+
len = emit_element(buf, len, item, child_plan,
|
|
155
|
+
register, my_index + child_rel)
|
|
156
|
+
op_slot(buf, len)
|
|
157
|
+
buf[len][:kind] = :end
|
|
158
|
+
len += 1
|
|
108
159
|
end
|
|
109
160
|
end
|
|
110
161
|
end
|
|
162
|
+
len
|
|
111
163
|
end
|
|
112
164
|
|
|
113
165
|
# {nested row_index => [child tree, relative flat index]} —
|
|
@@ -133,19 +185,50 @@ module Lutaml
|
|
|
133
185
|
end
|
|
134
186
|
end
|
|
135
187
|
|
|
136
|
-
|
|
188
|
+
# The name bookkeeping is static per plan — attr_rows and the
|
|
189
|
+
# tree's attribute rows never vary per instance — so the
|
|
190
|
+
# name->tree row_index map is memoized on the plan. Only
|
|
191
|
+
# instances carrying a recorded attribute order (parsed
|
|
192
|
+
# documents whose XML attribute sequence must be replayed)
|
|
193
|
+
# take the per-instance path.
|
|
194
|
+
def emit_attributes(buf, len, instance, plan, plan_index, register)
|
|
137
195
|
recorded = instance.attribute_order
|
|
138
|
-
attr_rows = plan[:attr_rows]
|
|
139
196
|
if recorded && !recorded.empty?
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
197
|
+
emit_attributes_recorded(buf, len, instance, plan, plan_index,
|
|
198
|
+
register, recorded)
|
|
199
|
+
return len
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
tree_attrs = plan[:tree][:attributes] || []
|
|
203
|
+
row_indices = attr_row_indices_for(plan, tree_attrs)
|
|
204
|
+
attr_rows = plan[:attr_rows]
|
|
205
|
+
attr_rows.each do |rule, attr, _sp, delegate|
|
|
206
|
+
value = value_of(instance, attr, delegate)
|
|
207
|
+
next if value.nil?
|
|
208
|
+
|
|
209
|
+
name = rule.name.to_s
|
|
210
|
+
row_index = row_indices[name]
|
|
211
|
+
next unless row_index
|
|
212
|
+
|
|
213
|
+
op_slot(buf, len)
|
|
214
|
+
buf[len][:kind] = :attr
|
|
215
|
+
buf[len][:plan_index] = plan_index
|
|
216
|
+
buf[len][:row_index] = row_index
|
|
217
|
+
buf[len][:name] = name
|
|
218
|
+
buf[len][:value] = attr.serialize(value, :xml, register).to_s
|
|
219
|
+
len += 1
|
|
147
220
|
end
|
|
221
|
+
len
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def emit_attributes_recorded(buf, len, instance, plan, plan_index,
|
|
225
|
+
register, recorded)
|
|
226
|
+
attr_rows = plan[:attr_rows]
|
|
227
|
+
by_name = {}
|
|
228
|
+
attr_rows.each { |rule, attr, _sp, delegate| by_name[rule.name.to_s] = [rule, attr, delegate] }
|
|
229
|
+
names = recorded | attr_rows.map { |rule, _a, _s, _d| rule.name.to_s }
|
|
148
230
|
tree_attrs = plan[:tree][:attributes] || []
|
|
231
|
+
row_indices = attr_row_indices_for(plan, tree_attrs)
|
|
149
232
|
names.each do |name|
|
|
150
233
|
entry = by_name[name]
|
|
151
234
|
next unless entry
|
|
@@ -154,18 +237,40 @@ module Lutaml
|
|
|
154
237
|
value = value_of(instance, attr, delegate)
|
|
155
238
|
next if value.nil?
|
|
156
239
|
|
|
157
|
-
row_index =
|
|
240
|
+
row_index = row_indices[name]
|
|
158
241
|
next unless row_index
|
|
159
242
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
243
|
+
op_slot(buf, len)
|
|
244
|
+
buf[len][:kind] = :attr
|
|
245
|
+
buf[len][:plan_index] = plan_index
|
|
246
|
+
buf[len][:row_index] = row_index
|
|
247
|
+
buf[len][:name] = name
|
|
248
|
+
buf[len][:value] = attr.serialize(value, :xml, register).to_s
|
|
249
|
+
len += 1
|
|
250
|
+
end
|
|
251
|
+
len
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# name -> tree_attrs index, first occurrence wins (same match
|
|
255
|
+
# semantics as the Array#index scan it replaced).
|
|
256
|
+
def attr_row_indices_for(plan, tree_attrs)
|
|
257
|
+
return plan[:attr_row_indices] if plan[:attr_row_indices]
|
|
258
|
+
|
|
259
|
+
indices = {}
|
|
260
|
+
tree_attrs.each_with_index do |tree_attr, i|
|
|
261
|
+
name = tree_attr[:name]
|
|
262
|
+
indices[name] = i unless indices.key?(name)
|
|
163
263
|
end
|
|
264
|
+
plan[:attr_row_indices] = indices
|
|
164
265
|
end
|
|
165
266
|
|
|
166
|
-
def scalar_op(plan_index, row_index, attr, value, register)
|
|
167
|
-
|
|
168
|
-
|
|
267
|
+
def scalar_op(buf, len, plan_index, row_index, attr, value, register)
|
|
268
|
+
op_slot(buf, len)
|
|
269
|
+
buf[len][:kind] = :scalar
|
|
270
|
+
buf[len][:plan_index] = plan_index
|
|
271
|
+
buf[len][:row_index] = row_index
|
|
272
|
+
buf[len][:value] = attr.serialize(value, :xml, register).to_s
|
|
273
|
+
len + 1
|
|
169
274
|
end
|
|
170
275
|
|
|
171
276
|
def value_of(instance, attr, delegate)
|
|
@@ -177,8 +282,11 @@ module Lutaml
|
|
|
177
282
|
end
|
|
178
283
|
end
|
|
179
284
|
|
|
180
|
-
|
|
181
|
-
|
|
285
|
+
# Compiled child plans per nested row, keyed by row index and
|
|
286
|
+
# then by member class. Keying through Integers and classes
|
|
287
|
+
# avoids allocating a [class, name] pair per emitted child.
|
|
288
|
+
def child_plans_for(plan, row_index)
|
|
289
|
+
(plan[:nested_plans_by_row] ||= {})[row_index] ||= {}
|
|
182
290
|
end
|
|
183
291
|
end
|
|
184
292
|
end
|
|
@@ -122,6 +122,8 @@ module Lutaml
|
|
|
122
122
|
# them (compiling here silently dropped pi_settings).
|
|
123
123
|
return opt_out!(:processing_instruction) unless
|
|
124
124
|
mapping.processing_instruction_mappings.empty?
|
|
125
|
+
# Wildcard attribute families ride the interpretive path too
|
|
126
|
+
return opt_out!(:all_attributes) if mapping.all_attributes_rule
|
|
125
127
|
|
|
126
128
|
rows = []
|
|
127
129
|
attr_rows = [] # [[rule, attr, row_name]] hydration metadata
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Lutaml
|
|
|
38
38
|
@xml_instance
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
attr_reader :root_element, :namespace_uri, :namespace_prefix, :element_sequence, :mappings_imported, :namespace_class, :namespace_param, :element_name, :documentation_text, :type_name_value, :namespace_scope, :mapper_class, :xml_space, :consolidation_maps, :processing_instruction_mappings, :any_element_rule
|
|
41
|
+
attr_reader :root_element, :namespace_uri, :namespace_prefix, :element_sequence, :mappings_imported, :namespace_class, :namespace_param, :element_name, :documentation_text, :type_name_value, :namespace_scope, :mapper_class, :xml_space, :consolidation_maps, :processing_instruction_mappings, :any_element_rule, :all_attributes_rule
|
|
42
42
|
|
|
43
43
|
def initialize
|
|
44
44
|
super
|
|
@@ -49,6 +49,7 @@ module Lutaml
|
|
|
49
49
|
@element_sequence = []
|
|
50
50
|
@content_mapping = nil
|
|
51
51
|
@raw_mapping = nil
|
|
52
|
+
@all_attributes_rule = nil
|
|
52
53
|
@mixed_content = false
|
|
53
54
|
@xml_space = nil
|
|
54
55
|
@format = :xml
|
|
@@ -875,6 +876,26 @@ module Lutaml
|
|
|
875
876
|
)
|
|
876
877
|
end
|
|
877
878
|
|
|
879
|
+
# Capture every attribute not claimed by an explicit map_attribute
|
|
880
|
+
# (and matching an optional pattern) into a Hash attribute, and emit
|
|
881
|
+
# it back as attributes after the mapped ones on serialization —
|
|
882
|
+
# the remainder semantics of map_all, for attribute families
|
|
883
|
+
# (lutaml-model#1031): HTML data-*, ARIA aria-*, on* handlers.
|
|
884
|
+
#
|
|
885
|
+
# @param to [Symbol] the Hash attribute name
|
|
886
|
+
# @param matching [Regexp, nil] optional name filter; nil captures
|
|
887
|
+
# every unclaimed attribute
|
|
888
|
+
#
|
|
889
|
+
# @example
|
|
890
|
+
# xml do
|
|
891
|
+
# element "mn"
|
|
892
|
+
# map_attribute "name", to: :name
|
|
893
|
+
# map_all_attributes to: :other_attributes, matching: /\A(data-|aria-|on)/
|
|
894
|
+
# end
|
|
895
|
+
def map_all_attributes(to:, matching: nil)
|
|
896
|
+
@all_attributes_rule = AllAttributesMapping.new(to, matching)
|
|
897
|
+
end
|
|
898
|
+
|
|
878
899
|
def sequence(&block)
|
|
879
900
|
@element_sequence << ::Lutaml::Model::Sequence.new(self,
|
|
880
901
|
format: :xml).tap do |s|
|
|
@@ -1681,5 +1702,21 @@ module Lutaml
|
|
|
1681
1702
|
self.class.new(@target.dup, @to)
|
|
1682
1703
|
end
|
|
1683
1704
|
end
|
|
1705
|
+
|
|
1706
|
+
# Wildcard attribute-family mapping (#1031): the target Hash collects
|
|
1707
|
+
# every attribute no explicit map_attribute claims and that matches
|
|
1708
|
+
# the optional pattern; serialization emits each pair as an attribute.
|
|
1709
|
+
class AllAttributesMapping
|
|
1710
|
+
attr_reader :to, :matching
|
|
1711
|
+
|
|
1712
|
+
def initialize(to, matching)
|
|
1713
|
+
@to = to
|
|
1714
|
+
@matching = matching
|
|
1715
|
+
end
|
|
1716
|
+
|
|
1717
|
+
def dup
|
|
1718
|
+
self.class.new(@to, @matching)
|
|
1719
|
+
end
|
|
1720
|
+
end
|
|
1684
1721
|
end
|
|
1685
1722
|
end
|
|
@@ -503,11 +503,50 @@ module Lutaml
|
|
|
503
503
|
apply_processing_instruction_deserialization(doc, instance, xml_mapping,
|
|
504
504
|
effective_register)
|
|
505
505
|
|
|
506
|
+
apply_all_attributes_deserialization(doc, instance, xml_mapping)
|
|
507
|
+
|
|
506
508
|
run_consolidation(instance, effective_register, instance_is_serialize)
|
|
507
509
|
|
|
508
510
|
instance
|
|
509
511
|
end
|
|
510
512
|
|
|
513
|
+
# #1031: wildcard attribute families. After the explicit attribute
|
|
514
|
+
# rules have consumed their names, every remaining unprefixed
|
|
515
|
+
# attribute matching the rule's pattern lands in the target Hash —
|
|
516
|
+
# names claimed by explicit rules never leak into it.
|
|
517
|
+
def apply_all_attributes_deserialization(doc, instance, xml_mapping)
|
|
518
|
+
rule = xml_mapping.all_attributes_rule
|
|
519
|
+
return if rule.nil?
|
|
520
|
+
|
|
521
|
+
element = doc.is_a?(::Lutaml::Xml::XmlElement) ? doc : doc.root
|
|
522
|
+
return if element.nil?
|
|
523
|
+
|
|
524
|
+
claimed = attribute_names_claimed_by_rules(xml_mapping)
|
|
525
|
+
remainder = {}
|
|
526
|
+
element.attributes.each_value do |attr|
|
|
527
|
+
name = attr.unprefixed_name.to_s
|
|
528
|
+
next if claimed.include?(name)
|
|
529
|
+
next if rule.matching && !rule.matching.match?(name)
|
|
530
|
+
|
|
531
|
+
remainder[name] = attr.value
|
|
532
|
+
end
|
|
533
|
+
return if remainder.empty?
|
|
534
|
+
|
|
535
|
+
instance.public_send(:"#{rule.to}=", remainder)
|
|
536
|
+
instance.value_set_for(rule.to) if instance.is_a?(::Lutaml::Model::Serialize)
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Attribute names claimed by explicit map_attribute rules, plus the
|
|
540
|
+
# engine-handled schemaLocation (which renders through
|
|
541
|
+
# raw_schema_location, not the remainder).
|
|
542
|
+
def attribute_names_claimed_by_rules(xml_mapping)
|
|
543
|
+
names = %w[schemaLocation]
|
|
544
|
+
xml_mapping.mapping_attributes_hash.each_value do |rule_or_array|
|
|
545
|
+
Array(rule_or_array).each { |rule| names << rule.name.to_s }
|
|
546
|
+
end
|
|
547
|
+
names
|
|
548
|
+
end
|
|
549
|
+
|
|
511
550
|
def validate_document!(doc, options)
|
|
512
551
|
return unless doc.is_a?(Array)
|
|
513
552
|
|
|
@@ -101,6 +101,9 @@ module Lutaml
|
|
|
101
101
|
apply_standard_rules(root, model_instance, options)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# The wildcard attribute family emits after the mapped attributes
|
|
105
|
+
apply_all_attributes_mappings(root, model_instance, mapping)
|
|
106
|
+
|
|
104
107
|
root
|
|
105
108
|
end
|
|
106
109
|
|
|
@@ -242,6 +245,27 @@ module Lutaml
|
|
|
242
245
|
end
|
|
243
246
|
|
|
244
247
|
# Check if element order should be used for serialization
|
|
248
|
+
|
|
249
|
+
# #1031: emit the wildcard attribute family after the mapped
|
|
250
|
+
# attributes. Attributes render in insertion order; the remainder
|
|
251
|
+
# follows the explicitly mapped names.
|
|
252
|
+
def apply_all_attributes_mappings(root, model_instance, mapping)
|
|
253
|
+
rule = mapping.all_attributes_rule
|
|
254
|
+
return if rule.nil?
|
|
255
|
+
|
|
256
|
+
value = model_instance.public_send(rule.to)
|
|
257
|
+
return if value.nil? || !value.is_a?(::Hash)
|
|
258
|
+
|
|
259
|
+
value.each do |name, attr_value|
|
|
260
|
+
next if attr_value.nil?
|
|
261
|
+
|
|
262
|
+
root.add_attribute(
|
|
263
|
+
::Lutaml::Xml::DataModel::XmlAttribute.new(name.to_s,
|
|
264
|
+
attr_value.to_s, nil),
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
245
269
|
#
|
|
246
270
|
# @param model_instance [Object] The model instance
|
|
247
271
|
# @param mapping [Xml::Mapping] The mapping
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #1031: wildcard attribute families — every attribute no explicit
|
|
6
|
+
# map_attribute claims and that matches the pattern lands in a Hash and
|
|
7
|
+
# is emitted back after the mapped attributes on serialization.
|
|
8
|
+
RSpec.describe "map_all_attributes" do
|
|
9
|
+
let(:model_class) do
|
|
10
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
11
|
+
attribute :name, :string
|
|
12
|
+
attribute :data_attributes, :hash
|
|
13
|
+
|
|
14
|
+
xml do
|
|
15
|
+
element "mn"
|
|
16
|
+
map_attribute "name", to: :name
|
|
17
|
+
map_all_attributes to: :data_attributes, matching: /\A(data-|aria-|on)/
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:stubbed) do
|
|
23
|
+
stub_const("MapAllAttributesModel", model_class)
|
|
24
|
+
MapAllAttributesModel
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "captures unclaimed, pattern-matching attributes into the Hash" do
|
|
28
|
+
xml = '<mn name="x" data-title="T" onclick="f()" aria-label="L" unre="nope"/>'
|
|
29
|
+
model = stubbed.from_xml(xml)
|
|
30
|
+
|
|
31
|
+
expect(model.name).to eq("x")
|
|
32
|
+
expect(model.data_attributes).to eq(
|
|
33
|
+
"data-title" => "T", "onclick" => "f()", "aria-label" => "L",
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "never lets mapped names leak into the remainder" do
|
|
38
|
+
xml = '<mn name="x" data-name="D"/>'
|
|
39
|
+
model = stubbed.from_xml(xml)
|
|
40
|
+
|
|
41
|
+
expect(model.data_attributes).to eq("data-name" => "D")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "emits the remainder after the mapped attributes on serialization" do
|
|
45
|
+
model = stubbed.from_xml(
|
|
46
|
+
'<mn name="x" data-title="T" onclick="f()"/>',
|
|
47
|
+
)
|
|
48
|
+
out = model.to_xml
|
|
49
|
+
|
|
50
|
+
expect(out).to include('name="x"')
|
|
51
|
+
expect(out).to include('data-title="T"')
|
|
52
|
+
expect(out).to include('onclick="f()"')
|
|
53
|
+
expect(out.index("name=")).to be < out.index("data-title=")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "round-trips losslessly" do
|
|
57
|
+
xml = '<mn name="x" data-title="T" onclick="f()" aria-label="L"/>'
|
|
58
|
+
model = stubbed.from_xml(xml)
|
|
59
|
+
round_tripped = stubbed.from_xml(model.to_xml)
|
|
60
|
+
|
|
61
|
+
expect(round_tripped.data_attributes).to eq(model.data_attributes)
|
|
62
|
+
expect(round_tripped.name).to eq(model.name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "emits nothing when the remainder is empty" do
|
|
66
|
+
model = stubbed.from_xml('<mn name="x"/>')
|
|
67
|
+
out = model.to_xml
|
|
68
|
+
|
|
69
|
+
expect(out).not_to include("data-")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "captures every unclaimed attribute when no pattern is given" do
|
|
73
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
74
|
+
attribute :name, :string
|
|
75
|
+
attribute :other_attributes, :hash
|
|
76
|
+
|
|
77
|
+
xml do
|
|
78
|
+
element "el"
|
|
79
|
+
map_attribute "name", to: :name
|
|
80
|
+
map_all_attributes to: :other_attributes
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
stub_const("MapAllNoPatternModel", klass)
|
|
84
|
+
|
|
85
|
+
model = klass.from_xml('<el name="x" zeta="1"/>')
|
|
86
|
+
|
|
87
|
+
expect(model.other_attributes).to eq("zeta" => "1")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# A class that defines its own initialize is built through it by the
|
|
6
|
+
# from_* walk, and the walk hands it the register exactly as
|
|
7
|
+
# `new(lutaml_register:)` did before the compiled seed existed: a
|
|
8
|
+
# constructor that tells a blank construction from a caller's by that
|
|
9
|
+
# keyword sees the same call either way.
|
|
10
|
+
RSpec.describe "Walk construction of a custom-initialize class" do
|
|
11
|
+
let(:model) do
|
|
12
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
13
|
+
attribute :name, :string
|
|
14
|
+
|
|
15
|
+
json do
|
|
16
|
+
map "name", to: :name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
attr_accessor :constructions
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(*args, **kwargs)
|
|
24
|
+
self.class.constructions << [args, kwargs.keys]
|
|
25
|
+
@internal = :ready
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_reader :internal
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
before do
|
|
34
|
+
stub_const("WalkConstructorModel", model)
|
|
35
|
+
model.constructions = []
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "runs the class's own initialize" do
|
|
39
|
+
expect(model.from_json('{"name":"a"}').internal).to eq(:ready)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "passes the register, as the walk did through new(lutaml_register:)" do
|
|
43
|
+
parsed = model.from_json('{"name":"a"}')
|
|
44
|
+
|
|
45
|
+
expect(model.constructions).to eq([[[], [:lutaml_register]]])
|
|
46
|
+
expect(parsed.name).to eq("a")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -819,4 +819,65 @@ RSpec.describe "XML plan fast path" do
|
|
|
819
819
|
)
|
|
820
820
|
end
|
|
821
821
|
end
|
|
822
|
+
|
|
823
|
+
describe "native emission op buffer" do
|
|
824
|
+
around do |example|
|
|
825
|
+
Lutaml::Model::Config.with_adapter(xml: :leptris) { example.run }
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
it "keeps a re-entrant to_xml from a getter from corrupting the outer emission" do
|
|
829
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
830
|
+
attribute :v, :string
|
|
831
|
+
|
|
832
|
+
xml do
|
|
833
|
+
element "inner"
|
|
834
|
+
map_attribute "v", to: :v
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
outer = Class.new(Lutaml::Model::Serializable) do
|
|
838
|
+
attribute :v, :string
|
|
839
|
+
attribute :items, inner, collection: true
|
|
840
|
+
|
|
841
|
+
xml do
|
|
842
|
+
element "outer"
|
|
843
|
+
map_attribute "v", to: :v
|
|
844
|
+
map_element "inner", to: :items
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
define_method(:items) do
|
|
848
|
+
inner.new(v: "reentrant").to_xml
|
|
849
|
+
@items
|
|
850
|
+
end
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
model = outer.new(v: "x", items: [inner.new(v: "1"), inner.new(v: "2")])
|
|
854
|
+
expected = "<outer v=\"x\">\n <inner v=\"1\"/>\n <inner v=\"2\"/>\n</outer>"
|
|
855
|
+
expect(model.to_xml).to eq(expected)
|
|
856
|
+
expect(model.to_xml).to eq(expected)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
it "keeps interleaved emissions of distinct models stable" do
|
|
860
|
+
alpha = Class.new(Lutaml::Model::Serializable) do
|
|
861
|
+
attribute :a, :string
|
|
862
|
+
|
|
863
|
+
xml do
|
|
864
|
+
element "alpha"
|
|
865
|
+
map_attribute "a", to: :a
|
|
866
|
+
end
|
|
867
|
+
end
|
|
868
|
+
beta = Class.new(Lutaml::Model::Serializable) do
|
|
869
|
+
attribute :b, :integer
|
|
870
|
+
|
|
871
|
+
xml do
|
|
872
|
+
element "beta"
|
|
873
|
+
map_attribute "b", to: :b
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
|
|
877
|
+
5.times do
|
|
878
|
+
expect(alpha.new(a: "x").to_xml).to eq("<alpha a=\"x\"/>")
|
|
879
|
+
expect(beta.new(b: 7).to_xml).to eq("<beta b=\"7\"/>")
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
end
|
|
822
883
|
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.139
|
|
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-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -1796,6 +1796,7 @@ files:
|
|
|
1796
1796
|
- spec/lutaml/model/liquid/indexed_access_spec.rb
|
|
1797
1797
|
- spec/lutaml/model/liquid_compatibility_spec.rb
|
|
1798
1798
|
- spec/lutaml/model/logger_spec.rb
|
|
1799
|
+
- spec/lutaml/model/map_all_attributes_spec.rb
|
|
1799
1800
|
- spec/lutaml/model/map_all_spec.rb
|
|
1800
1801
|
- spec/lutaml/model/map_content_spec.rb
|
|
1801
1802
|
- spec/lutaml/model/map_unmapped_spec.rb
|
|
@@ -1912,6 +1913,7 @@ files:
|
|
|
1912
1913
|
- spec/lutaml/model/value_map_spec.rb
|
|
1913
1914
|
- spec/lutaml/model/value_policy_spec.rb
|
|
1914
1915
|
- spec/lutaml/model/value_transformation_comprehensive_spec.rb
|
|
1916
|
+
- spec/lutaml/model/walk_constructor_spec.rb
|
|
1915
1917
|
- spec/lutaml/model/with_child_mapping_spec.rb
|
|
1916
1918
|
- spec/lutaml/model/writer_assigned_attribute_spec.rb
|
|
1917
1919
|
- spec/lutaml/model/xml_decoupling_spec.rb
|