lutaml-model 0.8.130 → 0.8.132

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 (38) 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/transform.rb +24 -8
  15. data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -2
  16. data/lib/lutaml/key_value/transformation.rb +33 -20
  17. data/lib/lutaml/model/attribute.rb +15 -2
  18. data/lib/lutaml/model/collection.rb +1 -5
  19. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +9 -4
  20. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +15 -8
  21. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +2 -2
  22. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +10 -0
  23. data/lib/lutaml/model/serialize/attribute_definition.rb +57 -4
  24. data/lib/lutaml/model/transform.rb +78 -25
  25. data/lib/lutaml/model/type_context.rb +9 -0
  26. data/lib/lutaml/model/version.rb +1 -1
  27. data/lib/lutaml/toml/adapter/toml_rb_adapter.rb +1 -7
  28. data/lib/lutaml/toml/adapter/tomlib_adapter.rb +1 -7
  29. data/lib/lutaml/xml/builder/base.rb +3 -1
  30. data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -2
  31. data/lib/lutaml/yaml/adapter/standard_adapter.rb +1 -7
  32. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +1 -7
  33. data/lib/lutaml/yamlld/adapter.rb +1 -6
  34. data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +3 -3
  35. data/spec/lutaml/key_value/transformation_spec.rb +50 -50
  36. data/spec/lutaml/model/collection_spec.rb +43 -0
  37. data/spec/lutaml/model/schema/xml_compiler_runtime_spec.rb +81 -0
  38. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 600c15c36628ae5051c4c447ebd9f6b8fe7e2b5b5c70dc894e2de663f2725a32
4
- data.tar.gz: a5bcfd67b0416116fdfe806123ce7c679866fc292d19eb6eb1c6ea8849ac2e72
3
+ metadata.gz: 83ece4646bc4f2ecd052f6ba5edf994cb512b4be3a08e8a1778c5b357daab424
4
+ data.tar.gz: 9bac20d16559163de764a7148974724c199cacbce0d88d945b932c7d36ccaa19
5
5
  SHA512:
6
- metadata.gz: 46d83e87714efcd4d5c60f552eaf94aac748b5c121f458008a07eeece815e1121a00ad95623dc85bad3cab77204de675c0cd876ca0745fe9dfcb63655304ae67
7
- data.tar.gz: be9904e6941df96b90c3feed829f399475a247ecc1900d86787d8dba224d1ef0a7ad8d68838ad13d5c4136df54d034dccb10bb72988424a95cf6541205243739
6
+ metadata.gz: 27120683de1ff8d9c95049e683dffdf302aa43edf9a4e5e6debda189df3903fdf98f4722200eb4106285e86dfabb170b77fcaea3313c6e1a8e8e548ec2db0eab
7
+ data.tar.gz: 26a84cc1b288f7b829c1bdb44360adc7211843e506d1d51c1f416c411636fa6099dee79fd26d4d41a7e22b5eaf3ef1510d9580dbe49318ac8c2b9e02b80974bc
@@ -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
 
@@ -106,8 +106,7 @@ module Lutaml
106
106
  # transformation_for returns nil for cyclic dependencies or :building sentinel
107
107
  # Fall back to legacy approach in these cases
108
108
  if transformation.is_a?(Lutaml::KeyValue::Transformation)
109
- # transform emits the output hash directly (TODO.perf 19) —
110
- # the __root__ Element tree and its to_hash flatten are gone.
109
+ # transform emits the output hash directly (TODO.perf 19)
111
110
  return transformation.transform(instance, options)
112
111
  end
113
112
  end
@@ -347,15 +346,21 @@ child_mappings)
347
346
  # path. Returns nil (caller falls back) on any shape the plan
348
347
  # does not cover, e.g. a non-Hash item where a model was expected.
349
348
  def kv_group_build(rows, doc, format, register, options)
350
- setters = []
349
+ # Positional values, not per-row [name, value] pairs: rows are a
350
+ # fixed per-class list, so the writer to apply lives at the row's
351
+ # index and the pair arrays were the lane's biggest allocation
352
+ # (~6 arrays per node on hydrate-heavy corpora).
353
+ values = Array.new(rows.size, MISSING)
351
354
  children = []
352
355
  # Lazy: the present-key hot path never pushes, and this runs
353
356
  # once per hydrated model instance.
354
357
  absent = nil
355
358
  unmapped = nil
359
+ i = 0
356
360
  rows.each do |rule, attr, kind, type, child_rows, child_register|
357
361
  if kind == :unmapped
358
362
  unmapped = [type, child_rows]
