lutaml-model 0.8.129 → 0.8.131

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/docs/key_value_data_model.adoc +9 -9
  3. data/lib/lutaml/json/adapter/multi_json_adapter.rb +1 -7
  4. data/lib/lutaml/json/adapter/oj_adapter.rb +1 -7
  5. data/lib/lutaml/json/adapter/standard_adapter.rb +1 -7
  6. data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +1 -7
  7. data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +1 -7
  8. data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +1 -7
  9. data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +1 -7
  10. data/lib/lutaml/key_value/adapter/toml/toml_rb_adapter.rb +1 -7
  11. data/lib/lutaml/key_value/adapter/toml/tomlib_adapter.rb +1 -7
  12. data/lib/lutaml/key_value/adapter/yaml/standard_adapter.rb +1 -7
  13. data/lib/lutaml/key_value/data_model/element.rb +31 -32
  14. data/lib/lutaml/key_value/mapping_rule.rb +4 -4
  15. data/lib/lutaml/key_value/transform.rb +6 -5
  16. data/lib/lutaml/key_value/transformation.rb +21 -18
  17. data/lib/lutaml/model/attribute.rb +23 -1
  18. data/lib/lutaml/model/collection.rb +1 -5
  19. data/lib/lutaml/model/mapping/mapping_rule.rb +21 -2
  20. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +9 -4
  21. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +15 -8
  22. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +2 -2
  23. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +10 -0
  24. data/lib/lutaml/model/serialize/attribute_definition.rb +133 -2
  25. data/lib/lutaml/model/serialize/initialization.rb +12 -0
  26. data/lib/lutaml/model/serialize.rb +30 -10
  27. data/lib/lutaml/model/version.rb +1 -1
  28. data/lib/lutaml/toml/adapter/toml_rb_adapter.rb +1 -7
  29. data/lib/lutaml/toml/adapter/tomlib_adapter.rb +1 -7
  30. data/lib/lutaml/xml/builder/base.rb +3 -1
  31. data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -2
  32. data/lib/lutaml/yaml/adapter/standard_adapter.rb +1 -7
  33. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +1 -7
  34. data/lib/lutaml/yamlld/adapter.rb +1 -6
  35. data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +3 -3
  36. data/spec/lutaml/key_value/group_instantiate_spec.rb +8 -0
  37. data/spec/lutaml/key_value/transformation_spec.rb +50 -50
  38. data/spec/lutaml/model/collection_spec.rb +43 -0
  39. data/spec/lutaml/model/schema/xml_compiler_runtime_spec.rb +81 -0
  40. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6903f41ec929cb64ce2450b6979f832e0a832f8afbbe82a134af3db157d2fdd8
4
- data.tar.gz: 3d2f61efc2e609ade7d440994d318c1b190255c243f33ec96538de2a4cda5774
3
+ metadata.gz: 411fee08073d8c3578b1071024486fa23ed6f1c3b93e918f7c47e175b9a95f90
4
+ data.tar.gz: 80e702fd74cda828374ffc7b5367311a2b49ce5ba2d5580e8038e13b6b9269ca
5
5
  SHA512:
6
- metadata.gz: 7f82b33a1e8ac555aa6c756007da45a99dc3e506fd58c1f780446c112397314b3a76b0978566dc3c3279651e76701f0537b87dd1c700accaeb77f789497e20dc
7
- data.tar.gz: 1100e41da62097636496a5fe31d1ac6a77f55ac491a8a3313eb96dfa4ceda55c52ba92166eda902f07bceae1ace17fd2224c2190c9b9792c5ca76ff4ce29293f
6
+ metadata.gz: d48928bc46206f2067989a4637ace150fcf71db4df6c18e1df93de5c621c088db0490bf85d860ec6861ad08d208218553a5d2ae7806ea4b5e88067b0032d51ed
7
+ data.tar.gz: 8d0127e8139146f8d1df26a488904d7e48202e4c5b75a49f67a895f87d372507f9217253590496c44513677653bd8516f009d59f7889a41832c2e12333987585
@@ -697,21 +697,21 @@ end
697
697
 
698
698
  === Detection Logic
699
699
 
700
- All adapters follow this pattern:
700
+ The transformation emits the output hash directly (hash-direct
701
+ emission), and every adapter serializes the hash it is given:
701
702
 
702
703
  [source,ruby]
