lutaml-model 0.8.6 → 0.8.8
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/.rubocop_todo.yml +15 -68
- data/lib/lutaml/model/attribute.rb +5 -7
- data/lib/lutaml/model/attribute_validator.rb +3 -1
- data/lib/lutaml/model/choice.rb +1 -1
- data/lib/lutaml/model/deep_dupable.rb +16 -0
- data/lib/lutaml/model/mapping/mapping.rb +2 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +5 -3
- data/lib/lutaml/model/sequence.rb +4 -2
- data/lib/lutaml/model/serialize/initialization.rb +91 -4
- data/lib/lutaml/model/serialize.rb +24 -34
- data/lib/lutaml/model/store.rb +75 -6
- data/lib/lutaml/model/transform.rb +14 -3
- data/lib/lutaml/model/type/hash.rb +9 -5
- data/lib/lutaml/model/type/symbol.rb +1 -1
- data/lib/lutaml/model/utils.rb +15 -4
- data/lib/lutaml/model/validation.rb +8 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +1 -0
- data/lib/lutaml/xml/schema/xsd/schema.rb +8 -5
- data/lib/lutaml/xml/serialization/instance_methods.rb +2 -0
- data/lib/lutaml/xml/xml_element.rb +1 -1
- data/spec/lutaml/model/attribute_spec.rb +8 -19
- data/spec/lutaml/model/register_methods_spec.rb +149 -0
- data/spec/lutaml/model/store_spec.rb +120 -0
- data/spec/lutaml/model/transform_cache_spec.rb +42 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +10 -3
- data/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb +35 -0
- data/spec/lutaml/xml/schema/xsd/spec_helper.rb +1 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 076bdbfb0836ad989e3fe90d5287d9662f35fb00492b6dfed0aef95db7296cdd
|
|
4
|
+
data.tar.gz: 1008556dd2eb02cd3aa08cffc2278f1952fab2d4c97af47a96eeea2b0975d622
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9019e0d1223841cc427b5ebe1c5e6afffa5f88b8abd9ab72fe521908ec43d431fc59bacc9054c50d3f190a436baae4047e12ad922db253b800554c4b1c93408
|
|
7
|
+
data.tar.gz: 33d5644f9275a211f45ea8d89a6bd371dab01b8140dfbd62a44c31496b33d09cda2c5ba7b03e361733e450d6186663fce61e449f80f51e1353ee4ec1a7dbfd5e
|
data/.rubocop_todo.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This configuration was generated by
|
|
2
2
|
# `rubocop --auto-gen-config`
|
|
3
|
-
# on 2026-05-
|
|
3
|
+
# on 2026-05-14 09:40:04 UTC using RuboCop version 1.86.0.
|
|
4
4
|
# The point is for the user to remove these configuration records
|
|
5
5
|
# one by one as the offenses are removed from the code base.
|
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
|
@@ -11,66 +11,27 @@ Gemspec/RequiredRubyVersion:
|
|
|
11
11
|
Exclude:
|
|
12
12
|
- 'lutaml-model.gemspec'
|
|
13
13
|
|
|
14
|
-
# Offense count:
|
|
14
|
+
# Offense count: 1
|
|
15
15
|
# This cop supports safe autocorrection (--autocorrect).
|
|
16
16
|
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
|
17
17
|
# SupportedStyles: with_first_argument, with_fixed_indentation
|
|
18
18
|
Layout/ArgumentAlignment:
|
|
19
19
|
Exclude:
|
|
20
|
-
- 'lib/lutaml/
|
|
21
|
-
- 'lib/lutaml/xml/adapter/xml_parser.rb'
|
|
22
|
-
- 'lib/lutaml/xml/serialization/instance_methods.rb'
|
|
23
|
-
- 'spec/lutaml/xml/schema/compiler_spec.rb'
|
|
24
|
-
|
|
25
|
-
# Offense count: 1
|
|
26
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
27
|
-
# Configuration parameters: IndentationWidth.
|
|
28
|
-
Layout/AssignmentIndentation:
|
|
29
|
-
Exclude:
|
|
30
|
-
- 'lib/lutaml/xml/adapter/xml_serializer.rb'
|
|
31
|
-
|
|
32
|
-
# Offense count: 3
|
|
33
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
34
|
-
# Configuration parameters: EnforcedStyleAlignWith.
|
|
35
|
-
# SupportedStylesAlignWith: either, start_of_block, start_of_line
|
|
36
|
-
Layout/BlockAlignment:
|
|
37
|
-
Exclude:
|
|
38
|
-
- 'lib/lutaml/xml/serialization/format_conversion.rb'
|
|
39
|
-
- 'lib/lutaml/xml/xml_element.rb'
|
|
40
|
-
|
|
41
|
-
# Offense count: 3
|
|
42
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
43
|
-
Layout/BlockEndNewline:
|
|
44
|
-
Exclude:
|
|
45
|
-
- 'lib/lutaml/xml/serialization/format_conversion.rb'
|
|
46
|
-
- 'lib/lutaml/xml/xml_element.rb'
|
|
47
|
-
|
|
48
|
-
# Offense count: 6
|
|
49
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
50
|
-
# Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
|
|
51
|
-
# SupportedStylesAlignWith: start_of_line, relative_to_receiver
|
|
52
|
-
Layout/IndentationWidth:
|
|
53
|
-
Exclude:
|
|
54
|
-
- 'lib/lutaml/xml/serialization/format_conversion.rb'
|
|
55
|
-
- 'lib/lutaml/xml/xml_element.rb'
|
|
20
|
+
- 'lib/lutaml/model/store.rb'
|
|
56
21
|
|
|
57
|
-
# Offense count:
|
|
22
|
+
# Offense count: 2982
|
|
58
23
|
# This cop supports safe autocorrection (--autocorrect).
|
|
59
24
|
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
|
60
25
|
# URISchemes: http, https
|
|
61
26
|
Layout/LineLength:
|
|
62
27
|
Enabled: false
|
|
63
28
|
|
|
64
|
-
# Offense count:
|
|
29
|
+
# Offense count: 1
|
|
65
30
|
# This cop supports safe autocorrection (--autocorrect).
|
|
66
31
|
# Configuration parameters: AllowInHeredoc.
|
|
67
32
|
Layout/TrailingWhitespace:
|
|
68
33
|
Exclude:
|
|
69
|
-
- 'lib/lutaml/
|
|
70
|
-
- 'lib/lutaml/xml/adapter/xml_parser.rb'
|
|
71
|
-
- 'lib/lutaml/xml/adapter/xml_serializer.rb'
|
|
72
|
-
- 'lib/lutaml/xml/serialization/instance_methods.rb'
|
|
73
|
-
- 'spec/lutaml/xml/schema/compiler_spec.rb'
|
|
34
|
+
- 'lib/lutaml/model/store.rb'
|
|
74
35
|
|
|
75
36
|
# Offense count: 21
|
|
76
37
|
# Configuration parameters: AllowedMethods.
|
|
@@ -83,7 +44,7 @@ Lint/ConstantDefinitionInBlock:
|
|
|
83
44
|
- 'spec/lutaml/xml/type_namespace_prefix_spec.rb'
|
|
84
45
|
- 'spec/lutaml/xml/xml_space_type_spec.rb'
|
|
85
46
|
|
|
86
|
-
# Offense count:
|
|
47
|
+
# Offense count: 36
|
|
87
48
|
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
|
|
88
49
|
Lint/DuplicateBranch:
|
|
89
50
|
Enabled: false
|
|
@@ -184,12 +145,12 @@ Metrics/BlockLength:
|
|
|
184
145
|
Metrics/BlockNesting:
|
|
185
146
|
Max: 6
|
|
186
147
|
|
|
187
|
-
# Offense count:
|
|
148
|
+
# Offense count: 302
|
|
188
149
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
189
150
|
Metrics/CyclomaticComplexity:
|
|
190
151
|
Enabled: false
|
|
191
152
|
|
|
192
|
-
# Offense count:
|
|
153
|
+
# Offense count: 554
|
|
193
154
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
194
155
|
Metrics/MethodLength:
|
|
195
156
|
Max: 514
|
|
@@ -200,7 +161,7 @@ Metrics/ParameterLists:
|
|
|
200
161
|
Max: 24
|
|
201
162
|
MaxOptionalParameters: 5
|
|
202
163
|
|
|
203
|
-
# Offense count:
|
|
164
|
+
# Offense count: 253
|
|
204
165
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
205
166
|
Metrics/PerceivedComplexity:
|
|
206
167
|
Enabled: false
|
|
@@ -292,7 +253,7 @@ RSpec/BeforeAfterAll:
|
|
|
292
253
|
RSpec/ContextWording:
|
|
293
254
|
Enabled: false
|
|
294
255
|
|
|
295
|
-
# Offense count:
|
|
256
|
+
# Offense count: 95
|
|
296
257
|
# Configuration parameters: IgnoredMetadata.
|
|
297
258
|
RSpec/DescribeClass:
|
|
298
259
|
Enabled: false
|
|
@@ -303,7 +264,7 @@ RSpec/DescribeMethod:
|
|
|
303
264
|
- 'spec/lutaml/xml/schema/xsd/schema_helper_methods_spec.rb'
|
|
304
265
|
- 'spec/lutaml/xml/serializable_namespace_spec.rb'
|
|
305
266
|
|
|
306
|
-
# Offense count:
|
|
267
|
+
# Offense count: 1246
|
|
307
268
|
# Configuration parameters: CountAsOne.
|
|
308
269
|
RSpec/ExampleLength:
|
|
309
270
|
Max: 68
|
|
@@ -378,7 +339,7 @@ RSpec/MultipleDescribes:
|
|
|
378
339
|
- 'spec/lutaml/xml/namespace_resolution_strategy_spec.rb'
|
|
379
340
|
- 'spec/lutaml/xml/xml_space_type_spec.rb'
|
|
380
341
|
|
|
381
|
-
# Offense count:
|
|
342
|
+
# Offense count: 1482
|
|
382
343
|
RSpec/MultipleExpectations:
|
|
383
344
|
Max: 21
|
|
384
345
|
|
|
@@ -451,18 +412,6 @@ Security/MarshalLoad:
|
|
|
451
412
|
Exclude:
|
|
452
413
|
- 'scripts-xmi-profile/profile_xmi.rb'
|
|
453
414
|
|
|
454
|
-
# Offense count: 4
|
|
455
|
-
# This cop supports safe autocorrection (--autocorrect).
|
|
456
|
-
# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
|
|
457
|
-
# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
|
|
458
|
-
# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
|
|
459
|
-
# FunctionalMethods: let, let!, subject, watch
|
|
460
|
-
# AllowedMethods: lambda, proc, it
|
|
461
|
-
Style/BlockDelimiters:
|
|
462
|
-
Exclude:
|
|
463
|
-
- 'lib/lutaml/xml/serialization/format_conversion.rb'
|
|
464
|
-
- 'lib/lutaml/xml/xml_element.rb'
|
|
465
|
-
|
|
466
415
|
# Offense count: 2
|
|
467
416
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
|
468
417
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
@@ -508,13 +457,11 @@ Style/MixinUsage:
|
|
|
508
457
|
- 'bench/bench_unitsml.rb'
|
|
509
458
|
- 'bench/bench_xmi.rb'
|
|
510
459
|
|
|
511
|
-
# Offense count:
|
|
460
|
+
# Offense count: 1
|
|
512
461
|
# This cop supports safe autocorrection (--autocorrect).
|
|
513
462
|
Style/MultilineIfModifier:
|
|
514
463
|
Exclude:
|
|
515
|
-
- 'lib/lutaml/
|
|
516
|
-
- 'lib/lutaml/xml/adapter/xml_serializer.rb'
|
|
517
|
-
- 'spec/lutaml/xml/schema/compiler_spec.rb'
|
|
464
|
+
- 'lib/lutaml/model/store.rb'
|
|
518
465
|
|
|
519
466
|
# Offense count: 12
|
|
520
467
|
# Configuration parameters: AllowedClasses.
|
|
@@ -6,6 +6,7 @@ module Lutaml
|
|
|
6
6
|
attr_reader :name, :options
|
|
7
7
|
|
|
8
8
|
include CollectionHandler
|
|
9
|
+
include DeepDupable
|
|
9
10
|
|
|
10
11
|
ALLOWED_OPTIONS = %i[
|
|
11
12
|
raw
|
|
@@ -194,7 +195,7 @@ module Lutaml
|
|
|
194
195
|
end
|
|
195
196
|
|
|
196
197
|
# If we don't have an actual Register object, fall back to standard resolution
|
|
197
|
-
result = if actual_register.
|
|
198
|
+
result = if actual_register.is_a?(Lutaml::Model::Register)
|
|
198
199
|
actual_register.resolve_in_namespace(unresolved_type,
|
|
199
200
|
namespace_uri)
|
|
200
201
|
end
|
|
@@ -290,7 +291,7 @@ module Lutaml
|
|
|
290
291
|
def required_value_set?(value)
|
|
291
292
|
return true unless options[:required]
|
|
292
293
|
return false if value.nil?
|
|
293
|
-
return false if
|
|
294
|
+
return false if Utils.empty?(value)
|
|
294
295
|
|
|
295
296
|
true
|
|
296
297
|
end
|
|
@@ -573,7 +574,6 @@ instance_object = nil)
|
|
|
573
574
|
end
|
|
574
575
|
|
|
575
576
|
def model_instance?(value)
|
|
576
|
-
return false unless value.respond_to?(:class)
|
|
577
577
|
return false unless @options[:ref_model_class]
|
|
578
578
|
|
|
579
579
|
value.class.name == @options[:ref_model_class]
|
|
@@ -609,10 +609,8 @@ instance_object = nil)
|
|
|
609
609
|
# These are never Serializable, so skip expensive can_serialize? and needs_conversion? checks
|
|
610
610
|
# Skip if type has custom from_xml/from_json methods (defined on the class itself, not inherited)
|
|
611
611
|
if resolved_type.is_a?(Class) && resolved_type < Lutaml::Model::Type::Value
|
|
612
|
-
has_custom_from_xml = resolved_type.
|
|
613
|
-
|
|
614
|
-
has_custom_from_json = resolved_type.respond_to?(:from_json) &&
|
|
615
|
-
resolved_type.method(:from_json).owner != Lutaml::Model::Type::Value
|
|
612
|
+
has_custom_from_xml = resolved_type.method(:from_xml).owner != Lutaml::Model::Type::Value
|
|
613
|
+
has_custom_from_json = resolved_type.method(:from_json).owner != Lutaml::Model::Type::Value
|
|
616
614
|
return resolved_type.cast(value) unless has_custom_from_xml || has_custom_from_json
|
|
617
615
|
end
|
|
618
616
|
|
|
@@ -56,7 +56,9 @@ module Lutaml
|
|
|
56
56
|
def ensure_required?(value)
|
|
57
57
|
return true unless attribute.options[:required]
|
|
58
58
|
return false if value.nil?
|
|
59
|
-
return false if value.
|
|
59
|
+
return false if value.is_a?(String) && value.empty?
|
|
60
|
+
return false if value.is_a?(Array) && value.empty?
|
|
61
|
+
return false if value.is_a?(Hash) && value.empty?
|
|
60
62
|
|
|
61
63
|
true
|
|
62
64
|
end
|
data/lib/lutaml/model/choice.rb
CHANGED
|
@@ -252,7 +252,7 @@ register = nil)
|
|
|
252
252
|
def should_render_empty?(object, attribute)
|
|
253
253
|
value = object.public_send(attribute.name)
|
|
254
254
|
return false if Utils.uninitialized?(value)
|
|
255
|
-
return false unless Utils.empty?(value)
|
|
255
|
+
return false unless Utils.empty?(value)
|
|
256
256
|
return false unless @format
|
|
257
257
|
|
|
258
258
|
# Check if this attribute should render when empty
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
# Protocol module for objects that support deep duplication.
|
|
6
|
+
#
|
|
7
|
+
# Include this module in any class that needs custom deep-copy semantics
|
|
8
|
+
# beyond Ruby's default `dup`. Used by Utils.deep_dup to dispatch correctly
|
|
9
|
+
# via type checking instead of respond_to?.
|
|
10
|
+
module DeepDupable
|
|
11
|
+
def deep_dup
|
|
12
|
+
raise NotImplementedError, "#{self.class} must implement deep_dup"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Lutaml
|
|
2
2
|
module Model
|
|
3
3
|
class MappingRule
|
|
4
|
+
include DeepDupable
|
|
5
|
+
|
|
4
6
|
attr_reader :name,
|
|
5
7
|
:to,
|
|
6
8
|
:to_instance,
|
|
@@ -163,7 +165,7 @@ module Lutaml
|
|
|
163
165
|
# This handles the case where collection is mutated with << or custom methods
|
|
164
166
|
elsif mutated_collection?(value, instance)
|
|
165
167
|
true
|
|
166
|
-
elsif instance.
|
|
168
|
+
elsif instance.is_a?(Lutaml::Model::Serialize) && instance.using_default?(to)
|
|
167
169
|
render_default? || RenderPolicy.derived_attribute_for?(instance, to)
|
|
168
170
|
else
|
|
169
171
|
true
|
|
@@ -200,13 +202,13 @@ module Lutaml
|
|
|
200
202
|
return false if value.empty? # Empty collection is still default
|
|
201
203
|
|
|
202
204
|
# If it's a non-empty collection and marked as using_default, it was mutated
|
|
203
|
-
instance.
|
|
205
|
+
instance.is_a?(Lutaml::Model::Serialize) && instance.using_default?(to)
|
|
204
206
|
end
|
|
205
207
|
|
|
206
208
|
# Check if value is a non-empty collection
|
|
207
209
|
def has_items?(value)
|
|
208
210
|
return false if value.nil? || Utils.uninitialized?(value)
|
|
209
|
-
return false unless value.
|
|
211
|
+
return false unless value.is_a?(String) || value.is_a?(Array) || value.is_a?(Hash)
|
|
210
212
|
|
|
211
213
|
!value.empty?
|
|
212
214
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
5
|
class Sequence
|
|
6
|
+
include DeepDupable
|
|
7
|
+
|
|
6
8
|
attr_accessor :model, :attributes
|
|
7
9
|
attr_reader :format
|
|
8
10
|
|
|
@@ -82,11 +84,11 @@ module Lutaml
|
|
|
82
84
|
def validate_child_content(instance, register)
|
|
83
85
|
instance.class.attributes(register).each_key do |name|
|
|
84
86
|
value = instance.public_send(name)
|
|
85
|
-
if value.
|
|
87
|
+
if value.is_a?(Lutaml::Model::Serialize)
|
|
86
88
|
value.validate!
|
|
87
89
|
elsif value.is_a?(Array)
|
|
88
90
|
value.each do |v|
|
|
89
|
-
v.validate!(register: register) if v.
|
|
91
|
+
v.validate!(register: register) if v.is_a?(Lutaml::Model::Serialize)
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
94
|
end
|
|
@@ -180,9 +180,14 @@ module Lutaml
|
|
|
180
180
|
# (Single Source of Truth - no longer uses instance variables)
|
|
181
181
|
TransformationRegistry.instance.clear
|
|
182
182
|
|
|
183
|
+
# Clear Transform cache (uses class identity as key)
|
|
184
|
+
Transform.invalidate_for(self, register_id)
|
|
185
|
+
|
|
183
186
|
# Clear import resolution guard flags so imports can be re-resolved
|
|
184
187
|
instance_variables.each do |ivar|
|
|
185
|
-
|
|
188
|
+
ivar_s = ivar.to_s
|
|
189
|
+
remove_instance_variable(ivar) if ivar_s.start_with?("@_imports_resolved_") ||
|
|
190
|
+
ivar_s == "@_register_methods_defined"
|
|
186
191
|
end
|
|
187
192
|
end
|
|
188
193
|
|
|
@@ -282,14 +287,96 @@ module Lutaml
|
|
|
282
287
|
def allocate_for_deserialization(register = nil)
|
|
283
288
|
instance = allocate
|
|
284
289
|
register_id = extract_register_id(register)
|
|
285
|
-
instance.
|
|
286
|
-
instance.send(:define_singleton_attribute_methods)
|
|
287
|
-
instance.send(:register_in_reference_store)
|
|
290
|
+
instance.finalize_deserialization(register_id)
|
|
288
291
|
instance
|
|
289
292
|
end
|
|
290
293
|
|
|
294
|
+
# Define register-specific attribute methods on the class itself.
|
|
295
|
+
#
|
|
296
|
+
# Called once per (class, register) combination. Replaces per-instance
|
|
297
|
+
# singleton class allocation with class-level method definitions,
|
|
298
|
+
# preserving Ruby's inline method cache optimization.
|
|
299
|
+
#
|
|
300
|
+
# @param register_id [Symbol] The register ID
|
|
301
|
+
def ensure_register_methods_defined(register_id)
|
|
302
|
+
return if register_id == :default
|
|
303
|
+
|
|
304
|
+
@_register_methods_defined ||= {}
|
|
305
|
+
return if @_register_methods_defined[register_id]
|
|
306
|
+
|
|
307
|
+
reg_record = register_records[register_id]
|
|
308
|
+
return unless reg_record
|
|
309
|
+
|
|
310
|
+
default_attrs = instance_variable_get(:@attributes) || {}
|
|
311
|
+
reg_record_attrs = reg_record[:attributes] || {}
|
|
312
|
+
|
|
313
|
+
reg_record_attrs.each do |name, attr|
|
|
314
|
+
next if default_attrs.key?(name)
|
|
315
|
+
next if method_defined?(name, false)
|
|
316
|
+
|
|
317
|
+
if attr.collection?
|
|
318
|
+
define_collection_register_methods(name)
|
|
319
|
+
else
|
|
320
|
+
define_scalar_register_methods(name)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
@_register_methods_defined[register_id] = true
|
|
325
|
+
end
|
|
326
|
+
|
|
291
327
|
private
|
|
292
328
|
|
|
329
|
+
# Define getter/setter for a scalar register-specific attribute.
|
|
330
|
+
def define_scalar_register_methods(name)
|
|
331
|
+
define_method(name) do |*args|
|
|
332
|
+
if args.empty?
|
|
333
|
+
instance_variable_get(:"@#{name}")
|
|
334
|
+
else
|
|
335
|
+
send(:"#{name}=", args.first)
|
|
336
|
+
track_order(name, args.first, nil) if @__order_tracking__
|
|
337
|
+
args.first
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
define_method(:"#{name}=") do |value|
|
|
342
|
+
value_set_for(name)
|
|
343
|
+
reg_attr = resolve_register_attr(name)
|
|
344
|
+
value = reg_attr.cast_value(value, lutaml_register)
|
|
345
|
+
instance_variable_set(:"@#{name}", value)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Define getter/setter for a collection register-specific attribute.
|
|
350
|
+
def define_collection_register_methods(name)
|
|
351
|
+
define_method(name) do |*args|
|
|
352
|
+
if args.empty?
|
|
353
|
+
current = instance_variable_get(:"@#{name}")
|
|
354
|
+
current.equal?(LAZY_EMPTY_COLLECTION) ? [] : current
|
|
355
|
+
else
|
|
356
|
+
value = args.first
|
|
357
|
+
current = instance_variable_get(:"@#{name}")
|
|
358
|
+
current = [] if current.equal?(LAZY_EMPTY_COLLECTION)
|
|
359
|
+
new_value = current.is_a?(Array) ? current + [value] : value
|
|
360
|
+
instance_variable_set(:"@#{name}", new_value)
|
|
361
|
+
track_order(name, value, nil) if @__order_tracking__
|
|
362
|
+
value
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
define_method(:"#{name}=") do |value|
|
|
367
|
+
value_set_for(name)
|
|
368
|
+
reg_attr = resolve_register_attr(name)
|
|
369
|
+
value = reg_attr.cast_value(value, lutaml_register)
|
|
370
|
+
current = instance_variable_get(:"@#{name}")
|
|
371
|
+
if current.equal?(LAZY_EMPTY_COLLECTION) &&
|
|
372
|
+
(value.nil? || Lutaml::Model::Utils.uninitialized?(value))
|
|
373
|
+
# Sentinel stays — no allocation for empty collections
|
|
374
|
+
else
|
|
375
|
+
instance_variable_set(:"@#{name}", value)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
293
380
|
# Extract and normalize register ID with default fallback
|
|
294
381
|
#
|
|
295
382
|
# Resolution order:
|
|
@@ -116,6 +116,15 @@ module Lutaml
|
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
+
# Complete deserialization initialization after allocation.
|
|
120
|
+
# Called by allocate_for_deserialization to set up instance state,
|
|
121
|
+
# define register-specific methods, and register in the reference store.
|
|
122
|
+
def finalize_deserialization(register)
|
|
123
|
+
init_deserialization_state(register)
|
|
124
|
+
define_singleton_attribute_methods
|
|
125
|
+
register_in_reference_store
|
|
126
|
+
end
|
|
127
|
+
|
|
119
128
|
def extract_register_id(attrs, options)
|
|
120
129
|
register = attrs&.dig(:lutaml_register) || options&.dig(:register)
|
|
121
130
|
self.class.extract_register_id(register)
|
|
@@ -242,40 +251,25 @@ module Lutaml
|
|
|
242
251
|
# No-op by default
|
|
243
252
|
end
|
|
244
253
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
#
|
|
248
|
-
# for attributes that are register-specific (not defined at class level).
|
|
254
|
+
# Ensure register-specific attribute methods are defined on the class.
|
|
255
|
+
# Delegates to the class method which defines methods once per
|
|
256
|
+
# (class, register) combination instead of per-instance singleton methods.
|
|
249
257
|
def define_singleton_attribute_methods
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
# Access class-level register_records via self.class to avoid
|
|
253
|
-
# triggering ensure_imports! which would resolve types in wrong context
|
|
254
|
-
reg_records = self.class.register_records
|
|
255
|
-
return unless reg_records
|
|
256
|
-
|
|
257
|
-
reg_record = reg_records[lutaml_register]
|
|
258
|
-
return unless reg_record
|
|
259
|
-
|
|
260
|
-
reg_record_attrs = reg_record[:attributes] || {}
|
|
261
|
-
# @attributes contains default register's class-level attributes
|
|
262
|
-
default_attrs = self.class.instance_variable_get(:@attributes) || {}
|
|
258
|
+
self.class.ensure_register_methods_defined(lutaml_register)
|
|
259
|
+
end
|
|
263
260
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
261
|
+
def register_in_reference_store
|
|
262
|
+
Lutaml::Model::Store.register(self)
|
|
263
|
+
end
|
|
267
264
|
|
|
268
|
-
|
|
269
|
-
singleton_class.define_method(name) do
|
|
270
|
-
instance_variable_get(:"@#{name}")
|
|
271
|
-
end
|
|
265
|
+
private
|
|
272
266
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
267
|
+
# Resolve the Attribute object for a register-specific attribute.
|
|
268
|
+
# Used by class-level setter methods to get the correct Attribute
|
|
269
|
+
# for the instance's active register.
|
|
270
|
+
def resolve_register_attr(name)
|
|
271
|
+
self.class.register_records[lutaml_register]&.dig(:attributes, name) ||
|
|
272
|
+
self.class.attributes[name]
|
|
279
273
|
end
|
|
280
274
|
|
|
281
275
|
def initialize_attributes(attrs, options = {})
|
|
@@ -308,10 +302,6 @@ module Lutaml
|
|
|
308
302
|
end
|
|
309
303
|
end
|
|
310
304
|
|
|
311
|
-
def register_in_reference_store
|
|
312
|
-
Lutaml::Model::Store.register(self)
|
|
313
|
-
end
|
|
314
|
-
|
|
315
305
|
def resolve_reference_key(ref)
|
|
316
306
|
return nil if ref.nil?
|
|
317
307
|
|
data/lib/lutaml/model/store.rb
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "weakref"
|
|
4
|
+
|
|
3
5
|
module Lutaml
|
|
4
6
|
module Model
|
|
5
7
|
class Store
|
|
8
|
+
# Compact dead WeakRef shells after this many entries per class bucket.
|
|
9
|
+
COMPACTION_THRESHOLD = 1000
|
|
10
|
+
|
|
6
11
|
class << self
|
|
7
12
|
def instance
|
|
8
13
|
@instance ||= new
|
|
@@ -31,28 +36,92 @@ module Lutaml
|
|
|
31
36
|
|
|
32
37
|
def initialize
|
|
33
38
|
@store = ::Hash.new { |hash, key| hash[key] = [] }
|
|
39
|
+
# Lazy index: built on first resolve for a given (class, key) pair.
|
|
40
|
+
# Key: [class_name, reference_method] → { value => WeakRef(object) }
|
|
41
|
+
@index = {}
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
def register(object)
|
|
37
|
-
|
|
45
|
+
model_key = object.class.to_s
|
|
46
|
+
refs = @store[model_key]
|
|
47
|
+
refs << WeakRef.new(object)
|
|
48
|
+
|
|
49
|
+
compact_if_needed(refs)
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
update_existing_indices(object, model_key)
|
|
40
52
|
end
|
|
41
53
|
|
|
42
54
|
def resolve(model_class, reference_key, reference_value)
|
|
43
|
-
|
|
55
|
+
model_key = model_class.to_s
|
|
56
|
+
index_key = [model_key, reference_key]
|
|
57
|
+
|
|
58
|
+
# Build index lazily on first resolve for this (class, key) pair
|
|
59
|
+
unless @index.key?(index_key)
|
|
60
|
+
ensure_index(index_key, model_key,
|
|
61
|
+
reference_key)
|
|
62
|
+
end
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
64
|
+
# O(1) indexed lookup
|
|
65
|
+
entry = @index[index_key][reference_value]
|
|
66
|
+
return nil unless entry
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
entry.__getobj__ if entry.weakref_alive?
|
|
70
|
+
rescue WeakRef::RefError
|
|
71
|
+
nil
|
|
47
72
|
end
|
|
48
73
|
end
|
|
49
74
|
|
|
50
75
|
def clear
|
|
51
76
|
@store = ::Hash.new { |hash, key| hash[key] = [] }
|
|
77
|
+
@index = {}
|
|
52
78
|
end
|
|
53
79
|
|
|
54
80
|
def store
|
|
55
|
-
@store
|
|
81
|
+
@store.transform_values do |refs|
|
|
82
|
+
refs.each_with_object([]) do |ref, alive|
|
|
83
|
+
alive << ref.__getobj__ if ref.weakref_alive?
|
|
84
|
+
rescue WeakRef::RefError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# Build index for a (model_class, reference_key) pair by scanning existing instances.
|
|
93
|
+
def ensure_index(index_key, model_key, reference_key)
|
|
94
|
+
entries = @index[index_key] = {}
|
|
95
|
+
@store[model_key]&.each do |ref|
|
|
96
|
+
obj = ref.__getobj__
|
|
97
|
+
value = obj.public_send(reference_key)
|
|
98
|
+
entries[value] = WeakRef.new(obj) if value
|
|
99
|
+
rescue WeakRef::RefError
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Update indices that already exist for this model class.
|
|
105
|
+
def update_existing_indices(object, model_key)
|
|
106
|
+
@index.each do |index_key, entries|
|
|
107
|
+
next unless index_key[0] == model_key
|
|
108
|
+
|
|
109
|
+
key_method = index_key[1]
|
|
110
|
+
value = object.public_send(key_method)
|
|
111
|
+
entries[value] = WeakRef.new(object) if value
|
|
112
|
+
rescue WeakRef::RefError
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def compact_if_needed(refs)
|
|
118
|
+
return unless refs.size > COMPACTION_THRESHOLD
|
|
119
|
+
|
|
120
|
+
refs.reject! do |ref|
|
|
121
|
+
!ref.weakref_alive?
|
|
122
|
+
rescue WeakRef::RefError
|
|
123
|
+
true
|
|
124
|
+
end
|
|
56
125
|
end
|
|
57
126
|
end
|
|
58
127
|
end
|