363
+ i += 1
359
364
  next
360
365
  end
361
366
 
@@ -365,6 +370,7 @@ child_mappings)
365
370
  v = Lutaml::Model::Utils.fetch_str_or_sym(doc, rule.name, MISSING)
366
371
  if v.equal?(MISSING)
367
372
  (absent ||= []) << rule
373
+ i += 1
368
374
  next
369
375
  end
370
376
 
@@ -390,13 +396,15 @@ child_mappings)
390
396
  built << self.class.data_to_model(type.attr_type,
391
397
  child_hash, format, { mappings: type.child_rules })
392
398
  end
393
- setters << [rule.kv_setter, built]
399
+ values[i] = built
394
400
  children.concat(built)
401
+ i += 1
395
402
  next
396
403
  end
397
404
 
398
405
  if kind == :scalar
399
- setters << [rule.kv_setter, v]
406
+ values[i] = v
407
+ i += 1
400
408
  next
401
409
  end
402
410
 
@@ -418,7 +426,7 @@ child_hash, format, { mappings: type.child_rules })
418
426
 
419
427
  built << child
420
428
  end
421
- setters << [rule.kv_setter, built]
429
+ values[i] = built
422
430
  children.concat(built)
423
431
  else
424
432
  return nil unless v.is_a?(::Hash)
@@ -428,12 +436,20 @@ child_hash, format, { mappings: type.child_rules })
428
436
  options)
429
437
  return nil if child.nil?
430
438
 
431
- setters << [rule.kv_setter, child]
439
+ values[i] = child
432
440
  children << child
433
441
  end
442
+ i += 1
434
443
  end
435
444
  instance = model_class.allocate_for_walk(register)
436
- setters.each { |name, value| instance.public_send(name, value) }
445
+ i = 0
446
+ rows.each do |rule, _attr, kind, *_rest|
447
+ v = values[i]
448
+ i += 1
449
+ next if v.equal?(MISSING) || kind == :unmapped
450
+
451
+ instance.public_send(rule.kv_setter, v)
452
+ end
437
453
  absent&.each do |rule|
438
454
  process_mapping_rule(doc, instance, format, rule, options, nil)
439
455
  end
@@ -43,6 +43,9 @@ model_class: nil)
43
43
  @register_id = register_id
44
44
  @transformation_factory = transformation_factory
45
45
  @model_class = model_class
46
+ # Pre-declared: the serializer may be frozen after setup, and
47
+ # the type-transformation memo mutates this hash in place.
48
+ @lutaml_type_transformations = {}
46
49
  end
47
50
 
48
51
  # Serialize a value for an item (handles nested models and primitives).
@@ -117,10 +120,10 @@ model_class: nil)
117
120
 
118
121
  # Get or create child transformation
119
122
  child_transformation = if uses_polymorphism
120
- create_transformation(actual_type)
123
+ transformation_for_type(actual_type)
121
124
  else
122
125
  rule.child_transformation ||
123
- create_transformation(rule.attribute_type)
126
+ transformation_for_type(rule.attribute_type)
124
127
  end
125
128
 
126
129
  if child_transformation
@@ -296,6 +299,15 @@ model_class: nil)
296
299
  transformation_factory.call(type_class)
297
300
  end
298
301
 
302
+ # Memoized on this object: the factory binds this object's
303
+ # mapping/format/register, so the memo is register-safe, and
304
+ # building one per serialized node is a Transformation per
305
+ # nested model per instance.
306
+ def transformation_for_type(type_class)
307
+ (@lutaml_type_transformations ||= {})[type_class] ||=
308
+ create_transformation(type_class)
309
+ end
310
+
299
311
  # Transform a nested model using the child transformation.
300
312
  #
301
313
  # @param value [Object] The model instance
@@ -63,6 +63,9 @@ module Lutaml
63
63
 
64
64
  # Set up serializers before calling super
65
65
  # The super will call compile_rules and freeze
66
+ # Pre-declared: the transformation is frozen after compile, and
67
+ # the type-transformation memo mutates this hash in place.
68
+ @lutaml_type_transformations = {}
66
69
  @rule_compiler = RuleCompiler.new(
67
70
  model_class: model_class,
68
71
  register_id: register_id,
@@ -221,9 +224,8 @@ register = self.register)
221
224
  def transform(model_instance, options = {})
222
225
  # For key-value formats, we typically don't have a named root
223
226
  # 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.
227
+ # Hash-direct emission (TODO.perf 19): the output hash is built
228
+ # directly; the intermediate Element tree and its flatten are gone.
227
229
  root = {}