703
704
  ----
704
- def to_json(root, options = {})
705
- if root.is_a?(Lutaml::KeyValue::DataModel::Element)
706
- build_from_key_value_element(root)
707
- elsif root.is_a?(Hash)
708
- build_from_hash(root) # Legacy path
709
- else
710
- raise TypeError, "Expected KeyValueElement or Hash, got #{root.class}"
711
- end
705
+ def to_json(hash, options = {})
706
+ JSON.generate(hash)
712
707
  end
713
708
  ----
714
709
 
710
+ `Lutaml::KeyValue::DataModel::Element` remains the content model for
711
+ rule compilation and for custom `to:` handlers (whose scratch document
712
+ element merges its payload into the output hash), but the Element tree
713
+ is no longer passed across the adapter boundary.
714
+
715
715
  === Building from KeyValueElement
716
716
 
717
717
  ==== JSON Example
@@ -22,13 +22,7 @@ module Lutaml
22
22
  def to_json(*args)
23
23
  require "multi_json"
24
24
  # Handle KeyValueElement input (new symmetric architecture)
25
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
26
- # Unwrap __root__ wrapper to get actual content
27
- @attributes.to_hash["__root__"]
28
- else
29
- # Legacy Hash input (backward compatibility)
30
- @attributes
31
- end
25
+ attributes_to_serialize = @attributes
32
26
 
33
27
  # LutaML's own threading keys must not reach the backend; the
34
28
  # rest (e.g. :pretty) are meaningful to multi_json. The json
@@ -27,13 +27,7 @@ module Lutaml
27
27
  def to_json(*args)
28
28
  require "oj"
29
29
  # Handle KeyValueElement input (new symmetric architecture)
30
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
31
- # Unwrap __root__ wrapper to get actual content
32
- @attributes.to_hash["__root__"]
33
- else
34
- # Legacy Hash input (backward compatibility)
35
- @attributes
36
- end
30
+ attributes_to_serialize = @attributes
37
31
 
38
32
  Oj.dump(attributes_to_serialize, *args)
39
33
  # rubocop:enable Style/ArgumentsForwarding
@@ -22,13 +22,7 @@ module Lutaml
22
22
 
23
23
  def to_json(*args)
24
24
  # Handle KeyValueElement input (new symmetric architecture)
25
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
26
- # Unwrap __root__ wrapper to get actual content
27
- @attributes.to_hash["__root__"]
28
- else
29
- # Legacy Hash input (backward compatibility)
30
- @attributes
31
- end
25
+ attributes_to_serialize = @attributes
32
26
 
33
27
  unless GeneratorOptions.lutaml_options?(args.first)
34
28
  return JSON.generate(attributes_to_serialize, args.first)
@@ -26,13 +26,7 @@ module Lutaml
26
26
  def to_json(*args)
27
27
  require "multi_json"
28
28
  # Handle KeyValueElement input (new symmetric architecture)
29
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
30
- # Unwrap __root__ wrapper to get actual content
31
- @attributes.to_hash["__root__"]
32
- else
33
- # Legacy Hash input (backward compatibility)
34
- @attributes
35
- end
29
+ attributes_to_serialize = @attributes
36
30
 
37
31
  # LutaML's own options must not reach the backend: the json
38
32
  # gem 3.x and Oj both raise on unknown generator options
@@ -31,13 +31,7 @@ module Lutaml
31
31
  def to_json(*args)
32
32
  require "oj"
33
33
  # Handle KeyValueElement input (new symmetric architecture)
34
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
35
- # Unwrap __root__ wrapper to get actual content
36
- @attributes.to_hash["__root__"]
37
- else
38
- # Legacy Hash input (backward compatibility)
39
- @attributes
40
- end
34
+ attributes_to_serialize = @attributes
41
35
 
42
36
  Oj.dump(attributes_to_serialize, *args)
43
37
  # rubocop:enable Style/ArgumentsForwarding
@@ -25,13 +25,7 @@ module Lutaml
25
25
 
26
26
  def to_json(*args)
27
27
  # Handle KeyValueElement input (new symmetric architecture)
28
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
29
- # Unwrap __root__ wrapper to get actual content
30
- @attributes.to_hash["__root__"]
31
- else
32
- # Legacy Hash input (backward compatibility)
33
- @attributes
34
- end
28
+ attributes_to_serialize = @attributes
35
29
 
