lutaml-model 0.8.41 → 0.8.42
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 +71 -32
- data/lib/lutaml/model/collection_handler.rb +3 -1
- data/lib/lutaml/model/global_context.rb +25 -1
- data/lib/lutaml/model/mapping/mapping_rule.rb +63 -19
- data/lib/lutaml/model/register.rb +7 -1
- data/lib/lutaml/model/serialize/initialization.rb +11 -2
- data/lib/lutaml/model/type/string.rb +1 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/model_transform.rb +34 -9
- data/spec/lutaml/model/parse_dispatch_hoist_spec.rb +173 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1cd747883eee7ea6a87aea0c708e6054f1f6cf9f7b06e295b32f1b7ba06a8a75
|
|
4
|
+
data.tar.gz: 639b21ad4dd77104b3bce40f0ba7bad925a5b608806e6e8979b50cb68f57b70e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05174a52193320261f496e92ac656b6ddc5a47167db5b98b3682c0397a4c9fb0f2a58f20e8be689d7f35486bea63dc2228d367574c3834587be6d5ba97306cd9
|
|
7
|
+
data.tar.gz: a6efef58f862390afe6fcf903ce3ff4d829d451079b7a1762990ed0e0bf1c5623c798c712a485d8e81cdc415189a37e9c5f7b1c02dd2d03ad6c17d96b5bac5a0
|
|
@@ -172,30 +172,56 @@ module Lutaml
|
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
context
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
175
|
+
# Register/Symbol path: the resolution is a static fact of
|
|
176
|
+
# (attribute, register id), but the old code walked
|
|
177
|
+
# GlobalContext.context — a singleton hop, a delegation, a to_sym
|
|
178
|
+
# normalization and a registry lookup — on every call (2.4 M calls,
|
|
179
|
+
# 3.9 M GlobalContext#context hits per ISO-13849 parse). Cache keyed
|
|
180
|
+
# on the register id itself; a context replaced under the same id
|
|
181
|
+
# bumps GlobalContext.context_generation and flushes the cache.
|
|
182
|
+
register_id = case context_or_register
|
|
183
|
+
when Symbol
|
|
184
|
+
context_or_register
|
|
185
|
+
when Lutaml::Model::Register
|
|
186
|
+
context_or_register.id
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if register_id
|
|
190
|
+
cache = (@type_cache_by_register ||= {})
|
|
191
|
+
generation = GlobalContext.context_generation
|
|
192
|
+
if @type_cache_generation != generation
|
|
193
|
+
cache.clear
|
|
194
|
+
@type_cache_generation = generation
|
|
195
|
+
end
|
|
196
|
+
cached = cache[register_id]
|
|
197
|
+
return cached if cached
|
|
198
|
+
|
|
199
|
+
context = GlobalContext.context(register_id)
|
|
200
|
+
# An unregistered id falls back to the (thread-local) default
|
|
201
|
+
# context — do not pin that answer in the shared cache.
|
|
202
|
+
return resolver.resolve(unresolved_type, context || GlobalContext.default_context) unless context
|
|
203
|
+
|
|
204
|
+
resolved = resolver.resolve(unresolved_type, context)
|
|
205
|
+
cache[register_id] = resolved
|
|
206
|
+
resolved
|
|
207
|
+
elsif context_or_register.is_a?(Lutaml::Model::TypeContext)
|
|
208
|
+
# Performance: Cache per TypeContext identity.
|
|
209
|
+
# When type substitution replaces a TypeContext, the object_id changes,
|
|
210
|
+
# so old cache entries are never hit again (safe invalidation).
|
|
211
|
+
cache_key = context_or_register.object_id
|
|
212
|
+
@type_cache ||= {}
|
|
213
|
+
cached = @type_cache[cache_key]
|
|
214
|
+
return cached if cached
|
|
215
|
+
|
|
216
|
+
resolved = resolver.resolve(unresolved_type, context_or_register)
|
|
217
|
+
@type_cache[cache_key] = resolved
|
|
218
|
+
resolved
|
|
219
|
+
else
|
|
220
|
+
@cached_type_default ||= begin
|
|
221
|
+
context = normalize_context(context_or_register)
|
|
222
|
+
resolver.resolve(unresolved_type, context)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
199
225
|
end
|
|
200
226
|
|
|
201
227
|
# @api public
|
|
@@ -305,7 +331,10 @@ module Lutaml
|
|
|
305
331
|
EMPTY_TRANSFORM_HASH = {}.freeze
|
|
306
332
|
|
|
307
333
|
def transform
|
|
308
|
-
|
|
334
|
+
# Options are fixed after definition (value_policy memoizes the
|
|
335
|
+
# same way) — the per-call @options[:transform] Hash#[] was hit
|
|
336
|
+
# 2.9 M times on the ISO-13849 parse profile.
|
|
337
|
+
@transform ||= @options[:transform] || EMPTY_TRANSFORM_HASH
|
|
309
338
|
end
|
|
310
339
|
|
|
311
340
|
def method_name
|
|
@@ -438,7 +467,7 @@ module Lutaml
|
|
|
438
467
|
def cast_element(value, register)
|
|
439
468
|
# Resolve first: an undeclared type has to raise UnknownTypeError
|
|
440
469
|
# even for a nil value (the 0.8.33 behavior).
|
|
441
|
-
type(register)
|
|
470
|
+
resolved_type = type(register)
|
|
442
471
|
|
|
443
472
|
return value if Utils.uninitialized?(value)
|
|
444
473
|
|
|
@@ -448,7 +477,7 @@ module Lutaml
|
|
|
448
477
|
# manufacture a typed INSTANCE from nothing: that is the
|
|
449
478
|
# phantom #793 removed, suppressed by the post-check below.
|
|
450
479
|
if value.nil?
|
|
451
|
-
result = cast_element_present(value, register)
|
|
480
|
+
result = cast_element_present(value, register, resolved_type)
|
|
452
481
|
return result if result.nil?
|
|
453
482
|
return nil if result.is_a?(Lutaml::Model::Type::Value) ||
|
|
454
483
|
result.is_a?(Lutaml::Model::Serialize)
|
|
@@ -456,11 +485,11 @@ module Lutaml
|
|
|
456
485
|
return result
|
|
457
486
|
end
|
|
458
487
|
|
|
459
|
-
cast_element_present(value, register)
|
|
488
|
+
cast_element_present(value, register, resolved_type)
|
|
460
489
|
end
|
|
461
490
|
|
|
462
|
-
def cast_element_present(value, register)
|
|
463
|
-
resolved_type
|
|
491
|
+
def cast_element_present(value, register, resolved_type = nil)
|
|
492
|
+
resolved_type ||= type(register)
|
|
464
493
|
|
|
465
494
|
return cast_union(value, nil, register) if union?
|
|
466
495
|
return resolved_type.new(value) if value.is_a?(::Hash) && !hash_type?
|
|
@@ -474,8 +503,10 @@ module Lutaml
|
|
|
474
503
|
# The castability verdict is a class-hierarchy fact, immutable
|
|
475
504
|
# per resolved type — check once, not per value (TODO.max-perf/15).
|
|
476
505
|
checked = (@castable_types ||= {}.compare_by_identity)
|
|
477
|
-
|
|
478
|
-
|
|
506
|
+
unless checked[resolved_type]
|
|
507
|
+
validate_attr_type!(resolved_type)
|
|
508
|
+
checked[resolved_type] = true
|
|
509
|
+
end
|
|
479
510
|
|
|
480
511
|
resolved_type.cast(value)
|
|
481
512
|
end
|
|
@@ -518,6 +549,7 @@ instance_object = nil)
|
|
|
518
549
|
def immutable_value?(value)
|
|
519
550
|
value.nil? || value.is_a?(Numeric) || value.is_a?(String) ||
|
|
520
551
|
value.is_a?(Symbol) || value == true || value == false ||
|
|
552
|
+
value.equal?(::Lutaml::Model::UninitializedClass.instance) ||
|
|
521
553
|
value.frozen?
|
|
522
554
|
end
|
|
523
555
|
|
|
@@ -925,6 +957,11 @@ instance_object = nil)
|
|
|
925
957
|
Lutaml::Model::Type::Union.validate_members!(@options[:union_member_types])
|
|
926
958
|
Lutaml::Model::Type::Union.validate_combo!(@options)
|
|
927
959
|
end
|
|
960
|
+
# `restrict` merges new options into the SAME attribute object
|
|
961
|
+
# post-construction — the option-derived memos must reset here or
|
|
962
|
+
# collection?/transform keep answering from the pre-restrict state.
|
|
963
|
+
remove_instance_variable(:@collection_option) if defined?(@collection_option)
|
|
964
|
+
remove_instance_variable(:@transform) if defined?(@transform)
|
|
928
965
|
@raw = !!@options[:raw]
|
|
929
966
|
if @raw
|
|
930
967
|
warn "[DEPRECATED] attribute :#{name}, :string, raw: true is deprecated. " \
|
|
@@ -1031,6 +1068,8 @@ instance_object = nil)
|
|
|
1031
1068
|
@type_cache&.clear
|
|
1032
1069
|
@cached_type_default = nil
|
|
1033
1070
|
@default_type_context = nil
|
|
1071
|
+
@type_cache_by_register&.clear
|
|
1072
|
+
@type_cache_generation = nil
|
|
1034
1073
|
end
|
|
1035
1074
|
|
|
1036
1075
|
# Public validation contract: mappings check their targets at
|
|
@@ -14,7 +14,9 @@ module Lutaml
|
|
|
14
14
|
#
|
|
15
15
|
# @return [Object, nil] The collection option value
|
|
16
16
|
def collection
|
|
17
|
-
@options[:collection]
|
|
17
|
+
@collection_option = @options[:collection] unless defined?(@collection_option)
|
|
18
|
+
|
|
19
|
+
@collection_option
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
# Check if this attribute is a collection
|
|
@@ -64,6 +64,18 @@ module Lutaml
|
|
|
64
64
|
@default_context_id = :default
|
|
65
65
|
@namespace_register_map = {} # namespace_uri => register_id
|
|
66
66
|
@mutex = Mutex.new
|
|
67
|
+
@context_generation = 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Bumped whenever a named context is registered, replaced,
|
|
71
|
+
# unregistered, or the registry is reset. Callers that cache a
|
|
72
|
+
# resolution keyed on a context ID compare this generation to
|
|
73
|
+
# invalidate — a replaced context under the same ID must not keep
|
|
74
|
+
# answering from a stale cache.
|
|
75
|
+
#
|
|
76
|
+
# @return [Integer]
|
|
77
|
+
def context_generation
|
|
78
|
+
@context_generation
|
|
67
79
|
end
|
|
68
80
|
|
|
69
81
|
# Get the current default context.
|
|
@@ -123,6 +135,10 @@ module Lutaml
|
|
|
123
135
|
# @return [void]
|
|
124
136
|
def register_context(context)
|
|
125
137
|
@registry.register(context)
|
|
138
|
+
# A replacement under an existing id must not keep answering from
|
|
139
|
+
# the resolver's [context.id, name]-keyed cache.
|
|
140
|
+
@resolver.clear_cache(context.id)
|
|
141
|
+
@context_generation += 1
|
|
126
142
|
end
|
|
127
143
|
|
|
128
144
|
# Create and register a new context.
|
|
@@ -133,12 +149,14 @@ module Lutaml
|
|
|
133
149
|
# @param substitutions [Array<TypeSubstitution, Hash>] Type substitutions
|
|
134
150
|
# @return [TypeContext] The created context
|
|
135
151
|
def create_context(id:, registry: nil, fallback_to: [], substitutions: [])
|
|
136
|
-
@registry.create(
|
|
152
|
+
context = @registry.create(
|
|
137
153
|
id: id,
|
|
138
154
|
registry: registry,
|
|
139
155
|
fallback_to: fallback_to,
|
|
140
156
|
substitutions: substitutions,
|
|
141
157
|
)
|
|
158
|
+
@context_generation += 1
|
|
159
|
+
context
|
|
142
160
|
end
|
|
143
161
|
|
|
144
162
|
# Unregister a context and clear its caches.
|
|
@@ -148,6 +166,7 @@ module Lutaml
|
|
|
148
166
|
def unregister_context(id)
|
|
149
167
|
@resolver.clear_cache(id)
|
|
150
168
|
@registry.unregister(id)
|
|
169
|
+
@context_generation += 1
|
|
151
170
|
end
|
|
152
171
|
|
|
153
172
|
# Execute a block with a specific context as default.
|
|
@@ -185,6 +204,7 @@ module Lutaml
|
|
|
185
204
|
end
|
|
186
205
|
@namespace_register_map.clear
|
|
187
206
|
@default_context_id = :default
|
|
207
|
+
@context_generation += 1
|
|
188
208
|
end
|
|
189
209
|
end
|
|
190
210
|
|
|
@@ -363,6 +383,10 @@ context_id = nil)
|
|
|
363
383
|
instance.context(id)
|
|
364
384
|
end
|
|
365
385
|
|
|
386
|
+
def context_generation
|
|
387
|
+
instance.context_generation
|
|
388
|
+
end
|
|
389
|
+
|
|
366
390
|
def clear_xml_namespace_registry!
|
|
367
391
|
instance.clear_xml_namespace_registry!
|
|
368
392
|
end
|
|
@@ -122,6 +122,15 @@ module Lutaml
|
|
|
122
122
|
@value_map[:from].freeze
|
|
123
123
|
@value_map[:to].freeze
|
|
124
124
|
@value_map.freeze
|
|
125
|
+
|
|
126
|
+
# The :from treatment flags are read on every rule application of
|
|
127
|
+
# every parse; they are static once @value_map is final. The three
|
|
128
|
+
# Hash#[] chains per value used to cost 1.4 M value_map calls per
|
|
129
|
+
# ISO-13849 parse.
|
|
130
|
+
from_map = @value_map[:from]
|
|
131
|
+
@treat_nil_flag = from_map[:nil] != :omitted
|
|
132
|
+
@treat_empty_flag = from_map[:empty] != :omitted
|
|
133
|
+
@treat_omitted_flag = from_map[:omitted] != :omitted
|
|
125
134
|
end
|
|
126
135
|
|
|
127
136
|
def default_value_map(options = {})
|
|
@@ -243,14 +252,20 @@ module Lutaml
|
|
|
243
252
|
end
|
|
244
253
|
|
|
245
254
|
def treat_nil?(options = {})
|
|
255
|
+
return @treat_nil_flag unless options[:nil] || options[:empty] || options[:omitted]
|
|
256
|
+
|
|
246
257
|
value_map(:from, options)[:nil] != :omitted
|
|
247
258
|
end
|
|
248
259
|
|
|
249
260
|
def treat_empty?(options = {})
|
|
261
|
+
return @treat_empty_flag unless options[:nil] || options[:empty] || options[:omitted]
|
|
262
|
+
|
|
250
263
|
value_map(:from, options)[:empty] != :omitted
|
|
251
264
|
end
|
|
252
265
|
|
|
253
266
|
def treat_omitted?(options = {})
|
|
267
|
+
return @treat_omitted_flag unless options[:nil] || options[:empty] || options[:omitted]
|
|
268
|
+
|
|
254
269
|
value_map(:from, options)[:omitted] != :omitted
|
|
255
270
|
end
|
|
256
271
|
|
|
@@ -402,6 +417,47 @@ context = nil)
|
|
|
402
417
|
# Frozen empty array for the common case of no transformers
|
|
403
418
|
EMPTY_TRANSFORMERS = [].freeze
|
|
404
419
|
|
|
420
|
+
# Verdict for handle_transform_method's dispatch, computed once per
|
|
421
|
+
# (rule, attribute) — both are static after definition, but rules may
|
|
422
|
+
# be frozen, so the memo lives at class level keyed on identity.
|
|
423
|
+
# :assign — no transformer runs on deserialize (fast path);
|
|
424
|
+
# :import — hash/proc transformers go through ImportTransformer.
|
|
425
|
+
TRANSFORM_DISPATCH = {}.compare_by_identity
|
|
426
|
+
|
|
427
|
+
def self.transform_dispatch(rule, attr)
|
|
428
|
+
per_attr = TRANSFORM_DISPATCH[rule]
|
|
429
|
+
if per_attr.nil?
|
|
430
|
+
per_attr = {}.compare_by_identity
|
|
431
|
+
TRANSFORM_DISPATCH[rule] = per_attr
|
|
432
|
+
end
|
|
433
|
+
verdict = per_attr[attr]
|
|
434
|
+
return verdict if verdict
|
|
435
|
+
|
|
436
|
+
rule_transform = rule.transform
|
|
437
|
+
# NB: `::Hash` — inside Lutaml::Model the bare constant `Hash` is
|
|
438
|
+
# the hash-format adapter class, so a plain is_a?(Hash) is always
|
|
439
|
+
# false here (the original dispatch shared that shadow and never
|
|
440
|
+
# took the hash-transform branch).
|
|
441
|
+
rule_has = rule_transform.is_a?(Class) ||
|
|
442
|
+
(rule_transform.is_a?(::Hash) && !rule_transform.empty?)
|
|
443
|
+
attr_transform = attr&.transform
|
|
444
|
+
attr_has = attr_transform &&
|
|
445
|
+
(attr_transform.is_a?(Class) ||
|
|
446
|
+
(attr_transform.is_a?(::Hash) && !attr_transform.empty?))
|
|
447
|
+
|
|
448
|
+
verdict = if !rule_has && !attr_has
|
|
449
|
+
:assign
|
|
450
|
+
elsif rule.get_transformers(attr).any? do |t|
|
|
451
|
+
t.is_a?(Class) && t < Lutaml::Model::ValueTransformer
|
|
452
|
+
end
|
|
453
|
+
:assign
|
|
454
|
+
else
|
|
455
|
+
:import
|
|
456
|
+
end
|
|
457
|
+
per_attr[attr] = verdict
|
|
458
|
+
verdict
|
|
459
|
+
end
|
|
460
|
+
|
|
405
461
|
def get_transformers(attribute)
|
|
406
462
|
# Fast path: most rules have no transforms at all
|
|
407
463
|
rule_transform = transform
|
|
@@ -480,28 +536,16 @@ context = nil)
|
|
|
480
536
|
|
|
481
537
|
def handle_transform_method(model, value, attributes, context = nil)
|
|
482
538
|
attr = attributes[to]
|
|
483
|
-
#
|
|
484
|
-
#
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if
|
|
488
|
-
assign_value(model, value)
|
|
489
|
-
return true
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
# If we have class-based transformers, they were already applied in transform_value
|
|
493
|
-
# Only call ImportTransformer for hash/proc-based transformers
|
|
494
|
-
transformers = get_transformers(attr)
|
|
495
|
-
has_class_transformer = transformers.any? { |t| t.is_a?(Class) && t < Lutaml::Model::ValueTransformer }
|
|
496
|
-
|
|
497
|
-
if has_class_transformer
|
|
498
|
-
# Class transformers already applied, just assign the value
|
|
499
|
-
assign_value(model, value)
|
|
500
|
-
else
|
|
501
|
-
# Hash/proc transformers need ImportTransformer
|
|
539
|
+
# The transform verdict (none / class-based already applied /
|
|
540
|
+
# hash-proc via ImportTransformer) is static per (rule, attr) —
|
|
541
|
+
# it was recomputed with is_a? chains on every rule application
|
|
542
|
+
# (16.8 M Kernel#is_a? per ISO-13849 parse across the walk).
|
|
543
|
+
if self.class.transform_dispatch(self, attr) == :import
|
|
502
544
|
transformed = ImportTransformer.call(value, self, attr,
|
|
503
545
|
context: context)
|
|
504
546
|
assign_value(model, transformed)
|
|
547
|
+
else
|
|
548
|
+
assign_value(model, value)
|
|
505
549
|
end
|
|
506
550
|
true
|
|
507
551
|
end
|
|
@@ -87,12 +87,18 @@ module Lutaml
|
|
|
87
87
|
(RESOLVE_CACHE[child_class] ||= {})[parent_register] =
|
|
88
88
|
_resolve_for_child_uncached(child_class, parent_register)
|
|
89
89
|
else
|
|
90
|
+
# Read-first: Concurrent::Map#compute_if_absent takes the mutex
|
|
91
|
+
# on EVERY call, hits included — 1.35 M synchronized blocks per
|
|
92
|
+
# ISO-13849 parse for answers that never change. The plain #[]
|
|
93
|
+
# read is lock-free; nil is a valid cached answer, hence key?.
|
|
90
94
|
# Nested maps: class → register → result. Every hop keys on a
|
|
91
95
|
# stable object (Class / Symbol), so the hot lookup allocates
|
|
92
96
|
# nothing — the flat [object_id, register] array key cost one
|
|
93
97
|
# Array per call, six per parsed element.
|
|
94
98
|
per_class = RESOLVE_CACHE[child_class] # rubocop:todo Style/IdenticalConditionalBranches
|
|
95
|
-
per_class
|
|
99
|
+
return per_class[parent_register] if per_class&.key?(parent_register)
|
|
100
|
+
|
|
101
|
+
per_class = RESOLVE_CACHE.compute_if_absent(child_class) do
|
|
96
102
|
Concurrent::Map.new
|
|
97
103
|
end
|
|
98
104
|
per_class.compute_if_absent(parent_register) do
|
|
@@ -122,12 +122,21 @@ module Lutaml
|
|
|
122
122
|
# @param register [Symbol, nil] The register context
|
|
123
123
|
# @return [Hash] The attributes hash
|
|
124
124
|
def attributes(register = nil)
|
|
125
|
-
|
|
125
|
+
# Cache hit first: a filled entry can only exist after imports
|
|
126
|
+
# were ensured for that register (the fill path below runs
|
|
127
|
+
# ensure_imports! first), and mutations that invalidate the
|
|
128
|
+
# merge clear @merged_attributes_cache. The lookup was walked
|
|
129
|
+
# per rule application (751k per ISO-13849 parse).
|
|
126
130
|
if @register_records&.any?
|
|
127
131
|
register_id = extract_register_id(register)
|
|
128
|
-
(@merged_attributes_cache ||= {})[register_id]
|
|
132
|
+
cached = (@merged_attributes_cache ||= {})[register_id]
|
|
133
|
+
return cached if cached
|
|
134
|
+
|
|
135
|
+
ensure_imports!(register) if finalized?
|
|
136
|
+
@merged_attributes_cache[register_id] ||=
|
|
129
137
|
@attributes.merge(@register_records[register_id][:attributes])
|
|
130
138
|
else
|
|
139
|
+
ensure_imports!(register) if finalized?
|
|
131
140
|
@attributes
|
|
132
141
|
end
|
|
133
142
|
end
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
module Type
|
|
6
6
|
class String < Value
|
|
7
7
|
# Performance-optimized cast with short-circuit for already-correct types
|
|
8
|
-
def self.cast(value, options =
|
|
8
|
+
def self.cast(value, options = EMPTY_OPTIONS)
|
|
9
9
|
return nil if value.nil?
|
|
10
10
|
return value if Utils.uninitialized?(value)
|
|
11
11
|
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -315,6 +315,13 @@ module Lutaml
|
|
|
315
315
|
compiled_rule_records(instance.class, effective_register,
|
|
316
316
|
grouped_plain_rules)
|
|
317
317
|
end
|
|
318
|
+
# Loop invariants: the entity-decode flag is a per-(class, register)
|
|
319
|
+
# fact and the attributes hash is memoized per register — both were
|
|
320
|
+
# re-asked per rule application (942k + 751k calls per ISO-13849
|
|
321
|
+
# parse).
|
|
322
|
+
decode_entities_flag = decode_html_entities_for(instance,
|
|
323
|
+
effective_register)
|
|
324
|
+
deserialize_attributes = attributes
|
|
318
325
|
mappings.each do |rule|
|
|
319
326
|
record = records&.[](rule)
|
|
320
327
|
rule_to = rule.to
|
|
@@ -376,6 +383,9 @@ module Lutaml
|
|
|
376
383
|
default_namespace)
|
|
377
384
|
# Pre-match: no child element matches this rule.
|
|
378
385
|
# Skip expensive value_for_rule, handle defaults inline.
|
|
386
|
+
# NOTE: do not "read the instance" here (to_value_for) —
|
|
387
|
+
# the compiled getters materialize lazy collections on
|
|
388
|
+
# first read, which tripled parse allocations when tried.
|
|
379
389
|
if instance.using_default?(rule_to) || rule.render_default
|
|
380
390
|
defaults_used << rule_to
|
|
381
391
|
attr&.default(effective_register) || rule.to_value_for(instance)
|
|
@@ -409,9 +419,7 @@ module Lutaml
|
|
|
409
419
|
value = apply_value_map(value, from_map, attr)
|
|
410
420
|
value = normalize_xml_value(value, rule, attr, new_opts,
|
|
411
421
|
effective_register)
|
|
412
|
-
value = decode_html_entities_value(value,
|
|
413
|
-
instance, effective_register
|
|
414
|
-
))
|
|
422
|
+
value = decode_html_entities_value(value, decode_entities_flag)
|
|
415
423
|
value = rule.transform_value(attr, value, :from, :xml)
|
|
416
424
|
# An over-count is normally left for `.validate` to report. A mapped
|
|
417
425
|
# PORO has no `.validate`, so deferring there would discard the
|
|
@@ -420,7 +428,7 @@ module Lutaml
|
|
|
420
428
|
!rule.content_mapping?
|
|
421
429
|
attr.valid_collection!(value, context)
|
|
422
430
|
end
|
|
423
|
-
rule.deserialize(instance, value,
|
|
431
|
+
rule.deserialize(instance, value, deserialize_attributes, context,
|
|
424
432
|
options[:context])
|
|
425
433
|
|
|
426
434
|
instance.value_set_for(rule_to)
|
|
@@ -786,10 +794,27 @@ _effective_register)
|
|
|
786
794
|
end
|
|
787
795
|
|
|
788
796
|
def decode_html_entities_for(instance, register)
|
|
797
|
+
# Asked per rule application (942k per ISO-13849 parse) for a
|
|
798
|
+
# per-(class, register) fact; the transform instance itself is
|
|
799
|
+
# cached per (model class, register), so the memo lives here.
|
|
800
|
+
per_class = (@decode_html_entities_cache ||= {}.compare_by_identity)
|
|
801
|
+
per_register = (per_class[instance.class] ||= {})
|
|
802
|
+
return per_register[register] if per_register.key?(register)
|
|
803
|
+
|
|
789
804
|
klass = instance.class
|
|
790
|
-
|
|
805
|
+
per_register[register] = klass.is_a?(Class) &&
|
|
806
|
+
klass.include?(Lutaml::Model::Serialize) &&
|
|
807
|
+
(klass.mappings_for(:xml, register)&.decode_html_entities? || false)
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
# Serializable type class → its XML namespace class, asked per
|
|
811
|
+
# (rule × element) in value_for_rule / resolve_rule_names_with_type
|
|
812
|
+
# for a per-type static.
|
|
813
|
+
def type_namespace_class_of(type_class)
|
|
814
|
+
cache = (@type_ns_class_cache ||= {}.compare_by_identity)
|
|
815
|
+
return cache[type_class] if cache.key?(type_class)
|
|
791
816
|
|
|
792
|
-
|
|
817
|
+
cache[type_class] = type_class.mappings_for(:xml)&.namespace_class
|
|
793
818
|
end
|
|
794
819
|
|
|
795
820
|
def value_for_rule(session, rule, options, cached_attr = nil,
|
|
@@ -847,7 +872,7 @@ _effective_register)
|
|
|
847
872
|
type_ns_all_uris = nil
|
|
848
873
|
if attr_type_is_serializable
|
|
849
874
|
type_ns_class = if attr_type_is_class
|
|
850
|
-
attr_type
|
|
875
|
+
type_namespace_class_of(attr_type)
|
|
851
876
|
else
|
|
852
877
|
attr.type_namespace_class(effective_register)
|
|
853
878
|
end
|
|
@@ -1292,7 +1317,7 @@ effective_register = lutaml_register)
|
|
|
1292
1317
|
elsif precomputed_type.is_a?(Class) &&
|
|
1293
1318
|
precomputed_type.include?(::Lutaml::Model::Serialize)
|
|
1294
1319
|
# Use pre-computed type directly — avoids redundant attr.type() call
|
|
1295
|
-
attr_type_ns_class = precomputed_type
|
|
1320
|
+
attr_type_ns_class = type_namespace_class_of(precomputed_type)
|
|
1296
1321
|
if attr_type_ns_class
|
|
1297
1322
|
return ["#{attr_type_ns_class.uri}:#{rule.name}"]
|
|
1298
1323
|
end
|
|
@@ -1309,7 +1334,7 @@ effective_register = lutaml_register)
|
|
|
1309
1334
|
if attr_type_class
|
|
1310
1335
|
attr_type_ns_class = if attr_type_class.is_a?(Class) &&
|
|
1311
1336
|
attr_type_class.include?(::Lutaml::Model::Serialize)
|
|
1312
|
-
attr_type_class
|
|
1337
|
+
type_namespace_class_of(attr_type_class)
|
|
1313
1338
|
end
|
|
1314
1339
|
if attr_type_ns_class
|
|
1315
1340
|
return ["#{attr_type_ns_class.uri}:#{rule.name}"]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#811: the parse path re-resolved per (element × rule) what is
|
|
6
|
+
# static per (attribute/rule, register). These guards pin the hoisting
|
|
7
|
+
# contracts: register-keyed type caching with replacement invalidation,
|
|
8
|
+
# the lock-free child-register resolution, per-rule treatment flags, and
|
|
9
|
+
# the transform dispatch verdicts.
|
|
10
|
+
RSpec.describe "parse-path dispatch hoisting" do
|
|
11
|
+
describe "Attribute#type register caching" do
|
|
12
|
+
let(:klass) do
|
|
13
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
14
|
+
attribute :value, :string
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "returns the same resolved class for a repeated register" do
|
|
19
|
+
attr = klass.attributes[:value]
|
|
20
|
+
expect(attr.type(:default)).to equal(attr.type(:default))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "does not pin an unregistered register to the thread default" do
|
|
24
|
+
attr = klass.attributes[:value]
|
|
25
|
+
expect(attr.type(:nonexistent_register)).to eq(Lutaml::Model::Type::String)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "re-resolves when a context is replaced under the same id" do
|
|
29
|
+
attr = klass.attributes[:value]
|
|
30
|
+
|
|
31
|
+
build_context = lambda do |string_type|
|
|
32
|
+
Lutaml::Model::TypeContext.new(
|
|
33
|
+
id: :hoist_test,
|
|
34
|
+
registry: Lutaml::Model::TypeRegistry.new.tap do |r|
|
|
35
|
+
r.register(:string, string_type)
|
|
36
|
+
end,
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
Lutaml::Model::GlobalContext.register_context(
|
|
41
|
+
build_context.call(Lutaml::Model::Type::String),
|
|
42
|
+
)
|
|
43
|
+
expect(attr.type(:hoist_test)).to equal(Lutaml::Model::Type::String)
|
|
44
|
+
expect(attr.type(:hoist_test)).to equal(Lutaml::Model::Type::String)
|
|
45
|
+
|
|
46
|
+
Lutaml::Model::GlobalContext.register_context(
|
|
47
|
+
build_context.call(Lutaml::Model::Type::Integer),
|
|
48
|
+
)
|
|
49
|
+
expect(attr.type(:hoist_test)).to equal(Lutaml::Model::Type::Integer)
|
|
50
|
+
|
|
51
|
+
Lutaml::Model::GlobalContext.unregister_context(:hoist_test)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "Register.resolve_for_child" do
|
|
56
|
+
it "caches a nil answer without recomputing" do
|
|
57
|
+
klass = Class.new(Lutaml::Model::Serializable)
|
|
58
|
+
expect(Lutaml::Model::Register.resolve_for_child(klass, nil)).to be_nil
|
|
59
|
+
expect(Lutaml::Model::Register.resolve_for_child(klass, nil)).to be_nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "resolves the child's own default register over the parent's" do
|
|
63
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
64
|
+
def self.lutaml_default_register
|
|
65
|
+
:child_default
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
expect(Lutaml::Model::Register.resolve_for_child(klass, :other)).to eq(:child_default)
|
|
69
|
+
expect(Lutaml::Model::Register.resolve_for_child(klass, nil)).to eq(:child_default)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "MappingRule treatment flags" do
|
|
74
|
+
def rule_with(treat_nil: :nil, treat_empty: :empty, treat_omitted: :nil)
|
|
75
|
+
Lutaml::Model::MappingRule.new(
|
|
76
|
+
"name", to: :name,
|
|
77
|
+
treat_nil: treat_nil, treat_empty: treat_empty,
|
|
78
|
+
treat_omitted: treat_omitted
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "computes the same answers as the value_map walk" do
|
|
83
|
+
rule = rule_with(treat_omitted: :omitted, treat_nil: :omitted)
|
|
84
|
+
expect(rule.treat_omitted?).to be(false)
|
|
85
|
+
expect(rule.treat_nil?).to be(false)
|
|
86
|
+
expect(rule.treat_empty?).to be(true)
|
|
87
|
+
|
|
88
|
+
expect(rule.treat_omitted?({})).to eq(rule.treat_omitted?)
|
|
89
|
+
expect(rule.treat_nil?({})).to eq(rule.treat_nil?)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "honors per-call overrides over the precomputed flags" do
|
|
93
|
+
rule = rule_with
|
|
94
|
+
expect(rule.treat_omitted?).to be(true)
|
|
95
|
+
expect(rule.treat_omitted?({ omitted: :omitted })).to be(false)
|
|
96
|
+
expect(rule.treat_nil?({ nil: :omitted })).to be(false)
|
|
97
|
+
expect(rule.treat_empty?({ empty: :omitted })).to be(false)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
describe "transform dispatch" do
|
|
102
|
+
let(:base) do
|
|
103
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
104
|
+
attribute :plain, :string
|
|
105
|
+
attribute :mapped, :string
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "assigns directly for a rule without transformers" do
|
|
110
|
+
rule = Lutaml::Model::MappingRule.new("mapped", to: :mapped)
|
|
111
|
+
expect(Lutaml::Model::MappingRule.transform_dispatch(
|
|
112
|
+
rule, base.attributes[:mapped]
|
|
113
|
+
)).to be(:assign)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "routes hash transformers through ImportTransformer" do
|
|
117
|
+
rule = Lutaml::Model::MappingRule.new(
|
|
118
|
+
"mapped", to: :mapped,
|
|
119
|
+
transform: { import: ->(v) { v.to_s.upcase } }
|
|
120
|
+
)
|
|
121
|
+
expect(Lutaml::Model::MappingRule.transform_dispatch(
|
|
122
|
+
rule, base.attributes[:mapped]
|
|
123
|
+
)).to be(:import)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "applies an import transform through a real round trip" do
|
|
127
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
128
|
+
attribute :shout, :string
|
|
129
|
+
|
|
130
|
+
xml do
|
|
131
|
+
element "doc"
|
|
132
|
+
map_element "shout", to: :shout,
|
|
133
|
+
transform: { import: ->(v) { v.to_s.upcase } }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
expect(klass.from_xml("<doc><shout>soft</shout></doc>").shout).to eq("SOFT")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
describe "uninitialized defaults are cached" do
|
|
142
|
+
let(:klass) do
|
|
143
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
144
|
+
attribute :nothing, :string
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "returns the same uninitialized sentinel without re-casting" do
|
|
149
|
+
attr = klass.attributes[:nothing]
|
|
150
|
+
expect(attr.default(:default)).to equal(attr.default(:default))
|
|
151
|
+
expect(attr.default(:default)).to equal(
|
|
152
|
+
Lutaml::Model::UninitializedClass.instance,
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe "html-entities decode flag" do
|
|
158
|
+
it "memoizes per class without changing the answer" do
|
|
159
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
160
|
+
attribute :body, :string
|
|
161
|
+
|
|
162
|
+
xml do
|
|
163
|
+
element "doc"
|
|
164
|
+
map_element "body", to: :body
|
|
165
|
+
html_entities
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
expect(klass.from_xml("<doc><body>a & b</body></doc>").body).to eq("a & b")
|
|
170
|
+
expect(klass.from_xml("<doc><body>a & b</body></doc>").body).to eq("a & b")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.42
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1765,6 +1765,7 @@ files:
|
|
|
1765
1765
|
- spec/lutaml/model/opal_smoke_spec.rb
|
|
1766
1766
|
- spec/lutaml/model/optimization_spec.rb
|
|
1767
1767
|
- spec/lutaml/model/ordered_content_spec.rb
|
|
1768
|
+
- spec/lutaml/model/parse_dispatch_hoist_spec.rb
|
|
1768
1769
|
- spec/lutaml/model/parsed_model_mutation_spec.rb
|
|
1769
1770
|
- spec/lutaml/model/phantom_value_spec.rb
|
|
1770
1771
|
- spec/lutaml/model/polymorphic_spec.rb
|