lutaml-model 0.8.130 → 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 (35) 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 +1 -2
  15. data/lib/lutaml/key_value/transformation.rb +20 -17
  16. data/lib/lutaml/model/attribute.rb +15 -2
  17. data/lib/lutaml/model/collection.rb +1 -5
  18. data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +9 -4
  19. data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +15 -8
  20. data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +2 -2
  21. data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +10 -0
  22. data/lib/lutaml/model/serialize/attribute_definition.rb +57 -4
  23. data/lib/lutaml/model/version.rb +1 -1
  24. data/lib/lutaml/toml/adapter/toml_rb_adapter.rb +1 -7
  25. data/lib/lutaml/toml/adapter/tomlib_adapter.rb +1 -7
  26. data/lib/lutaml/xml/builder/base.rb +3 -1
  27. data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -2
  28. data/lib/lutaml/yaml/adapter/standard_adapter.rb +1 -7
  29. data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +1 -7
  30. data/lib/lutaml/yamlld/adapter.rb +1 -6
  31. data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +3 -3
  32. data/spec/lutaml/key_value/transformation_spec.rb +50 -50
  33. data/spec/lutaml/model/collection_spec.rb +43 -0
  34. data/spec/lutaml/model/schema/xml_compiler_runtime_spec.rb +81 -0
  35. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 600c15c36628ae5051c4c447ebd9f6b8fe7e2b5b5c70dc894e2de663f2725a32
4
- data.tar.gz: a5bcfd67b0416116fdfe806123ce7c679866fc292d19eb6eb1c6ea8849ac2e72
3
+ metadata.gz: 411fee08073d8c3578b1071024486fa23ed6f1c3b93e918f7c47e175b9a95f90
4
+ data.tar.gz: 80e702fd74cda828374ffc7b5367311a2b49ce5ba2d5580e8038e13b6b9269ca
5
5
  SHA512:
6
- metadata.gz: 46d83e87714efcd4d5c60f552eaf94aac748b5c121f458008a07eeece815e1121a00ad95623dc85bad3cab77204de675c0cd876ca0745fe9dfcb63655304ae67
7
- data.tar.gz: be9904e6941df96b90c3feed829f399475a247ecc1900d86787d8dba224d1ef0a7ad8d68838ad13d5c4136df54d034dccb10bb72988424a95cf6541205243739
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
 
@@ -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
@@ -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
@@ -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
@@ -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
  )
@@ -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
@@ -83,6 +83,16 @@ module Lutaml
83
83
  def skippable?(value)
84
84
  SKIPPABLE_NAMES.include?(value.to_s)
85
85
  end
86
+
87
+ # Lutaml primitive symbol for a skippable XSD type
88
+ # (`"int"` -> `:integer`, `"dateTime"` -> `:date_time`); nil
89
+ # when the name is not a skippable built-in.
90
+ def primitive_symbol(value)
91
+ info = TABLE[value.to_sym]
92
+ return nil unless info && info[:skippable]
93
+
94
+ Utils.snake_case(info[:class_name].split("::").last)
95
+ end
86
96
  end
87
97
  end
88
98
  end
@@ -142,6 +142,7 @@ module Lutaml
142
142
  marks = {}
143
143
  simple = []
144
144
  complex = []
145
+ lazy = []
145
146
  attrs.each do |name, attr|
146
147
  next if attr.derived?
147
148
 
@@ -154,8 +155,17 @@ module Lutaml
154
155
  # per-attribute code, so its state is identical by
155
156
  # construction.
156
157
  marks[name] = false