36
30
  unless Lutaml::Json::GeneratorOptions.lutaml_options?(args.first)
37
31
  return JSON.generate(attributes_to_serialize, args.first)
@@ -25,13 +25,7 @@ module Lutaml
25
25
  def to_toml(*)
26
26
  require "teptris"
27
27
  # Handle KeyValueElement input (new symmetric architecture)
28
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
29
- # Unwrap __root__ wrapper to get actual content
30
- @attributes.to_hash["__root__"]
31
- else
32
- # Legacy Hash input (backward compatibility)
33
- @attributes
34
- end
28
+ attributes_to_serialize = @attributes
35
29
 
36
30
  Teptris::TOML.dump(attributes_to_serialize)
37
31
  end
@@ -20,13 +20,7 @@ module Lutaml
20
20
  def to_toml(*)
21
21
  require "toml-rb"
22
22
  # Handle KeyValueElement input (new symmetric architecture)
23
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
24
- # Unwrap __root__ wrapper to get actual content
25
- @attributes.to_hash["__root__"]
26
- else
27
- # Legacy Hash input (backward compatibility)
28
- @attributes
29
- end
23
+ attributes_to_serialize = @attributes
30
24
 
31
25
  TomlRB.dump(attributes_to_serialize)
32
26
  rescue LoadError
@@ -25,13 +25,7 @@ module Lutaml
25
25
  def to_toml(*)
26
26
  require "tomlib"
27
27
  # Handle KeyValueElement input (new symmetric architecture)
28
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
29
- # Unwrap __root__ wrapper to get actual content
30
- @attributes.to_hash["__root__"]
31
- else
32
- # Legacy Hash input (backward compatibility)
33
- @attributes
34
- end
28
+ attributes_to_serialize = @attributes
35
29
 
36
30
  Tomlib.dump(attributes_to_serialize)
37
31
  # Tomlib::Generator.new(to_h).toml_str
@@ -24,13 +24,7 @@ module Lutaml
24
24
 
25
25
  def to_yaml(options = {})
26
26
  # Handle KeyValueElement input (new symmetric architecture)
27
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
28
- # Unwrap __root__ wrapper to get actual content
29
- @attributes.to_hash["__root__"]
30
- else
31
- # Legacy Hash input (backward compatibility)
32
- @attributes
33
- end
27
+ attributes_to_serialize = @attributes
34
28
 
35
29
  YAML.dump(attributes_to_serialize, options)
36
30
  end
@@ -85,6 +85,21 @@ module Lutaml
85
85
  has_value? && !has_children?
86
86
  end
87
87
 
88
+ # The content this element carries, independent of its own key:
89
+ # the child-merged structure when children exist, otherwise the
90
+ # direct value (nil when neither). This is the half of to_hash
91
+ # that survives the {key => ...} envelope, so callers that
92
+ # assemble documents themselves never round-trip through a key.
93
+ #
94
+ # @return [Object, nil]
95
+ def payload
96
+ if has_children?
97
+ children_to_value
98
+ elsif has_value?
99
+ @value
100
+ end
101
+ end
102
+
88
103
  # Convert to Hash representation
89
104
  #
90
105
  # This is the primary method for adapter rendering.
@@ -93,19 +108,10 @@ module Lutaml
93
108
  # 2. If has children that are all Elements: {key => merged_hash}
94
109
  # 3. If has children that are mixed/primitives: {key => array}
95
110
  # 4. If has both value and children: children take precedence
96
- # 5. Special case: __root__ with no children returns empty hash (for omitted attributes)
97
111
  #
98
112
  # @return [Hash]
99
113
  def to_hash
100
- if has_children?
101
- { @key => children_to_value }
102
- elsif has_value?
103
- { @key => @value }
104
- else
105
- # Special case: __root__ element should return empty hash when all attributes omitted
106
- # This allows hash["__root__"].keys to work without raising NoMethodError
107
- { @key => (@key == "__root__" ? {} : nil) }
108
- end
114
+ { @key => payload }
109
115
  end
110
116
 
111
117
  # String representation for debugging
@@ -123,43 +129,36 @@ module Lutaml
123
129
 
124
130
  # Hash-like access for backward compatibility
125
131
  #
