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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf72982044ef4dcc8a413bafe9432721faac7954f6cee3ff75446b3cf9af0016
4
- data.tar.gz: 9c920d3cf43328d88cfd4be60cfd4b03dbf40b2c7f7d3e532d35518e3081014c
3
+ metadata.gz: 076bdbfb0836ad989e3fe90d5287d9662f35fb00492b6dfed0aef95db7296cdd
4
+ data.tar.gz: 1008556dd2eb02cd3aa08cffc2278f1952fab2d4c97af47a96eeea2b0975d622
5
5
  SHA512:
6
- metadata.gz: d396accc65f1dff8a826baf62764e01f2217d20bff02ca3aee70d4dba7244df7c8577ff6f1d84fd3d97f93e7f8dcc87583d07a441398c691aaa90f5955799d73
7
- data.tar.gz: 124d44d40a30dbe9ff896d7afc8eedfda5a66d6a06453d43901cbd6dffdcbab2f2f34234b9cebdd30326e0110678a3f346e35ed1d0e8553a896bf69af42b5a66
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-13 07:30:26 UTC using RuboCop version 1.86.0.
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: 4
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/xml/adapter/plan_based_builder.rb'
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: 2981
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: 5
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/xml/adapter/plan_based_builder.rb'
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: 34
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: 300
148
+ # Offense count: 302
188
149
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
189
150
  Metrics/CyclomaticComplexity:
190
151
  Enabled: false
191
152
 
192
- # Offense count: 549
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: 251
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: 94
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: 1238
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: 1468
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: 3
460
+ # Offense count: 1
512
461
  # This cop supports safe autocorrection (--autocorrect).
513
462
  Style/MultilineIfModifier:
514
463
  Exclude:
515
- - 'lib/lutaml/xml/adapter/xml_parser.rb'
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.respond_to?(:resolve_in_namespace)
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 value.respond_to?(:empty?) && value.empty?
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.respond_to?(:from_xml) &&
613
- resolved_type.method(:from_xml).owner != Lutaml::Model::Type::Value
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.respond_to?(:empty?) && value.empty?
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
@@ -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) || (value.respond_to?(:empty?) && value.empty?)
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 Mapping
4
+ include DeepDupable
5
+
4
6
  def initialize
5
7
  @mappings = []
6
8
  @listeners = {} # target => [Listener, ...]
@@ -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.respond_to?(:using_default?) && instance.using_default?(to)
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.respond_to?(:using_default?) && instance.using_default?(to)
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.respond_to?(:empty?)
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.respond_to?(:validate!)
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.respond_to?(:validate!)
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
- remove_instance_variable(ivar) if ivar.to_s.start_with?("@_imports_resolved_")
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.init_deserialization_state(register_id)
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
- private
246
-
247
- # Define attribute accessor methods on the instance's singleton class
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
- return if lutaml_register == :default
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
- reg_record_attrs.each do |name, attr|
265
- # Skip if already defined at class level (from default register)
266
- next if default_attrs.key?(name)
261
+ def register_in_reference_store
262
+ Lutaml::Model::Store.register(self)
263
+ end
267
264
 
268
- # Define getter on singleton class
269
- singleton_class.define_method(name) do
270
- instance_variable_get(:"@#{name}")
271
- end
265
+ private
272
266
 
273
- # Define setter on singleton class with type casting
274
- singleton_class.define_method(:"#{name}=") do |value|
275
- value = attr.cast_value(value, lutaml_register)
276
- instance_variable_set(:"@#{name}", value)
277
- end
278
- end
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
 
@@ -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
- model_class = object.class.to_s
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
- @store[model_class] << object
51
+ update_existing_indices(object, model_key)
40
52
  end
41
53
 
42
54
  def resolve(model_class, reference_key, reference_value)
43
- return nil unless @store[model_class.to_s]
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
- @store[model_class.to_s].find do |obj|
46
- obj.send(reference_key) == reference_value
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
- MAX_CACHE_SIZE = 256
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.respond_to?(:lutaml_register)
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.object_id, register]
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 = if value.respond_to?(:to_h)
12
- value.to_h
13
- else
14
- Hash(value)
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
- value.respond_to?(:to_h) ? value.to_h : Hash(value)
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.respond_to?(:empty?) && value.empty?
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)
@@ -115,18 +115,25 @@ module Lutaml
115
115
  end
116
116
 
117
117
  def blank?(value)
118
- value.respond_to?(:empty?) ? value.empty? : value.nil?
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
- value.respond_to?(:empty?) ? value.empty? : false
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.respond_to?(:deep_dup) ? object.deep_dup : object.dup
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.respond_to?(:validate)
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 respond_to?(:element_order) && element_order
84
+ return [] unless element_order
79
85
 
80
86
  element_order.each_with_object([]) do |element, arr|
81
87
  next if element.text?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.7"
5
+ VERSION = "0.8.8"
6
6
  end
7
7
  end
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"
@@ -34,6 +34,8 @@ module Lutaml
34
34
  def clear_xml_parse_state!
35
35
  @import_declaration_plan = nil
36
36
  @pending_plan_root_element = nil
37
+ @element_order = nil
38
+ @attribute_order = nil
37
39
  self
38
40
  end
39
41
 
@@ -282,7 +282,7 @@ module Lutaml
282
282
  namespace_uri: child.namespace_uri,
283
283
  namespace_prefix: child.namespace_prefix)
284
284
  end
285
- end
285
+ end.each(&:freeze).freeze
286
286
  end
287
287
 
288
288
  def root
@@ -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 deep_dup method is not defined and instance is deep_duplicated" do
292
- let(:attribute) { described_class.new("name", :string) }
293
-
294
- before do
295
- described_class.alias_method :orig_deep_dup, :deep_dup
296
- described_class.undef_method :deep_dup
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 deep_dup method is defined and instance is deep_duplicated" do
300
+ context "when Attribute is deep_duplicated" do
312
301
  let(:attribute) { described_class.new("name", :string) }
313
302
 
314
- it "confirms that options values are not linked of original and duplicate instances" do
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 "does not clear element_order" do
78
+ it "clears element_order to release parse buffers" do
79
79
  model = model_class.from_xml(xml_with_ns)
80
- original_order = model.element_order
80
+ expect(model.element_order).not_to be_nil
81
81
  model.clear_xml_parse_state!
82
- expect(model.element_order).to eq(original_order)
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.7
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-13 00:00:00.000000000 Z
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