157
- if attr.default_kind == :none && attr.options[:values].nil? &&
158
- name.to_s.match?(PLAIN_NAME)
158
+ plain_name = name.to_s.match?(PLAIN_NAME)
159
+ if plain_name && attr.lazy_collection_default?
160
+ # The implicit empty-collection default: the constructor
161
+ # ends at `[]` + a default mark, and the LAZY sentinel
162
+ # answers `[]` from the reader — so seed the sentinel and
163
+ # skip the per-instance lambda (#1010). The false mark is
164
+ # what the XML unmatched-rule contract reads: a seeded
165
+ # collection must not render as a mid-parse default.
166
+ lazy << name
167
+ elsif attr.default_kind == :none && attr.options[:values].nil? &&
168
+ plain_name
159
169
  simple << name
160
170
  else
161
171
  complex << name
@@ -169,6 +179,9 @@ module Lutaml
169
179
  "@using_default = { #{pairs.join(', ')} }"
170
180
  end
171
181
  simple_lines = simple.map { |n| "@#{n} = nil" }
182
+ lazy_lines = lazy.map do |n|
183
+ "@#{n} = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION"
184
+ end
172
185
  complex_lines = complex.map do |n|
173
186
  "initialize_single_attribute({}, #{n.inspect}, " \
174
187
  "self.class.attributes(@lutaml_register)[#{n.inspect}], " \
@@ -179,6 +192,11 @@ module Lutaml
179
192
  define_method(method_name) do
180
193
  @using_default = marks
181
194
  simple.each { |n| IVAR_SET.bind_call(self, :"@#{n}", nil) }
195
+ lazy.each do |n|
196
+ IVAR_SET.bind_call(
197
+ self, :"@#{n}", Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION
198
+ )
199
+ end
182
200
  complex.each do |n|