126
- # Allows Element to be used like a hash in tests and existing code.
127
- # Converts to hash first, then accesses the key.
132
+ # A child element's payload answers by the child's key; a leaf
133
+ # answers by its own key ({"name" => "John"}["name"]). This is
134
+ # what lets custom `to:` handlers build nested structures in
135
+ # place: `doc["locality"] = {}; doc["locality"]["type"] = v` —
136
+ # the read returns the child's value hash and the write mutates
137
+ # it in place.
128
138
  #
129
139
  # @param key [String, Symbol] Key to access
130
140
  # @return [Object] Value for the key
131
141
  def [](key)
132
- hash = to_hash
133
- # If this is the root element, access the inner hash
134
- if @key == "__root__" && hash.key?("__root__")
135
- inner = hash["__root__"]
136
- inner.is_a?(Hash) ? inner[key.to_s] : nil
137
- else
138
- hash[key.to_s]
139
- end
142
+ key = key.to_s
143
+ child = @children.find { |c| c.is_a?(Element) && c.key == key }
144
+ child ? child.payload : to_hash[key]
140
145
  end
141
146
 
142
147
  # Hash-like assignment for backward compatibility
143
148
  #
144
149
  # Allows Element to be used like a hash in tests and existing code.
145
- # Creates a new child Element for the key-value pair.
150
+ # Replaces any existing child element of the same key, then adds
151
+ # a new child Element for the key-value pair.
146
152
  #
147
153
  # @param key [String, Symbol] Key to set
148
154
  # @param value [Object] Value to set
149
155
  # @return [Object] The assigned value
150
156
  def []=(key, value)
151
- # For root element, add child with the key-value pair
152
- if @key == "__root__"
153
- # Remove existing child with same key if any
154
- @children.reject! do |child|
155
- child.is_a?(Element) && child.key == key.to_s
156
- end
157
- # Add new child
158
- else
159
- # For non-root elements, convert to hash and merge
160
- # This shouldn't normally happen in the transformation flow
157
+ key = key.to_s
158
+ @children.reject! do |child|
159
+ child.is_a?(Element) && child.key == key
161
160
  end
162
- @children << Element.new(key.to_s, value)
161
+ @children << Element.new(key, value)
163
162
  value
164
163
  end
165
164
 
@@ -57,9 +57,8 @@ module Lutaml
57
57
  end
58
58
 
59
59
  def hash_mappings
60
- return @root_mappings if @root_mappings
61
-
62
- @child_mappings
60
+ @hash_mappings = @root_mappings || @child_mappings if @hash_mappings.nil?
61
+ @hash_mappings
63
62
  end
64
63
 
65
64
  def deep_dup
@@ -81,7 +80,8 @@ module Lutaml
81
80
  end
82
81
 
83
82
  def root_mapping?
84
- name == "root_mapping"
83
+ @root_mapping = name == "root_mapping" if @root_mapping.nil?
84
+ @root_mapping
85
85
  end
86
86
 
87
87
  # The writer symbol for this rule's target, interned once per
@@ -28,7 +28,9 @@ module Lutaml
28
28
  end
29
29
 
30
30
  if model_class.include?(Lutaml::Model::Serialize)
31
- instance = model_class.new(lutaml_register: child_register)
31
+ # #1003: constructor-equivalent state via the compiled walk
32
+ # seed — no per-attribute determine_value walk per instance.
33
+ instance = model_class.allocate_for_walk(child_register)
32
34
  else
33
35
  instance = model_class.new
34
36
  register_accessor_methods_for(instance, child_register)
@@ -104,8 +106,7 @@ module Lutaml
104
106
  # transformation_for returns nil for cyclic dependencies or :building sentinel
105
107
  # Fall back to legacy approach in these cases
106
108
  if transformation.is_a?(Lutaml::KeyValue::Transformation)
107
- # transform emits the output hash directly (TODO.perf 19) —
108
- # the __root__ Element tree and its to_hash flatten are gone.
109
+ # transform emits the output hash directly (TODO.perf 19)
109
110
  return transformation.transform(instance, options)
110
111
  end
111
112
  end
@@ -430,7 +431,7 @@ child_hash, format, { mappings: type.child_rules })
430
431
  children << child
431
432
  end
432
433
  end
433
- instance = model_class.new(lutaml_register: register)
434
+ instance = model_class.allocate_for_walk(register)
434
435
  setters.each { |name, value| instance.public_send(name, value) }
