lutaml-model 0.8.7 → 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/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 +15 -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
|
|
@@ -6,7 +6,8 @@ module Lutaml
|
|
|
6
6
|
@transform_cache = {}
|
|
7
7
|
|
|
8
8
|
# Maximum number of cached Transform instances before eviction.
|
|
9
|
-
|
|
9
|
+
# Covers most OOXML/ISO schemas with ~1000+ classes and multiple registers.
|
|
10
|
+
MAX_CACHE_SIZE = 2048
|
|
10
11
|
|
|
11
12
|
def self.data_to_model(context, data, format, options = {})
|
|
12
13
|
register = options[:register] || Lutaml::Model::Config.default_register
|
|
@@ -15,7 +16,7 @@ module Lutaml
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def self.model_to_data(context, model, format, options = {})
|
|
18
|
-
register = model.lutaml_register if model.
|
|
19
|
+
register = model.lutaml_register if model.is_a?(Lutaml::Model::Serialize)
|
|
19
20
|
register ||= Lutaml::Model::Config.default_register
|
|
20
21
|
transform = cached_transform(context, register)
|
|
21
22
|
transform.model_to_data(model, format, options)
|
|
@@ -23,7 +24,7 @@ module Lutaml
|
|
|
23
24
|
|
|
24
25
|
def self.cached_transform(context, register)
|
|
25
26
|
@transform_cache ||= {}
|
|
26
|
-
cache_key = [context
|
|
27
|
+
cache_key = [context, register]
|
|
27
28
|
entry = @transform_cache[cache_key]
|
|
28
29
|
return entry if entry
|
|
29
30
|
|
|
@@ -39,6 +40,16 @@ module Lutaml
|
|
|
39
40
|
@transform_cache&.size || 0
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
def self.invalidate_for(context, register = nil)
|
|
44
|
+
return unless @transform_cache
|
|
45
|
+
|
|
46
|
+
if register
|
|
47
|
+
@transform_cache.delete([context, register])
|
|
48
|
+
else
|
|
49
|
+
@transform_cache.reject! { |(ctx, _reg)| ctx == context }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
42
53
|
def self.evict_if_needed
|
|
43
54
|
# Evict oldest half of entries when cache is full
|
|
44
55
|
keys_to_remove = @transform_cache.keys.first(@transform_cache.size / 2)
|
|
@@ -8,10 +8,10 @@ module Lutaml
|
|
|
8
8
|
return super if Utils.uninitialized?(value)
|
|
9
9
|
return nil if value.nil?
|
|
10
10
|
|
|
11
|
-
hash =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
hash = case value
|
|
12
|
+
when ::Hash then value
|
|
13
|
+
when ::Array then value.to_h
|
|
14
|
+
else value.to_h
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
normalize_hash(hash)
|
|
@@ -40,7 +40,11 @@ module Lutaml
|
|
|
40
40
|
return nil if value.nil?
|
|
41
41
|
return value if value.is_a?(::Hash)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
case value
|
|
44
|
+
when ::Hash then value
|
|
45
|
+
when ::Array then value.to_h
|
|
46
|
+
else value.to_h
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# XSD type for Hash
|
|
@@ -7,7 +7,7 @@ module Lutaml
|
|
|
7
7
|
def self.cast(value, options = {})
|
|
8
8
|
return nil if value.nil?
|
|
9
9
|
return value if Utils.uninitialized?(value)
|
|
10
|
-
return nil if value.
|
|
10
|
+
return nil if value.is_a?(::String) && value.empty?
|
|
11
11
|
|
|
12
12
|
# Convert to string for validation and unwrapping
|
|
13
13
|
str_value = if value.is_a?(::Symbol)
|
data/lib/lutaml/model/utils.rb
CHANGED
|
@@ -115,18 +115,25 @@ module Lutaml
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def blank?(value)
|
|
118
|
-
|
|
118
|
+
case value
|
|
119
|
+
when ::String, ::Array, ::Hash then value.empty?
|
|
120
|
+
when ::NilClass then true
|
|
121
|
+
else false
|
|
122
|
+
end
|
|
119
123
|
end
|
|
120
124
|
|
|
121
125
|
def empty_collection?(collection)
|
|
122
126
|
return false if collection.nil?
|
|
123
|
-
return false unless [Array, Hash].include?(collection.class)
|
|
127
|
+
return false unless [::Array, ::Hash].include?(collection.class)
|
|
124
128
|
|
|
125
129
|
collection.empty?
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
def empty?(value)
|
|
129
|
-
|
|
133
|
+
case value
|
|
134
|
+
when ::String, ::Array, ::Hash then value.empty?
|
|
135
|
+
else false
|
|
136
|
+
end
|
|
130
137
|
end
|
|
131
138
|
|
|
132
139
|
def add_if_present(hash, key, value)
|
|
@@ -273,7 +280,11 @@ module Lutaml
|
|
|
273
280
|
end
|
|
274
281
|
|
|
275
282
|
def deep_dup_object(object)
|
|
276
|
-
object.
|
|
283
|
+
if object.is_a?(DeepDupable)
|
|
284
|
+
object.deep_dup
|
|
285
|
+
else
|
|
286
|
+
object.dup
|
|
287
|
+
end
|
|
277
288
|
end
|
|
278
289
|
|
|
279
290
|
def camelize_part(part)
|
|
@@ -10,7 +10,7 @@ module Lutaml
|
|
|
10
10
|
value = public_send(:"#{name}")
|
|
11
11
|
|
|
12
12
|
begin
|
|
13
|
-
if value.
|
|
13
|
+
if value.is_a?(Lutaml::Model::Serialize)
|
|
14
14
|
sub_errors = value.validate
|
|
15
15
|
errors.concat(sub_errors) if sub_errors.is_a?(Array)
|
|
16
16
|
else
|
|
@@ -74,8 +74,14 @@ module Lutaml
|
|
|
74
74
|
nil
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Default: no element order. XML overrides via InstanceMethods prepend
|
|
78
|
+
# with attr_accessor :element_order.
|
|
79
|
+
def element_order
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
77
83
|
def order_names
|
|
78
|
-
return [] unless
|
|
84
|
+
return [] unless element_order
|
|
79
85
|
|
|
80
86
|
element_order.each_with_object([]) do |element, arr|
|
|
81
87
|
next if element.text?
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Lutaml
|
|
|
33
33
|
autoload :AdapterResolver, "#{__dir__}/model/adapter_resolver"
|
|
34
34
|
autoload :AdapterScope, "#{__dir__}/model/adapter_scope"
|
|
35
35
|
autoload :Utils, "#{__dir__}/model/utils"
|
|
36
|
+
autoload :DeepDupable, "#{__dir__}/model/deep_dupable"
|
|
36
37
|
autoload :Serializable, "#{__dir__}/model/serializable"
|
|
37
38
|
autoload :Error, "#{__dir__}/model/error"
|
|
38
39
|
autoload :Constants, "#{__dir__}/model/constants"
|
|
@@ -288,30 +288,19 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
288
288
|
describe "#deep_dup" do
|
|
289
289
|
let(:duplicate_attribute) { Lutaml::Model::Utils.deep_dup(attribute) }
|
|
290
290
|
|
|
291
|
-
context "when
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
after do
|
|
300
|
-
described_class.alias_method :deep_dup, :orig_deep_dup
|
|
301
|
-
attribute.options.delete(:foo)
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
it "confirms that options values are linked of original and duplicate instances" do
|
|
305
|
-
duplicate_attribute
|
|
306
|
-
attribute.options[:foo] = "bar"
|
|
307
|
-
expect(duplicate_attribute.options).to include(:foo)
|
|
291
|
+
context "when object does not include DeepDupable" do
|
|
292
|
+
it "falls back to dup for plain objects" do
|
|
293
|
+
plain = Struct.new(:value).new("hello")
|
|
294
|
+
result = Lutaml::Model::Utils.deep_dup(plain)
|
|
295
|
+
expect(result.value).to eq("hello")
|
|
296
|
+
expect(result).not_to equal(plain)
|
|
308
297
|
end
|
|
309
298
|
end
|
|
310
299
|
|
|
311
|
-
context "when
|
|
300
|
+
context "when Attribute is deep_duplicated" do
|
|
312
301
|
let(:attribute) { described_class.new("name", :string) }
|
|
313
302
|
|
|
314
|
-
it "
|
|
303
|
+
it "creates independent copies of options" do
|
|
315
304
|
duplicate_attribute
|
|
316
305
|
attribute.options[:foo] = "bar"
|
|
317
306
|
expect(duplicate_attribute.options).not_to include(:foo)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Register-specific attribute methods" do
|
|
6
|
+
let(:register) do
|
|
7
|
+
Lutaml::Model::Register.new(:register_methods_test,
|
|
8
|
+
fallback: [:default])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:base_model) do
|
|
12
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
13
|
+
attribute :name, :string
|
|
14
|
+
|
|
15
|
+
xml do
|
|
16
|
+
root "Base"
|
|
17
|
+
map_element "Name", to: :name
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:extension_model) do
|
|
23
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
24
|
+
attribute :version, :string
|
|
25
|
+
attribute :priority, :integer
|
|
26
|
+
|
|
27
|
+
xml do
|
|
28
|
+
root "Extension"
|
|
29
|
+
map_element "Version", to: :version
|
|
30
|
+
map_element "Priority", to: :priority
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
before do
|
|
36
|
+
Lutaml::Model::GlobalContext.reset!
|
|
37
|
+
Lutaml::Model::GlobalRegister.register(register)
|
|
38
|
+
register.register_model(extension_model, id: :extension_model)
|
|
39
|
+
base_model.import_model_attributes(extension_model, register.id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "class-level method definition" do
|
|
43
|
+
it "does not create singleton class methods for register-specific attributes" do
|
|
44
|
+
instance = base_model.new({ name: "test",
|
|
45
|
+
version: "1.0" },
|
|
46
|
+
register: register.id)
|
|
47
|
+
|
|
48
|
+
expect(instance.version).to eq("1.0")
|
|
49
|
+
|
|
50
|
+
# Method should be on the class, NOT directly on the singleton class
|
|
51
|
+
expect(instance.singleton_class.instance_methods(false)).not_to include(:version)
|
|
52
|
+
expect(instance.singleton_class.instance_methods(false)).not_to include(:version=)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "defines methods on the class accessible to all instances" do
|
|
56
|
+
instance1 = base_model.new({ name: "a" }, register: register.id)
|
|
57
|
+
instance2 = base_model.new({ name: "b" }, register: register.id)
|
|
58
|
+
|
|
59
|
+
instance1.version = "1.0"
|
|
60
|
+
instance2.version = "2.0"
|
|
61
|
+
|
|
62
|
+
expect(instance1.version).to eq("1.0")
|
|
63
|
+
expect(instance2.version).to eq("2.0")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "guards against re-definition with @_register_methods_defined" do
|
|
67
|
+
base_model.new({ name: "a" }, register: register.id)
|
|
68
|
+
|
|
69
|
+
guard = base_model.instance_variable_get(:@_register_methods_defined)
|
|
70
|
+
expect(guard).to include(register.id => true)
|
|
71
|
+
|
|
72
|
+
# Second instance creation should not re-trigger method definition
|
|
73
|
+
base_model.new({ name: "b" }, register: register.id)
|
|
74
|
+
expect(guard[register.id]).to be(true)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "type casting in setter" do
|
|
79
|
+
it "casts integer values correctly" do
|
|
80
|
+
instance = base_model.new({ name: "test" }, register: register.id)
|
|
81
|
+
|
|
82
|
+
instance.priority = "5"
|
|
83
|
+
expect(instance.priority).to eq(5)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "casts string values correctly" do
|
|
87
|
+
instance = base_model.new({ name: "test" }, register: register.id)
|
|
88
|
+
|
|
89
|
+
instance.version = 42
|
|
90
|
+
expect(instance.version).to eq("42")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "value_set_for tracking" do
|
|
95
|
+
it "marks register-specific attributes as explicitly set" do
|
|
96
|
+
instance = base_model.new({ name: "test" }, register: register.id)
|
|
97
|
+
|
|
98
|
+
# After initialization, version was set via public_send in initialize_attributes
|
|
99
|
+
expect(instance.using_default?(:version)).to be(false)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "uses default when register-specific attribute is not provided" do
|
|
103
|
+
instance = base_model.allocate_for_deserialization(register.id)
|
|
104
|
+
|
|
105
|
+
# Not yet set — using default
|
|
106
|
+
expect(instance.using_default?(:version)).to be(true)
|
|
107
|
+
|
|
108
|
+
instance.version = "1.0"
|
|
109
|
+
expect(instance.using_default?(:version)).to be(false)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "default register instances" do
|
|
114
|
+
it "returns early without defining methods for :default register" do
|
|
115
|
+
base_model.ensure_register_methods_defined(:default)
|
|
116
|
+
|
|
117
|
+
guard = base_model.instance_variable_get(:@_register_methods_defined)
|
|
118
|
+
expect(guard).to be_nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "default-register instances still work for class-level attributes" do
|
|
122
|
+
instance = base_model.new(name: "test")
|
|
123
|
+
|
|
124
|
+
expect(instance.name).to eq("test")
|
|
125
|
+
instance.name = "changed"
|
|
126
|
+
expect(instance.name).to eq("changed")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "deserialization path" do
|
|
131
|
+
it "defines methods via finalize_deserialization" do
|
|
132
|
+
instance = base_model.allocate_for_deserialization(register.id)
|
|
133
|
+
instance.version = "3.0"
|
|
134
|
+
|
|
135
|
+
expect(instance.version).to eq("3.0")
|
|
136
|
+
expect(instance.singleton_class.instance_methods(false)).not_to include(:version)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe "clear_cache resets the guard" do
|
|
141
|
+
it "clears @_register_methods_defined on clear_cache" do
|
|
142
|
+
base_model.new({ name: "a" }, register: register.id)
|
|
143
|
+
expect(base_model.instance_variable_get(:@_register_methods_defined)).not_to be_nil
|
|
144
|
+
|
|
145
|
+
base_model.clear_cache
|
|
146
|
+
expect(base_model.instance_variable_get(:@_register_methods_defined)).to be_nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Model::Store do
|
|
6
|
+
let(:model_class) do
|
|
7
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
8
|
+
attribute :id, :string
|
|
9
|
+
attribute :name, :string
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
before { described_class.clear }
|
|
14
|
+
after { described_class.clear }
|
|
15
|
+
|
|
16
|
+
describe "#register and #resolve" do
|
|
17
|
+
it "resolves a registered object by reference key" do
|
|
18
|
+
obj = model_class.new(id: "abc", name: "test")
|
|
19
|
+
result = described_class.resolve(model_class, :id, "abc")
|
|
20
|
+
expect(result).to eq(obj)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "returns nil for non-existent reference" do
|
|
24
|
+
model_class.new(id: "abc")
|
|
25
|
+
result = described_class.resolve(model_class, :id, "nonexistent")
|
|
26
|
+
expect(result).to be_nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "returns nil for non-existent class" do
|
|
30
|
+
other_class = Class.new(Lutaml::Model::Serializable) do
|
|
31
|
+
attribute :id, :string
|
|
32
|
+
end
|
|
33
|
+
result = described_class.resolve(other_class, :id, "anything")
|
|
34
|
+
expect(result).to be_nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "resolves by name attribute" do
|
|
38
|
+
obj = model_class.new(id: "abc", name: "myobj")
|
|
39
|
+
result = described_class.resolve(model_class, :name, "myobj")
|
|
40
|
+
expect(result).to eq(obj)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "indexed lookup" do
|
|
45
|
+
it "builds index lazily on first resolve" do
|
|
46
|
+
5.times { |i| model_class.new(id: "obj-#{i}") }
|
|
47
|
+
|
|
48
|
+
result = described_class.resolve(model_class, :id, "obj-3")
|
|
49
|
+
expect(result.id).to eq("obj-3")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "updates index when new objects are registered after first resolve" do
|
|
53
|
+
model_class.new(id: "first")
|
|
54
|
+
described_class.resolve(model_class, :id, "first") # triggers index build
|
|
55
|
+
|
|
56
|
+
model_class.new(id: "second")
|
|
57
|
+
result = described_class.resolve(model_class, :id, "second")
|
|
58
|
+
expect(result.id).to eq("second")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "reuses index across multiple resolves for same key" do
|
|
62
|
+
10.times { |i| model_class.new(id: "obj-#{i}") }
|
|
63
|
+
|
|
64
|
+
# First resolve builds the index
|
|
65
|
+
described_class.resolve(model_class, :id, "obj-5")
|
|
66
|
+
# Second resolve should use the same index
|
|
67
|
+
result = described_class.resolve(model_class, :id, "obj-7")
|
|
68
|
+
expect(result.id).to eq("obj-7")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "WeakRef behavior" do
|
|
73
|
+
it "uses WeakRef for storage (objects can be collected when unreferenced)" do
|
|
74
|
+
obj = model_class.new(id: "alive")
|
|
75
|
+
result = described_class.resolve(model_class, :id, "alive")
|
|
76
|
+
expect(result).to eq(obj)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "resolves objects with strong references held externally" do
|
|
80
|
+
obj = model_class.new(id: "alive")
|
|
81
|
+
GC.start
|
|
82
|
+
result = described_class.resolve(model_class, :id, "alive")
|
|
83
|
+
expect(result).to eq(obj)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "#store" do
|
|
88
|
+
it "returns only live objects" do
|
|
89
|
+
obj1 = model_class.new(id: "keep")
|
|
90
|
+
_obj2 = model_class.new(id: "drop")
|
|
91
|
+
_obj2 = nil
|
|
92
|
+
GC.start
|
|
93
|
+
|
|
94
|
+
entries = described_class.store[model_class.to_s]
|
|
95
|
+
# obj1 should be alive; obj2 may or may not be GC'd depending on timing
|
|
96
|
+
expect(entries).to include(obj1)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "#clear" do
|
|
101
|
+
it "removes all registered objects" do
|
|
102
|
+
model_class.new(id: "a")
|
|
103
|
+
model_class.new(id: "b")
|
|
104
|
+
described_class.clear
|
|
105
|
+
|
|
106
|
+
expect(described_class.resolve(model_class, :id, "a")).to be_nil
|
|
107
|
+
expect(described_class.resolve(model_class, :id, "b")).to be_nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "clears the index" do
|
|
111
|
+
model_class.new(id: "indexed")
|
|
112
|
+
described_class.resolve(model_class, :id, "indexed") # build index
|
|
113
|
+
described_class.clear
|
|
114
|
+
|
|
115
|
+
model_class.new(id: "new-one")
|
|
116
|
+
result = described_class.resolve(model_class, :id, "new-one")
|
|
117
|
+
expect(result.id).to eq("new-one")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -59,4 +59,46 @@ RSpec.describe "Transform caching" do
|
|
|
59
59
|
expect(Lutaml::Model::Transform.cache_size).to be <= 4
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
describe ".invalidate_for" do
|
|
64
|
+
it "invalidates cache for a specific context" do
|
|
65
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
66
|
+
attribute :name, :string
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
t1 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
70
|
+
Lutaml::Model::Transform.invalidate_for(klass)
|
|
71
|
+
t2 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
72
|
+
expect(t1).not_to equal(t2)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "invalidates cache for a specific context and register" do
|
|
76
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
77
|
+
attribute :name, :string
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
t1_default = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
81
|
+
t1_other = Lutaml::Model::Transform.cached_transform(klass, :other)
|
|
82
|
+
|
|
83
|
+
Lutaml::Model::Transform.invalidate_for(klass, :default)
|
|
84
|
+
|
|
85
|
+
t2_default = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
86
|
+
t2_other = Lutaml::Model::Transform.cached_transform(klass, :other)
|
|
87
|
+
|
|
88
|
+
expect(t1_default).not_to equal(t2_default)
|
|
89
|
+
expect(t1_other).to equal(t2_other)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "uses class identity as key, not object_id" do
|
|
93
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
94
|
+
attribute :name, :string
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
t1 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
98
|
+
# Force GC — object_id reuse should not cause stale hits
|
|
99
|
+
GC.start
|
|
100
|
+
t2 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
101
|
+
expect(t1).to equal(t2)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
62
104
|
end
|
|
@@ -75,11 +75,18 @@ RSpec.describe "#clear_xml_parse_state!" do
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
describe "user-facing attributes" do
|
|
78
|
-
it "
|
|
78
|
+
it "clears element_order to release parse buffers" do
|
|
79
79
|
model = model_class.from_xml(xml_with_ns)
|
|
80
|
-
|
|
80
|
+
expect(model.element_order).not_to be_nil
|
|
81
81
|
model.clear_xml_parse_state!
|
|
82
|
-
expect(model.element_order).to
|
|
82
|
+
expect(model.element_order).to be_nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "clears attribute_order to release parse buffers" do
|
|
86
|
+
model = model_class.from_xml(xml_with_ns)
|
|
87
|
+
expect(model.attribute_order).not_to be_nil
|
|
88
|
+
model.clear_xml_parse_state!
|
|
89
|
+
expect(model.attribute_order).to be_nil
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
it "does not clear encoding" do
|
|
@@ -32,6 +32,21 @@ RSpec.describe "Schema mapping integration" do
|
|
|
32
32
|
expect(parsed.target_namespace_prefix).to eq("t")
|
|
33
33
|
expect(parsed.element.first.target_prefix).to eq("t")
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
it "captures nil prefix when target namespace is the default (unprefixed)" do
|
|
37
|
+
xsd_default_ns = <<~XSD
|
|
38
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
39
|
+
xmlns="http://example.com/default"
|
|
40
|
+
targetNamespace="http://example.com/default">
|
|
41
|
+
<xs:element name="Root" type="xs:string"/>
|
|
42
|
+
</xs:schema>
|
|
43
|
+
XSD
|
|
44
|
+
|
|
45
|
+
parsed = Lutaml::Xml::Schema::Xsd.parse(xsd_default_ns)
|
|
46
|
+
|
|
47
|
+
expect(parsed.target_namespace).to eq("http://example.com/default")
|
|
48
|
+
expect(parsed.target_namespace_prefix).to be_nil
|
|
49
|
+
end
|
|
35
50
|
end
|
|
36
51
|
|
|
37
52
|
describe "parsing with exact string mappings" do
|
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.8
|
|
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-05-
|
|
11
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -404,6 +404,7 @@ files:
|
|
|
404
404
|
- lib/lutaml/model/consolidation_rule/pattern_element_rule.rb
|
|
405
405
|
- lib/lutaml/model/constants.rb
|
|
406
406
|
- lib/lutaml/model/context_registry.rb
|
|
407
|
+
- lib/lutaml/model/deep_dupable.rb
|
|
407
408
|
- lib/lutaml/model/error.rb
|
|
408
409
|
- lib/lutaml/model/error/choice_lower_bound_error.rb
|
|
409
410
|
- lib/lutaml/model/error/choice_upper_bound_error.rb
|
|
@@ -1617,6 +1618,7 @@ files:
|
|
|
1617
1618
|
- spec/lutaml/model/polymorphic_spec.rb
|
|
1618
1619
|
- spec/lutaml/model/processing_instruction_spec.rb
|
|
1619
1620
|
- spec/lutaml/model/register_key_value_spec.rb
|
|
1621
|
+
- spec/lutaml/model/register_methods_spec.rb
|
|
1620
1622
|
- spec/lutaml/model/register_spec.rb
|
|
1621
1623
|
- spec/lutaml/model/render_empty_spec.rb
|
|
1622
1624
|
- spec/lutaml/model/render_nil_spec.rb
|
|
@@ -1637,6 +1639,7 @@ files:
|
|
|
1637
1639
|
- spec/lutaml/model/services/default_value_resolver_spec.rb
|
|
1638
1640
|
- spec/lutaml/model/services/transformer_spec.rb
|
|
1639
1641
|
- spec/lutaml/model/simple_model_spec.rb
|
|
1642
|
+
- spec/lutaml/model/store_spec.rb
|
|
1640
1643
|
- spec/lutaml/model/toml_adapter_spec.rb
|
|
1641
1644
|
- spec/lutaml/model/toml_spec.rb
|
|
1642
1645
|
- spec/lutaml/model/transform_cache_spec.rb
|