183
201
  initialize_single_attribute(
184
202
  {}, n, self.class.attributes(@lutaml_register)[n],
@@ -193,6 +211,7 @@ module Lutaml
193
211
  def #{method_name}
194
212
  #{marks_line}
195
213
  #{simple_lines.join("\n")}
214
+ #{lazy_lines.join("\n")}
196
215
  #{complex_lines.join("\n")}
197
216
  end
198
217
  RUBY
@@ -452,7 +471,7 @@ module Lutaml
452
471
 
453
472
  if !AttributeDefinition.source_compilation?
454
473
  define_block_attribute_setters(name, attr)
455
- elsif attr.collection?
474
+ elsif attr.collection? && !attr.lazy_collection_default?
456
475
  # class_eval interpolates, e.g.:
457
476
  # def items=(value)
458
477
  # value_set_for(:items)
@@ -493,6 +512,25 @@ module Lutaml
493
512
  record_mutation_collection(:#{name}, value)
494
513
  end
495
514
  RUBY
515
+ elsif attr.collection?
516
+ # Lazy-seeded default collections (#1010): a nil write is a
517
+ # real state here (initialize_empty reads nil after an
518
+ # explicit nil value), so it must land over the sentinel
519
+ # instead of preserving it.
520
+ class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
521
+ def #{name}=(value)
522
+ value_set_for(:#{name})
523
+ value = __attribute_definition_#{name}.cast_value(value, lutaml_register)
524
+ @#{name} = value
525
+ record_mutation_collection(:#{name}, value)
526
+ end
527
+
528
+ def __assign_parsed_#{name}=(value)
529
+ value_set_for(:#{name})
530
+ @#{name} = value
531
+ record_mutation_collection(:#{name}, value)
532
+ end
533
+ RUBY
496
534
  else
497
535
  class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
498
536
  def #{name}=(value)
@@ -551,7 +589,7 @@ module Lutaml
551
589
  name = name.to_sym # the compiled form passes :name literals
552
590
  ivar = :"@#{name}"
553
591
  handle = :"__attribute_definition_#{name}"
554
- if attr.collection?
592
+ if attr.collection? && !attr.lazy_collection_default?
555
593
  assign = lambda do |model, value|
556
594
  current = IVAR_GET.bind_call(model, ivar)
557
595
  unless current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
@@ -568,6 +606,21 @@ module Lutaml
568
606
  value_set_for(name)
569
607
  assign.call(self, value)
570
608
  end
609
+ elsif attr.collection?
610
+ # Lazy-seeded default collections (#1010): nil lands over the
611
+ # sentinel — see the source-compiled branch.
612
+ assign = lambda do |model, value|
613
+ IVAR_SET.bind_call(model, ivar, value)
614
+ SEND.bind_call(model, :record_mutation_collection, name, value)
615
+ end
616
+ define_method(:"#{name}=") do |value|
617
+ value_set_for(name)
618
+ assign.call(self, SEND.bind_call(self, handle).cast_value(value, lutaml_register))
619
+ end
620
+ define_method(:"__assign_parsed_#{name}=") do |value|
621
+ value_set_for(name)
622
+ assign.call(self, value)
623
+ end
571
624
  else
572
625
  define_method(:"#{name}=") do |value|
573
626
  value_set_for(name)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.130"
5
+ VERSION = "0.8.131"
6
6
  end
7
7
  end
@@ -19,13 +19,7 @@ module Lutaml
19
19
  def to_toml(*)
20
20
  require "toml-rb"
21
21
  # Handle KeyValueElement input (new symmetric architecture)
22
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
23
- # Unwrap __root__ wrapper to get actual content
24
- @attributes.to_hash["__root__"]
25
- else
26
- # Legacy Hash input (backward compatibility)
27
- @attributes
28
- end
22
+ attributes_to_serialize = @attributes
29
23
 
30
24
  TomlRB.dump(attributes_to_serialize)
31
25
  rescue LoadError
@@ -34,13 +34,7 @@ module Lutaml
34
34
  def to_toml(*)
35
35
  require "tomlib"
36
36
  # Handle KeyValueElement input (new symmetric architecture)
37
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
38
- # Unwrap __root__ wrapper to get actual content
39
- @attributes.to_hash["__root__"]
40
- else
41
- # Legacy Hash input (backward compatibility)
42
- @attributes
43
- end
37
+ attributes_to_serialize = @attributes
44
38
 
45
39
  Tomlib.dump(attributes_to_serialize)
46
40
  # Tomlib::Generator.new(to_h).toml_str
@@ -116,7 +116,9 @@ module Lutaml
116
116
 
117
117
  def add_xml_fragment(element, content)
118
118
  target = resolve_target(element)
119
- parsed = @context.parse("<__root__>#{content}</__root__>")
119
+ # The wrapper tag only gives the parser a root; its name is
120
+ # discarded — only the parsed children are adopted.
121
+ parsed = @context.parse("<fragment>#{content}</fragment>")
120
122
  parsed.root&.children&.each { |child| target.add_child(child) }
121
123
  rescue Moxml::ParseError
122
124
  target.add_child(@doc.create_text(content.to_s))
@@ -64,8 +64,8 @@ module Lutaml
64
64
  # This ensures root namespace is included in all_namespaces
65
65
  if mapping_dsl.namespace_class
66
66
  rules << ::Lutaml::Model::CompiledRule.new(
67
- attribute_name: :__root_namespace__,
68
- serialized_name: "__root__",
67
+ attribute_name: :root_namespace,
68
+ serialized_name: "root_namespace",
69
69
  namespace_class: mapping_dsl.namespace_class,
70
70
  mapping_type: :root_namespace,
71
71
  )
@@ -25,13 +25,7 @@ module Lutaml
25
25
 
26
26
  def to_yaml(options = {})
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
  YAML.dump(attributes_to_serialize, options)
37
31
  end
@@ -91,13 +91,7 @@ module Lutaml
91
91
  private_class_method :syntax_error
92
92
 
93
93
  def to_yaml(_options = {})
94
- attributes_to_serialize = if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
95
- # Unwrap __root__ wrapper to get actual content
96
- @attributes.to_hash["__root__"]
97
- else
98
- # Legacy Hash input (backward compatibility)
99
- @attributes
100
- end
94
+ attributes_to_serialize = @attributes
101
95
 
102
96
  # Generation must match the standard (Psych) adapter's output
103
97
  # byte-for-byte: document separator, quoting of digit-leading
@@ -12,12 +12,7 @@ module Lutaml
12
12
  end
13
13
 
14
14
  def to_yamlld(options = {})
15
- attributes_to_serialize =
16
- if @attributes.is_a?(Lutaml::KeyValue::DataModel::Element)
17
- @attributes.to_hash["__root__"]
18
- else
19
- @attributes
20
- end
15
+ attributes_to_serialize = @attributes
21
16
  YAML.dump(attributes_to_serialize, options)
22
17
  end
23
18
  end
@@ -54,12 +54,12 @@ RSpec.describe(TEPTRIS_AVAILABLE ? Lutaml::KeyValue::Adapter::Toml::TeptrisAdapt
54
54
  .to include("name", "[nested]", "a = [1, 2]")
55
55
  end
56
56
 
57
- it "unwraps the __root__ KeyValueElement wrapper" do
58
- element = Lutaml::KeyValue::DataModel::Element.new("__root__")
57
+ it "serializes Element children through their keyed hash shape" do
58
+ element = Lutaml::KeyValue::DataModel::Element.new("doc")
59
59
  name_child = Lutaml::KeyValue::DataModel::Element.new("name")
60
60
  name_child.value = "x"
61
61
  element.add_child(name_child)
62
- expect(described_class.new(element).to_toml).to include("name")
62
+ expect(element.to_hash).to eq("doc" => { "name" => "x" })
63
63
  end
64
64
  end
65
65
 
@@ -69,14 +69,11 @@ RSpec.describe Lutaml::KeyValue::Transformation do
69
69
  transformation = described_class.new(KVSimpleModel, mapping, :json, nil)
70
70
 
71
71
  result = transformation.transform(model)
72
- hash = { "__root__" => result }
73
-
74
- # Remove __root__ wrapper
75
- expect(hash["__root__"]).to eq({
76
- "name" => "John",
77
- "age" => 30,
78
- "active" => true,
79
- })
72
+ expect(result).to eq({
73
+ "name" => "John",
74
+ "age" => 30,
75
+ "active" => true,
76
+ })
80
77
  end
81
78
 
82
79
  it "handles nil values based on render_nil" do
@@ -85,10 +82,8 @@ RSpec.describe Lutaml::KeyValue::Transformation do
85
82
  transformation = described_class.new(KVSimpleModel, mapping, :json, nil)
86
83
 
87
84
  result = transformation.transform(model)
88
- hash = { "__root__" => result }
89
-
90
85
  # nil values should be rendered by default
91
- expect(hash["__root__"]["age"]).to be_nil
86
+ expect(result["age"]).to be_nil
92
87
  end
93
88
  end
94
89
 
@@ -101,16 +96,14 @@ RSpec.describe Lutaml::KeyValue::Transformation do
101
96
  transformation = described_class.new(KVPerson, mapping, :json, nil)
102
97
 
103
98
  result = transformation.transform(person)
104
- hash = { "__root__" => result }
105
-
106
- expect(hash["__root__"]).to eq({
107
- "name" => "John",
108
- "address" => {
109
- "street" => "123 Main St",
110
- "city" => "NYC",
111
- "zip" => "10001",
112
- },
113
- })
99
+ expect(result).to eq({
100
+ "name" => "John",
101
+ "address" => {
102
+ "street" => "123 Main St",
103
+ "city" => "NYC",
104
+ "zip" => "10001",
105
+ },
106
+ })
114
107
  end
115
108
 
116
109
  it "handles nil nested model" do
@@ -119,10 +112,8 @@ RSpec.describe Lutaml::KeyValue::Transformation do
119
112
  transformation = described_class.new(KVPerson, mapping, :json, nil)
120
113
 
121
114
  result = transformation.transform(person)
122
- hash = { "__root__" => result }
123
-
124
115
  # nil nested model should be in output (based on default render_nil)
125
- expect(hash["__root__"]["address"]).to be_nil
116
+ expect(result["address"]).to be_nil
126
117
  end
127
118
  end
128
119
 
@@ -135,13 +126,11 @@ RSpec.describe Lutaml::KeyValue::Transformation do
135
126
  transformation = described_class.new(KVTeam, mapping, :json, nil)
136
127
 
137
128
  result = transformation.transform(team)
138
- hash = { "__root__" => result }
139
-
140
- expect(hash["__root__"]).to eq({
141
- "name" => "Dev Team",
142
- "members" => ["Alice", "Bob",
143
- "Charlie"],
144
- })
129
+ expect(result).to eq({
130
+ "name" => "Dev Team",
131
+ "members" => ["Alice", "Bob",
132
+ "Charlie"],
133
+ })
145
134
  end
146
135
 
147
136
  it "handles empty collection" do
@@ -150,10 +139,8 @@ RSpec.describe Lutaml::KeyValue::Transformation do
150
139
  transformation = described_class.new(KVTeam, mapping, :json, nil)
151
140
 
152
141
  result = transformation.transform(team)
153
- hash = { "__root__" => result }
154
-
155
142
  # Empty collections are not rendered by default (render_empty defaults to false)
156
- expect(hash["__root__"]["members"]).to be_nil
143
+ expect(result["members"]).to be_nil
157
144
  end
158
145
 
159
146
  it "handles nil collection" do
@@ -162,10 +149,8 @@ RSpec.describe Lutaml::KeyValue::Transformation do
162
149
  transformation = described_class.new(KVTeam, mapping, :json, nil)
163
150
 
164
151
  result = transformation.transform(team)
165
- hash = { "__root__" => result }
166
-
167
152
  # nil collection should be nil by default
168
- expect(hash["__root__"]["members"]).to be_nil
153
+ expect(result["members"]).to be_nil
169
154
  end
170
155
  end
171
156
 
@@ -188,9 +173,7 @@ RSpec.describe Lutaml::KeyValue::Transformation do
188
173
  nil)
189
174
 
190
175
  result = transformation.transform(model)
191
- hash = { "__root__" => result }
192
-
193
- expect(hash["__root__"]["name"]).to eq("JOHN")
176
+ expect(result["name"]).to eq("JOHN")
194
177
  end
195
178
  end
196
179
 
@@ -211,10 +194,8 @@ RSpec.describe Lutaml::KeyValue::Transformation do
211
194
  transformation = described_class.new(KVRenderModel, mapping, :json, nil)
212
195
 
213
196
  result = transformation.transform(model)
214
- hash = { "__root__" => result }
215
-
216
197
  # optional should be omitted
217
- expect(hash["__root__"].keys).not_to include("optional")
198
+ expect(result.keys).not_to include("optional")
218
199
  end
219
200
 
220
201
  it "omits default values when render_default: false" do
@@ -223,10 +204,8 @@ RSpec.describe Lutaml::KeyValue::Transformation do
223
204
  transformation = described_class.new(KVRenderModel, mapping, :json, nil)
224
205
 
225
206
  result = transformation.transform(model)
226
- hash = { "__root__" => result }
227
-
228
207
  # defaulted should be omitted
229
- expect(hash["__root__"].keys).not_to include("defaulted")
208
+ expect(result.keys).not_to include("defaulted")
230
209
  end
231
210
  end
232
211
  end
@@ -239,9 +218,6 @@ RSpec.describe Lutaml::KeyValue::Transformation do
239
218
 
240
219
  result = transformation.transform(model)
241
220
 
242
- # Hash-direct emission: the __root__ Element tree (write-only in
243
- # production, immediately flattened by every consumer) is gone;
244
- # transform produces the output hash itself.
245
221
  expect(result).to be_a(Hash)
246
222
  expect(result).to eq("name" => "John", "age" => 30, "active" => true)
247
223
  end
@@ -257,7 +233,6 @@ RSpec.describe Lutaml::KeyValue::Transformation do
257
233
  expect(result).to be_a(Lutaml::KeyValue::DataModel::Element)
258
234
 
259
235
  # Presentation happens via to_hash, not during transformation
260
- hash = { "__root__" => result }
261
236
  expect(hash).to be_a(Hash)
262
237
  end
263
238
  end
@@ -350,6 +325,31 @@ RSpec.describe Lutaml::KeyValue::Transformation do
350
325
  expect(model.to_hash).to eq("code" => "X")
351
326
  end
352
327
 
328
+ it "builds nested structures through chained Element writes" do
329
+ # glossarist's Citation#locality_to_yaml pattern: assign an empty
330
+ # hash child, then write into it through the hash-like reader —
331
+ # the reader must return the child's own value object.
332
+ klass = Class.new(Lutaml::Model::Serializable) do
333
+ attribute :type, :string
334
+ attribute :reference_from, :string
335
+
336
+ key_value do
337
+ map "locality", to: :type, with: { to: :locality_to_yaml }
338
+ end
339
+
340
+ def locality_to_yaml(model, doc)
341
+ doc["locality"] = {}
342
+ doc["locality"]["type"] = model.type
343
+ doc["locality"]["reference_from"] = model.reference_from
344
+ end
345
+ end
346
+
347
+ model = klass.new(type: "clause", reference_from: "§3")
348
+ expect(model.to_hash).to eq(
349
+ "locality" => { "type" => "clause", "reference_from" => "§3" },
350
+ )
351
+ end
352
+
353
353
  it "ignores a custom method's return value (mutation-only contract)" do
354
354
  # A method returning a hash without touching the doc serializes
355
355
  # as nothing — honoring returns merged child content at the
@@ -416,6 +416,49 @@ RSpec.describe Lutaml::Model::Collection do
416
416
  expect(model.to_yaml).to eq(expected_yaml)
417
417
  end
418
418
 
419
+ # #1010: implicit-default collections are walk-seeded as the lazy
420
+ # sentinel instead of running the default lambda per instance. The
421
+ # sentinel answers [] on first read, and an explicit nil value must
422
+ # land over it — including through the deserialization fast path,
423
+ # whose writers otherwise preserve the sentinel.
424
+ it "reads nil when a parsed nil lands on the deserialization seed" do
425
+ klass = Class.new(Lutaml::Model::Serializable) do
426
+ attribute :items, :string, collection: true, initialize_empty: true
427
+ yaml { map "items", to: :items }
428
+ end
429
+
430
+ instance = klass.allocate_for_deserialization
431
+ instance.__assign_parsed_items = nil
432
+
433
+ expect(instance.items).to be_nil
434
+ end
435
+
436
+ it "materializes [] for untouched lazy-seeded collections" do
437
+ klass = Class.new(Lutaml::Model::Serializable) do
438
+ attribute :items, :string, collection: true, initialize_empty: true
439
+ yaml { map "items", to: :items }
440
+ end
441
+
442
+ parsed = klass.from_yaml("---\n{}\n")
443
+
444
+ expect(parsed.items).to eq([])
445
+ first = parsed.items
446
+ expect(parsed.items).to equal(first)
447
+ end
448
+
449
+ it "keeps an explicit default over the implicit empty collection" do
450
+ klass = Class.new(Lutaml::Model::Serializable) do
451
+ attribute :items, :integer,
452
+ collection: true, initialize_empty: true,
453
+ default: -> { [1, 2] }
454
+ yaml { map "items", to: :items }
455
+ end
456
+
457
+ parsed = klass.from_yaml("---\n{}\n")
458
+
459
+ expect(parsed.items).to eq([1, 2])
460
+ end
461
+
419
462
  it "raises StandardError for initialize_empty without collection" do
420
463
  expect do
421
464
  Class.new(Lutaml::Model::Serializable) do
@@ -49,4 +49,85 @@ RSpec.describe "runtime XSD class loading" do
49
49
  expect(doc.to_xml).to include('id="e1"')
50
50
  end
51
51
  end
52
+
53
+ # Anonymous (inline) complexTypes get CT_-prefixed internal names;
54
+ # the generated classes must still (a) map skippable XSD built-ins
55
+ # like xs:int to Lutaml primitives and (b) serialize the owning
56
+ # element name, not the internal alias (#1014).
57
+ describe "inline anonymous complexTypes" do
58
+ let(:catalogue_xsd) do
59
+ <<~XSD
60
+ <?xml version="1.0" encoding="UTF-8"?>
61
+ <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
62
+ elementFormDefault="qualified">
63
+ <xs:element name="catalogue">
64
+ <xs:complexType>
65
+ <xs:sequence>
66
+ <xs:element name="title" type="xs:string"/>
67
+ <xs:element name="entry" maxOccurs="unbounded">
68
+ <xs:complexType>
69
+ <xs:sequence>
70
+ <xs:element name="name" type="xs:string"/>
71
+ <xs:element name="year" type="xs:int"/>
72
+ </xs:sequence>
73
+ <xs:attribute name="id" type="xs:string" use="required"/>
74
+ </xs:complexType>
75
+ </xs:element>
76
+ </xs:sequence>
77
+ </xs:complexType>
78
+ </xs:element>
79
+ </xs:schema>
80
+ XSD
81
+ end
82
+
83
+ let(:catalogue_xml) do
84
+ <<~XML
85
+ <catalogue>
86
+ <title>Test</title>
87
+ <entry id="e1"><name>Alpha</name><year>2024</year></entry>
88
+ <entry id="e2"><name>Beta</name><year>2025</year></entry>
89
+ </catalogue>
90
+ XML
91
+ end
92
+
93
+ it "maps xs:int to :integer and parses the instance" do
94
+ Dir.mktmpdir do |dir|
95
+ Lutaml::Model::Schema.from_xml(
96
+ catalogue_xsd, output_dir: File.join(dir, "anon_ct_models"),
97
+ load_classes: true
98
+ )
99
+
100
+ entry_class = AnonCtModels::CTEntry
101
+ expect(entry_class.attributes[:year].type).to eq(
102
+ Lutaml::Model::Type::Integer,
103
+ )
104
+
105
+ doc = AnonCtModels::CTCatalogue.from_xml(catalogue_xml)
106
+
107
+ expect(doc.title).to eq("Test")
108
+ expect(doc.entry.map(&:name)).to eq(["Alpha", "Beta"])
109
+ expect(doc.entry.map(&:year)).to eq([2024, 2025])
110
+ expect(doc.entry.map(&:id)).to eq(%w[e1 e2])
111
+ end
112
+ end
113
+
114
+ it "serializes the owning element name, not the CT_ alias" do
115
+ Dir.mktmpdir do |dir|
116
+ Lutaml::Model::Schema.from_xml(
117
+ catalogue_xsd, output_dir: File.join(dir, "anon_ct_models2"),
118
+ load_classes: true
119
+ )
120
+
121
+ entry = AnonCtModels2::CTEntry.from_xml(
122
+ '<entry id="z9"><name>Gamma</name><year>1999</year></entry>',
123
+ )
124
+
125
+ expect(entry.name).to eq("Gamma")
126
+ expect(entry.year).to eq(1999)
127
+ expect(entry.id).to eq("z9")
128
+ expect(entry.to_xml).to include('<entry id="z9">')
129
+ expect(entry.to_xml).not_to include("CT_entry")
130
+ end
131
+ end
132
+ end
52
133
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.130
4
+ version: 0.8.131
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.