435
436
  absent&.each do |rule|
436
437
  process_mapping_rule(doc, instance, format, rule, options, nil)
@@ -477,7 +478,7 @@ options)
477
478
  to_method = rule.custom_methods[:to]
478
479
  # lutaml-model#550: custom methods may declare a third context
479
480
  # parameter to receive the options passed to `to_*`.
480
- if instance.method(to_method).parameters.size >= 3
481
+ if Lutaml::Model::MappingRule.custom_method_arity(instance.class, to_method).to_i >= 3
481
482
  return instance.public_send(to_method, instance, hash,
482
483
  options[:context])
483
484
  end
@@ -221,9 +221,8 @@ register = self.register)
221
221
  def transform(model_instance, options = {})
222
222
  # For key-value formats, we typically don't have a named root
223
223
  # Instead, we create an anonymous root that holds all attributes
224
- # Hash-direct emission (TODO.perf 19): the __root__ Element
225
- # tree was write-only in production — every consumer flattened
226
- # it straight back to a hash. The output hash is built directly.
224
+ # Hash-direct emission (TODO.perf 19): the output hash is built
225
+ # directly; the intermediate Element tree and its flatten are gone.
227
226
  root = {}
228
227
 
229
228
  # Apply each compiled rule (with filtering support). A
@@ -343,9 +342,10 @@ register = self.register)
343
342
  if rule.has_custom_methods? && rule.custom_methods[:to]
344
343
  to_method = rule.custom_methods[:to]
345
344
  # Custom methods receive an Element "doc" and populate it
346
- # (public contract, TODO.perf 19 keeps it): a scratch element
347
- # is merged into the output hash afterwards.
348
- doc = Lutaml::KeyValue::DataModel::Element.new("__root__")
345
+ # (public contract, TODO.perf 19 keeps it): only the doc's
346
+ # payload — the merged children or the direct value hash —
347
+ # is merged into the output.
348
+ doc = Lutaml::KeyValue::DataModel::Element.new(rule.serialized_name)
349
349
  # Custom methods may write children, or doc.value[key]=, or
350
350
  # nothing at all (pubid 2.0 regression: an empty scratch
351
351
  # flattened to {key => nil} and crashed the merge). Seed the
@@ -353,7 +353,7 @@ register = self.register)
353
353
  doc.value = {}
354
354
  # lutaml-model#550: custom methods may declare a third context
355
355
  # parameter to receive the options passed to `to_*`.
356
- if model_instance.method(to_method).parameters.size >= 3
356
+ if Lutaml::Model::MappingRule.custom_method_arity(model_instance.class, to_method).to_i >= 3
357
357
  model_instance.public_send(to_method, model_instance,
358
358
  doc, options[:context])
359
359
  else
@@ -363,11 +363,11 @@ register = self.register)
363
363
  # its return value is ignored. (A mutation-style handler whose
364
364
  # last statement is an assignment returns the assigned inner
365
365
  # value; honoring it merged child content at the wrong level —
366
- # lutaml-model#892.) Empty docs serialize as nothing, matching
367
- # the pre-0.8.74 behavior.
368
- merged = doc.to_hash
369
- parent.update(merged["__root__"]) if merged.is_a?(::Hash) &&
370
- merged["__root__"].is_a?(::Hash)
366
+ # lutaml-model#892.) A doc whose payload is not a hash — nil
367
+ # (untouched), a scalar, or an array of primitives — serializes
368
+ # as nothing, matching the pre-0.8.74 behavior.
369
+ merged = doc.payload
370
+ parent.update(merged) if merged.is_a?(::Hash)
371
371
  return
372
372
  end
373
373
 
@@ -539,8 +539,7 @@ converted_from_empty_to_nil: false, converted_from_nil_to_empty: false)
539
539
  value_root = value_transformation.transform(attr_value,
540
540
  options)
541
541
 
542
- # transform emits the output hash directly now
543
- # (TODO.perf 19) — no __root__ wrapper to unwrap.
542
+ # transform emits the output hash directly (TODO.perf 19)
544
543
  parent[key_value.to_s] = value_root
545
544
  next
546
545
  end
@@ -1147,10 +1146,14 @@ child_mappings, options)
1147
1146
  parsed_content.each do |key, val|
1148
1147
  parent[key.to_s] = val
1149
1148
  end
