lutaml-model 0.8.41 → 0.8.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/docs/_guides/advanced-mapping.adoc +32 -0
- data/lib/lutaml/key_value/mapping.rb +3 -1
- data/lib/lutaml/key_value/mapping_rule.rb +4 -1
- data/lib/lutaml/key_value/transform.rb +2 -0
- data/lib/lutaml/key_value/transformation/rule_compiler.rb +3 -0
- data/lib/lutaml/model/attribute.rb +71 -32
- data/lib/lutaml/model/collection_handler.rb +14 -2
- data/lib/lutaml/model/global_context.rb +25 -1
- data/lib/lutaml/model/mapping/mapping_rule.rb +86 -20
- data/lib/lutaml/model/register.rb +7 -1
- data/lib/lutaml/model/serialize/initialization.rb +17 -3
- data/lib/lutaml/model/serialize.rb +12 -3
- data/lib/lutaml/model/type/string.rb +1 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/mapping.rb +20 -1
- data/lib/lutaml/xml/mapping_rule.rb +13 -1
- data/lib/lutaml/xml/model_transform.rb +90 -14
- data/lib/lutaml/xml/parse_session.rb +11 -2
- data/lib/lutaml/xml/transformation/element_builder.rb +25 -6
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
- data/spec/lutaml/key_value/transformation/rule_compiler_spec.rb +16 -6
- data/spec/lutaml/key_value/transformation_spec.rb +4 -1
- data/spec/lutaml/model/attributes_memo_clear_race_spec.rb +90 -0
- data/spec/lutaml/model/hydrate_only_mapping_spec.rb +47 -0
- data/spec/lutaml/model/parse_dispatch_hoist_spec.rb +173 -0
- data/spec/lutaml/xml/when_attribute_spec.rb +133 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fe2357c36c2a199a3788090dffd420874612482e21e7ac678bd5f1e6fdd575ce
|
|
4
|
+
data.tar.gz: a3d601af07558b2915dce9f4df383c00b905505c8fbcd192a89ad88b3e66ea49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c82bb3761127b4beb8f2d267a60d107d69eb509cd2fd094763eed5ab5d0814497252db4cd1311709072ab829b156aac46f839852bcc4f20f37e627aa7b5daea7
|
|
7
|
+
data.tar.gz: 32aa679d229c03ccccf0ee8f1359f9559309b92f5493a0207fd5a495e0f8ef1a0cc3255a0e4e30c2e7c5319e8910a8863286116d6a0e4bfe72af1a0b8be2c71f
|
|
@@ -562,3 +562,35 @@ end
|
|
|
562
562
|
====
|
|
563
563
|
|
|
564
564
|
|
|
565
|
+
|
|
566
|
+
=== Attribute-value dispatch on a shared element name
|
|
567
|
+
|
|
568
|
+
When several distinct attributes share one wire element, selected by a
|
|
569
|
+
sibling attribute's value, declare the discriminator with `when_attribute:`
|
|
570
|
+
|
|
571
|
+
[source,ruby]
|
|
572
|
+
----
|
|
573
|
+
class Requirement < Lutaml::Model::Serializable
|
|
574
|
+
attribute :guidance, Component, collection: true
|
|
575
|
+
attribute :purpose, Component, collection: true
|
|
576
|
+
|
|
577
|
+
xml do
|
|
578
|
+
element "requirement"
|
|
579
|
+
map_element "component", to: :guidance,
|
|
580
|
+
when_attribute: { "type" => "guidance" }
|
|
581
|
+
map_element "component", to: :purpose,
|
|
582
|
+
when_attribute: { "type" => "purpose" }
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
----
|
|
586
|
+
|
|
587
|
+
Each rule hydrates only from the `<component>` occurrences whose `type`
|
|
588
|
+
attribute carries the expected value, in document order; uncovered values
|
|
589
|
+
are ignored by all discriminator rules. On serialization the discriminator
|
|
590
|
+
pair is re-emitted on the element (unless the value's own mapping already
|
|
591
|
+
writes that attribute).
|
|
592
|
+
|
|
593
|
+
Co-occurrence semantics: several `when_attribute` rules may share the
|
|
594
|
+
element name; each target attribute fills independently, so declare each
|
|
595
|
+
as `collection: true` when the element repeats. Rules without
|
|
596
|
+
`when_attribute` keep claiming every occurrence of their name as before.
|
|
@@ -78,7 +78,8 @@ module Lutaml
|
|
|
78
78
|
polymorphic: {},
|
|
79
79
|
polymorphic_map: {},
|
|
80
80
|
transform: {},
|
|
81
|
-
value_map: {}
|
|
81
|
+
value_map: {},
|
|
82
|
+
serialize: true
|
|
82
83
|
)
|
|
83
84
|
mapping_name = name_for_mapping(root_mappings, name)
|
|
84
85
|
validate!(mapping_name, to, with, render_nil, render_empty)
|
|
@@ -100,6 +101,7 @@ module Lutaml
|
|
|
100
101
|
polymorphic_map: polymorphic_map,
|
|
101
102
|
transform: transform,
|
|
102
103
|
value_map: value_map,
|
|
104
|
+
serialize: serialize,
|
|
103
105
|
)
|
|
104
106
|
end
|
|
105
107
|
|
|
@@ -22,7 +22,8 @@ module Lutaml
|
|
|
22
22
|
polymorphic: {},
|
|
23
23
|
polymorphic_map: {},
|
|
24
24
|
transform: {},
|
|
25
|
-
value_map: {}
|
|
25
|
+
value_map: {},
|
|
26
|
+
serialize: true
|
|
26
27
|
)
|
|
27
28
|
super(
|
|
28
29
|
name,
|
|
@@ -41,6 +42,7 @@ module Lutaml
|
|
|
41
42
|
polymorphic_map: polymorphic_map,
|
|
42
43
|
transform: transform,
|
|
43
44
|
value_map: value_map,
|
|
45
|
+
serialize: serialize,
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
@child_mappings = child_mappings
|
|
@@ -65,6 +67,7 @@ module Lutaml
|
|
|
65
67
|
child_mappings: Lutaml::Model::Utils.deep_dup(child_mappings),
|
|
66
68
|
root_mappings: Lutaml::Model::Utils.deep_dup(root_mappings),
|
|
67
69
|
value_map: Lutaml::Model::Utils.deep_dup(@value_map),
|
|
70
|
+
serialize: serialize?,
|
|
68
71
|
)
|
|
69
72
|
end
|
|
70
73
|
|
|
@@ -230,6 +230,7 @@ transformation_factory:)
|
|
|
230
230
|
custom_methods: custom_methods,
|
|
231
231
|
delegate: delegate,
|
|
232
232
|
root_mappings: mapping_rule.root_mappings,
|
|
233
|
+
serialize: mapping_rule.serialize?,
|
|
233
234
|
)
|
|
234
235
|
end
|
|
235
236
|
|
|
@@ -239,6 +240,8 @@ transformation_factory:)
|
|
|
239
240
|
# @param options [Hash] Transformation options (may contain :only, :except)
|
|
240
241
|
# @return [Boolean] true if the rule should be applied
|
|
241
242
|
def valid_mapping?(rule, options)
|
|
243
|
+
return false if rule.option(:serialize) == false
|
|
244
|
+
|
|
242
245
|
only = options[:only]
|
|
243
246
|
except = options[:except]
|
|
244
247
|
name = rule.attribute_name
|
|
@@ -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
|
|
@@ -59,7 +61,17 @@ module Lutaml
|
|
|
59
61
|
# @param register [Symbol, nil] Register for custom-collection type resolution
|
|
60
62
|
# @return [Object] A new collection instance
|
|
61
63
|
def build_collection(*args, register: nil)
|
|
62
|
-
|
|
64
|
+
# args.flatten allocated a fresh Array even for the dominant
|
|
65
|
+
# calls: no args, or one already-flat Array from cast_items
|
|
66
|
+
# (71,949 flatten allocations per ISO-13849 parse).
|
|
67
|
+
items = if args.empty?
|
|
68
|
+
[]
|
|
69
|
+
elsif args.length == 1 && (first = args[0]).is_a?(::Array) &&
|
|
70
|
+
first.none?(::Array)
|
|
71
|
+
first
|
|
72
|
+
else
|
|
73
|
+
args.flatten
|
|
74
|
+
end
|
|
63
75
|
unless register && initialize_accepts_register?
|
|
64
76
|
return collection_class.new(items)
|
|
65
77
|
end
|
|
@@ -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
|
|
@@ -70,7 +70,9 @@ module Lutaml
|
|
|
70
70
|
polymorphic: {},
|
|
71
71
|
polymorphic_map: {},
|
|
72
72
|
transform: {},
|
|
73
|
-
value_map: {}
|
|
73
|
+
value_map: {},
|
|
74
|
+
serialize: true,
|
|
75
|
+
when_attribute: {}
|
|
74
76
|
)
|
|
75
77
|
@name = name
|
|
76
78
|
@to = to
|
|
@@ -92,6 +94,11 @@ module Lutaml
|
|
|
92
94
|
@polymorphic = polymorphic
|
|
93
95
|
@polymorphic_map = polymorphic_map
|
|
94
96
|
@transform = transform
|
|
97
|
+
@serialize_mapping = serialize
|
|
98
|
+
# Attribute-value discriminator (lutaml-model#88): select this
|
|
99
|
+
# rule's wire occurrences by a sibling attribute value, e.g.
|
|
100
|
+
# `when_attribute: { "type" => "guidance" }`.
|
|
101
|
+
@when_attribute = (when_attribute || {}).freeze
|
|
95
102
|
|
|
96
103
|
# Cache whether this rule needs the full deserialize chain.
|
|
97
104
|
# Over 95% of rules are "simple" (no custom method, no delegate).
|
|
@@ -122,6 +129,15 @@ module Lutaml
|
|
|
122
129
|
@value_map[:from].freeze
|
|
123
130
|
@value_map[:to].freeze
|
|
124
131
|
@value_map.freeze
|
|
132
|
+
|
|
133
|
+
# The :from treatment flags are read on every rule application of
|
|
134
|
+
# every parse; they are static once @value_map is final. The three
|
|
135
|
+
# Hash#[] chains per value used to cost 1.4 M value_map calls per
|
|
136
|
+
# ISO-13849 parse.
|
|
137
|
+
from_map = @value_map[:from]
|
|
138
|
+
@treat_nil_flag = from_map[:nil] != :omitted
|
|
139
|
+
@treat_empty_flag = from_map[:empty] != :omitted
|
|
140
|
+
@treat_omitted_flag = from_map[:omitted] != :omitted
|
|
125
141
|
end
|
|
126
142
|
|
|
127
143
|
def default_value_map(options = {})
|
|
@@ -165,6 +181,11 @@ module Lutaml
|
|
|
165
181
|
|
|
166
182
|
alias from name
|
|
167
183
|
alias render_default? render_default
|
|
184
|
+
|
|
185
|
+
# Whether this rule participates in serialization output. False
|
|
186
|
+
# marks a hydrate-only mapping: consumed by from_* but never
|
|
187
|
+
# rendered by to_*. (The value callback #serialize is separate.)
|
|
188
|
+
def serialize? = @serialize_mapping
|
|
168
189
|
alias attribute? attribute
|
|
169
190
|
|
|
170
191
|
def render?(value, instance = nil, options = {})
|
|
@@ -243,14 +264,20 @@ module Lutaml
|
|
|
243
264
|
end
|
|
244
265
|
|
|
245
266
|
def treat_nil?(options = {})
|
|
267
|
+
return @treat_nil_flag unless options[:nil] || options[:empty] || options[:omitted]
|
|
268
|
+
|
|
246
269
|
value_map(:from, options)[:nil] != :omitted
|
|
247
270
|
end
|
|
248
271
|
|
|
249
272
|
def treat_empty?(options = {})
|
|
273
|
+
return @treat_empty_flag unless options[:nil] || options[:empty] || options[:omitted]
|
|
274
|
+
|
|
250
275
|
value_map(:from, options)[:empty] != :omitted
|
|
251
276
|
end
|
|
252
277
|
|
|
253
278
|
def treat_omitted?(options = {})
|
|
279
|
+
return @treat_omitted_flag unless options[:nil] || options[:empty] || options[:omitted]
|
|
280
|
+
|
|
254
281
|
value_map(:from, options)[:omitted] != :omitted
|
|
255
282
|
end
|
|
256
283
|
|
|
@@ -313,6 +340,16 @@ context = nil)
|
|
|
313
340
|
# each time (240k allocations on the ISO-13849 serialize profile).
|
|
314
341
|
# Rules may be frozen, so no per-instance memo: one to_s instead
|
|
315
342
|
# of two is the frozen-safe diet.
|
|
343
|
+
# Discriminator pairs this rule selects (and re-emits) on, empty
|
|
344
|
+
# for an ordinary rule.
|
|
345
|
+
def when_attribute
|
|
346
|
+
@when_attribute ||= {}.freeze
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def when_attribute?
|
|
350
|
+
!when_attribute.empty?
|
|
351
|
+
end
|
|
352
|
+
|
|
316
353
|
def custom_method_only?
|
|
317
354
|
name = attribute_name.to_s
|
|
318
355
|
(name.start_with?("__") && name.end_with?("__")) ||
|
|
@@ -402,6 +439,47 @@ context = nil)
|
|
|
402
439
|
# Frozen empty array for the common case of no transformers
|
|
403
440
|
EMPTY_TRANSFORMERS = [].freeze
|
|
404
441
|
|
|
442
|
+
# Verdict for handle_transform_method's dispatch, computed once per
|
|
443
|
+
# (rule, attribute) — both are static after definition, but rules may
|
|
444
|
+
# be frozen, so the memo lives at class level keyed on identity.
|
|
445
|
+
# :assign — no transformer runs on deserialize (fast path);
|
|
446
|
+
# :import — hash/proc transformers go through ImportTransformer.
|
|
447
|
+
TRANSFORM_DISPATCH = {}.compare_by_identity
|
|
448
|
+
|
|
449
|
+
def self.transform_dispatch(rule, attr)
|
|
450
|
+
per_attr = TRANSFORM_DISPATCH[rule]
|
|
451
|
+
if per_attr.nil?
|
|
452
|
+
per_attr = {}.compare_by_identity
|
|
453
|
+
TRANSFORM_DISPATCH[rule] = per_attr
|
|
454
|
+
end
|
|
455
|
+
verdict = per_attr[attr]
|
|
456
|
+
return verdict if verdict
|
|
457
|
+
|
|
458
|
+
rule_transform = rule.transform
|
|
459
|
+
# NB: `::Hash` — inside Lutaml::Model the bare constant `Hash` is
|
|
460
|
+
# the hash-format adapter class, so a plain is_a?(Hash) is always
|
|
461
|
+
# false here (the original dispatch shared that shadow and never
|
|
462
|
+
# took the hash-transform branch).
|
|
463
|
+
rule_has = rule_transform.is_a?(Class) ||
|
|
464
|
+
(rule_transform.is_a?(::Hash) && !rule_transform.empty?)
|
|
465
|
+
attr_transform = attr&.transform
|
|
466
|
+
attr_has = attr_transform &&
|
|
467
|
+
(attr_transform.is_a?(Class) ||
|
|
468
|
+
(attr_transform.is_a?(::Hash) && !attr_transform.empty?))
|
|
469
|
+
|
|
470
|
+
verdict = if !rule_has && !attr_has
|
|
471
|
+
:assign
|
|
472
|
+
elsif rule.get_transformers(attr).any? do |t|
|
|
473
|
+
t.is_a?(Class) && t < Lutaml::Model::ValueTransformer
|
|
474
|
+
end
|
|
475
|
+
:assign
|
|
476
|
+
else
|
|
477
|
+
:import
|
|
478
|
+
end
|
|
479
|
+
per_attr[attr] = verdict
|
|
480
|
+
verdict
|
|
481
|
+
end
|
|
482
|
+
|
|
405
483
|
def get_transformers(attribute)
|
|
406
484
|
# Fast path: most rules have no transforms at all
|
|
407
485
|
rule_transform = transform
|
|
@@ -480,28 +558,16 @@ context = nil)
|
|
|
480
558
|
|
|
481
559
|
def handle_transform_method(model, value, attributes, context = nil)
|
|
482
560
|
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
|
|
561
|
+
# The transform verdict (none / class-based already applied /
|
|
562
|
+
# hash-proc via ImportTransformer) is static per (rule, attr) —
|
|
563
|
+
# it was recomputed with is_a? chains on every rule application
|
|
564
|
+
# (16.8 M Kernel#is_a? per ISO-13849 parse across the walk).
|
|
565
|
+
if self.class.transform_dispatch(self, attr) == :import
|
|
502
566
|
transformed = ImportTransformer.call(value, self, attr,
|
|
503
567
|
context: context)
|
|
504
568
|
assign_value(model, transformed)
|
|
569
|
+
else
|
|
570
|
+
assign_value(model, value)
|
|
505
571
|
end
|
|
506
572
|
true
|
|
507
573
|
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,26 @@ 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]
|
|
129
|
-
|
|
132
|
+
cached = (@merged_attributes_cache ||= {})[register_id]
|
|
133
|
+
return cached if cached
|
|
134
|
+
|
|
135
|
+
ensure_imports!(register) if finalized?
|
|
136
|
+
# ensure_imports! may re-entrantly run clear_cache (restrict /
|
|
137
|
+
# import resolution), which nils @merged_attributes_cache —
|
|
138
|
+
# compute the merge first and re-materialize the memo store
|
|
139
|
+
# at write time (#815: standalone to_xml on register-bound
|
|
140
|
+
# models crashed with nil[] here).
|
|
141
|
+
merged = @attributes.merge(@register_records[register_id][:attributes])
|
|
142
|
+
(@merged_attributes_cache ||= {})[register_id] = merged
|
|
130
143
|
else
|
|
144
|
+
ensure_imports!(register) if finalized?
|
|
131
145
|
@attributes
|
|
132
146
|
end
|
|
133
147
|
end
|
|
@@ -241,12 +241,21 @@ module Lutaml
|
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
def respond_to_missing?(method_name, include_private = false)
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
# Setter probes from non-compiled dispatch land here ~24k times per
|
|
245
|
+
# ISO-13849 parse; strip the suffix once instead of re-walking
|
|
246
|
+
# attribute_exist?'s to_s/chomp/to_sym chain.
|
|
247
|
+
name = method_name.to_s
|
|
248
|
+
if name.end_with?("=") &&
|
|
249
|
+
self.class.attributes(lutaml_register)
|
|
250
|
+
.key?(name.delete_suffix("=").to_sym)
|
|
251
|
+
return true
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
super
|
|
246
255
|
end
|
|
247
256
|
|
|
248
257
|
def attribute_exist?(name)
|
|
249
|
-
name = name.to_s.
|
|
258
|
+
name = name.to_s.delete_suffix("=").to_sym if name.end_with?("=")
|
|
250
259
|
|
|
251
260
|
self.class.attributes(lutaml_register).key?(name)
|
|
252
261
|
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
|
|