228
230
 
229
231
  # Apply each compiled rule (with filtering support). A
@@ -343,9 +345,10 @@ register = self.register)
343
345
  if rule.has_custom_methods? && rule.custom_methods[:to]
344
346
  to_method = rule.custom_methods[:to]
345
347
  # 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__")
348
+ # (public contract, TODO.perf 19 keeps it): only the doc's
349
+ # payload — the merged children or the direct value hash —
350
+ # is merged into the output.
351
+ doc = Lutaml::KeyValue::DataModel::Element.new(rule.serialized_name)
349
352
  # Custom methods may write children, or doc.value[key]=, or
350
353
  # nothing at all (pubid 2.0 regression: an empty scratch
351
354
  # flattened to {key => nil} and crashed the merge). Seed the
@@ -363,11 +366,11 @@ register = self.register)
363
366
  # its return value is ignored. (A mutation-style handler whose
364
367
  # last statement is an assignment returns the assigned inner
365
368
  # 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)
369
+ # lutaml-model#892.) A doc whose payload is not a hash — nil
370
+ # (untouched), a scalar, or an array of primitives — serializes
371
+ # as nothing, matching the pre-0.8.74 behavior.
372
+ merged = doc.payload
373
+ parent.update(merged) if merged.is_a?(::Hash)
371
374
  return
372
375
  end
373
376
 
@@ -539,8 +542,7 @@ converted_from_empty_to_nil: false, converted_from_nil_to_empty: false)
539
542
  value_root = value_transformation.transform(attr_value,
540
543
  options)
541
544
 
542
- # transform emits the output hash directly now
543
- # (TODO.perf 19) — no __root__ wrapper to unwrap.
545
+ # transform emits the output hash directly (TODO.perf 19)
544
546
  parent[key_value.to_s] = value_root
545
547
  next
546
548
  end
@@ -1035,6 +1037,15 @@ child_mappings, options)
1035
1037
  # @param value [Object] The value
1036
1038
  # @param options [Hash] Options
1037
1039
  # @return [Object] The value (could be Hash, primitive, or KeyValueElement)
1040
+ # Child transformations memoized on this transformation instance:
1041
+ # the factory binds this instance's mapping/format/register, so
1042
+ # the memo is register-safe, and building one per serialized node
1043
+ # is a Transformation per nested model per instance.
1044
+ def transformation_for_type(type_class)
1045
+ (@lutaml_type_transformations ||= {})[type_class] ||=
1046
+ build_child_transformation(type_class)
1047
+ end
1048
+
1038
1049
  def create_value_for_item(rule, value, options)
1039
1050
  return nil if value.nil?
1040
1051
 
@@ -1099,9 +1110,7 @@ child_mappings, options)
1099
1110
  if !child_transformation || uses_polymorphism
1100
1111
  # For polymorphic types, use the actual runtime type's mappings
1101
1112
  type_for_mapping = uses_polymorphism ? actual_type : rule.attribute_type
1102
- mapping = type_for_mapping.mappings_for(format, register_id)
1103
- child_transformation = self.class.new(type_for_mapping, mapping,
1104
- format, register)
1113
+ child_transformation = transformation_for_type(type_for_mapping)
1105
1114
  end
1106
1115
 
1107
1116
  if child_transformation
@@ -1147,10 +1156,14 @@ child_mappings, options)
1147
1156
  parsed_content.each do |key, val|
1148
1157
  parent[key.to_s] = val
1149
1158
  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
1159
+ elsif !parsed_content.nil?
1160
+ # A raw mapping's content must be a key-value document — it is
1161
+ # merged into the parent by key. Any other shape (an Array, a
1162
+ # scalar) has no representation here; serializing it under a
1163
+ # synthetic key would leak that key into the document.
1164
+ raise ArgumentError,
1165
+ "raw mapping content must parse to a Hash, got " \
1166
+ "#{parsed_content.class}"
1154
1167
  end
1155
1168
  end
1156
1169
 
@@ -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
@@ -1375,8 +1387,9 @@ instance_object = nil)
1375
1387
  def set_default_for_collection
1376
1388
  validate_collection_range
1377
1389
  attr = self
1378
- if initialize_empty?
1379
- @options[:default] ||= -> { attr.build_collection }
1390
+ if initialize_empty? && !@options[:default]
1391
+ @options[:default] = -> { attr.build_collection }
1392
+ @implicit_collection_default = true
1380
1393
  end
1381
1394
  end
1382
1395
 
@@ -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)
@@ -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
  )