1150
- elsif parsed_content.is_a?(::Array)
1151
- # Graceful handling of unusual map_all content (parity with
1152
- # the tree's __root__ wrapper)
1153
- parent["__root__"] = parsed_content
1149
+ elsif !parsed_content.nil?
1150
+ # A raw mapping's content must be a key-value document — it is
1151
+ # merged into the parent by key. Any other shape (an Array, a
1152
+ # scalar) has no representation here; serializing it under a
1153
+ # synthetic key would leak that key into the document.
1154
+ raise ArgumentError,
1155
+ "raw mapping content must parse to a Hash, got " \
1156
+ "#{parsed_content.class}"
1154
1157
  end
1155
1158
  end
1156
1159
 
@@ -394,6 +394,18 @@ module Lutaml
394
394
  @options[:initialize_empty]
395
395
  end
396
396
 
397
+ # True when the default is the empty-collection lambda installed by
398
+ # set_default_for_collection: the one default whose constructor
399
+ # state (a plain empty Array) the LAZY_EMPTY_COLLECTION sentinel
400
+ # materializes to on first read, so walk-state seeding can use the
401
+ # sentinel instead of running the lambda per instance (#1010).
402
+ # Restricted to plain-Array collections because the lazy reader
403
+ # materializes []; custom Collection classes would lose their type.
404
+ def lazy_collection_default?
405
+ @implicit_collection_default == true && collection? &&
406
+ collection_class == ::Array && !enum?
407
+ end
408
+
397
409
  def validations
398
410
  @options[:validations]
399
411
  end
@@ -624,6 +636,13 @@ instance_object = nil)
624
636
  value.frozen?
625
637
  end
626
638
 
639
+ # Static classification of this attribute's default form (:none,
640
+ # :static, :proc, :delegate) — the constructor's determine_value
641
+ # and the compiled walk-seed share it.
642
+ def default_kind
643
+ @default_kind ||= _default_kind
644
+ end
645
+
627
646
  def default_value(register, instance_object = nil)
628
647
  # The kind is fixed at definition; the common case (no default
629
648
  # at all) paid three option checks per absent attribute per
@@ -1368,7 +1387,10 @@ instance_object = nil)
1368
1387
  def set_default_for_collection
1369
1388
  validate_collection_range
1370
1389
  attr = self
1371
- @options[:default] ||= -> { attr.build_collection } if initialize_empty?
1390
+ if initialize_empty? && !@options[:default]
1391
+ @options[:default] = -> { attr.build_collection }
1392
+ @implicit_collection_default = true
1393
+ end
1372
1394
  end
1373
1395
 
1374
1396
  def validate_options!(options)
@@ -384,11 +384,7 @@ module Lutaml
384
384
  private
385
385
 
386
386
  def unwrap_unwrapped_data(data)
387
- # Convert KeyValueElement to Hash if needed
388
- hash = data.is_a?(Hash) ? data : data.to_hash
389
- # Handle "__root__" wrapper for key-value formats (created by transformation)
390
- hash = hash["__root__"] if hash.key?("__root__")
391
- result = Utils.fetch_str_or_sym(hash, instance_name)
387
+ result = Utils.fetch_str_or_sym(data, instance_name)
392
388
 
393
389
  # Extract values from nested hashes with empty string keys
394
390
  # (created by transformation for simple models with single attribute)
@@ -300,12 +300,31 @@ module Lutaml
300
300
  end
301
301
  end
302
302
 
303
+ # Arity of a custom (from:/to:) method, memoized per class+method:
304
+ # the reflection (method()/parameters) allocated per call and ran
305
+ # per value in the serialize/deserialize hot loops (42% of a
306
+ # custom-heavy to_json profile).
307
+ def self.custom_method_arity(klass, method_name)
308
+ per_klass = (@custom_method_arities ||= {}.compare_by_identity)
309
+ cache = (per_klass[klass] ||= {}.compare_by_identity)
310
+ cached = cache[method_name]
311
+ return cached if cached
312
+
313
+ begin
314
+ arity = klass.instance_method(method_name).parameters.size
315
+ rescue NameError
316
+ arity = nil
317
+ end
318
+ cache[method_name] = arity
319
+ arity
320
+ end
321
+
303
322
  def serialize(model, parent = nil, doc = nil, context = nil)
304
323
  if custom_methods[:to]
305
324
  to_method = custom_methods[:to]
306
325
  # lutaml-model#550: custom methods may declare a fourth
307
326
  # context parameter to receive the options passed to `to_*`.
308
- if model.method(to_method).parameters.size >= 4
327
+ if self.class.custom_method_arity(model.class, to_method).to_i >= 4
309
328
  model.public_send(to_method, model, parent, doc, context)
310
329
  else
311
330
  model.public_send(to_method, model, parent, doc)
@@ -555,7 +574,7 @@ context = nil, pre_cast: false)
555
574
  target = mapper_class.new
556
575
  # lutaml-model#550: custom methods may declare a third context
557
576
  # parameter to receive the options passed to `from_*`.
558
- if target.method(custom).parameters.size >= 3
577
+ if self.class.custom_method_arity(target.class, custom).to_i >= 3
559
578
  target.public_send(custom, model, value, context)
560
579
  else
561
580
  target.public_send(custom, model, value)
@@ -21,12 +21,17 @@ module Lutaml
21
21
  @parent = parent
22
22
  end
23
23
 
24
- # <xs:complexType> -> Definitions::Model.
25
- def build(complex_type)
24
+ # <xs:complexType> -> Definitions::Model. Anonymous types
25
+ # carry the owning element's name as `root_name` so the
26
+ # generated class serializes the element, not the internal
27
+ # CT_ alias.
28
+ def build(complex_type, root_name: nil)
26
29
  model = Definitions::Model.new(
27
30
  class_name: Utils.camel_case(complex_type.name),
28
- xml_root: Definitions::XmlRoot.new(kind: :element,
29
- name: complex_type.name),
31
+ xml_root: Definitions::XmlRoot.new(
32
+ kind: :element,
33
+ name: root_name || complex_type.name,
34
+ ),
30
35
  mixed: !!complex_type.mixed,
31
36
  namespace_class_name: @parent.namespace_class_name,
32
37
  )
@@ -110,8 +110,11 @@ module Lutaml
110
110
  end
111
111
 
112
112
  local = Utils.last_of_split(raw_type)
113
- Definitions::TypeRef.new(kind: :symbol,
114
- value: Utils.snake_case(local))
113
+ primitive = SupportedDataTypes.primitive_symbol(local)
114
+ Definitions::TypeRef.new(
115
+ kind: :symbol,
116
+ value: primitive || Utils.snake_case(local),
117
+ )
115
118
  end
116
119
 
117
120
  private
@@ -169,8 +172,10 @@ module Lutaml
169
172
  element.simple_type)
170
173
  end
171
174
  if element.complex_type
172
- return register_anonymous_complex_type("CT_#{element.name}",
173
- element.complex_type)
175
+ return register_anonymous_complex_type(
176
+ "CT_#{element.name}", element.complex_type,
177
+ root_name: element.name
178
+ )
174
179
  end
175
180
 
176
181
  "string"
@@ -184,8 +189,10 @@ module Lutaml
184
189
  end
185
190
 
186
191
  if item.respond_to?(:complex_type) && item.complex_type
187
- register_anonymous_complex_type("CT_#{item.name}",
188
- item.complex_type)
192
+ register_anonymous_complex_type(
193
+ "CT_#{item.name}", item.complex_type,
194
+ root_name: item.name
195
+ )
189
196
  end
190
197
  end
191
198
 
@@ -196,10 +203,10 @@ module Lutaml
196
203
  anon_name
197
204
  end
198
205
 
199
- def register_anonymous_complex_type(anon_name, anon_node)
206
+ def register_anonymous_complex_type(anon_name, anon_node, root_name: nil)
200
207
  anon_node.name = anon_name
201
208
  @parent.complex_types[anon_name] =
202
- @parent.build_complex_type(anon_node)
209
+ @parent.build_complex_type(anon_node, root_name: root_name)
203
210
  anon_name
204
211
  end
205
212
 
@@ -156,8 +156,8 @@ module Lutaml
156
156
  @simple_types_builder.build(simple_type)
157
157
  end
158
158
 
159
- def build_complex_type(complex_type)
160
- @complex_types_builder.build(complex_type)
159
+ def build_complex_type(complex_type, root_name: nil)
160
+ @complex_types_builder.build(complex_type, root_name: root_name)
161
161
  end
162
162
 
163
163
  private