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.
- checksums.yaml +4 -4
- data/docs/key_value_data_model.adoc +9 -9
- data/lib/lutaml/json/adapter/multi_json_adapter.rb +1 -7
- data/lib/lutaml/json/adapter/oj_adapter.rb +1 -7
- data/lib/lutaml/json/adapter/standard_adapter.rb +1 -7
- data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +1 -7
- data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +1 -7
- data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +1 -7
- data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +1 -7
- data/lib/lutaml/key_value/adapter/toml/toml_rb_adapter.rb +1 -7
- data/lib/lutaml/key_value/adapter/toml/tomlib_adapter.rb +1 -7
- data/lib/lutaml/key_value/adapter/yaml/standard_adapter.rb +1 -7
- data/lib/lutaml/key_value/data_model/element.rb +31 -32
- data/lib/lutaml/key_value/transform.rb +1 -2
- data/lib/lutaml/key_value/transformation.rb +20 -17
- data/lib/lutaml/model/attribute.rb +15 -2
- data/lib/lutaml/model/collection.rb +1 -5
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +9 -4
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +15 -8
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +2 -2
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +10 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +57 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/toml/adapter/toml_rb_adapter.rb +1 -7
- data/lib/lutaml/toml/adapter/tomlib_adapter.rb +1 -7
- data/lib/lutaml/xml/builder/base.rb +3 -1
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -2
- data/lib/lutaml/yaml/adapter/standard_adapter.rb +1 -7
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +1 -7
- data/lib/lutaml/yamlld/adapter.rb +1 -6
- data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +3 -3
- data/spec/lutaml/key_value/transformation_spec.rb +50 -50
- data/spec/lutaml/model/collection_spec.rb +43 -0
- data/spec/lutaml/model/schema/xml_compiler_runtime_spec.rb +81 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 411fee08073d8c3578b1071024486fa23ed6f1c3b93e918f7c47e175b9a95f90
|
|
4
|
+
data.tar.gz: 80e702fd74cda828374ffc7b5367311a2b49ce5ba2d5580e8038e13b6b9269ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d48928bc46206f2067989a4637ace150fcf71db4df6c18e1df93de5c621c088db0490bf85d860ec6861ad08d208218553a5d2ae7806ea4b5e88067b0032d51ed
|
|
7
|
+
data.tar.gz: 8d0127e8139146f8d1df26a488904d7e48202e4c5b75a49f67a895f87d372507f9217253590496c44513677653bd8516f009d59f7889a41832c2e12333987585
|
|
@@ -697,21 +697,21 @@ end
|
|
|
697
697
|
|
|
698
698
|
=== Detection Logic
|
|
699
699
|
|
|
700
|
-
|
|
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(
|
|
705
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
#
|
|
127
|
-
#
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
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
|
|
225
|
-
#
|
|
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):
|
|
347
|
-
#
|
|
348
|
-
|
|
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.)
|
|
367
|
-
#
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
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.
|
|
1151
|
-
#
|
|
1152
|
-
# the
|
|
1153
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
29
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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(
|
|
173
|
-
|
|
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(
|
|
188
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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)
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
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: :
|
|
68
|
-
serialized_name: "
|
|
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 =
|
|
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 =
|
|
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 "
|
|
58
|
-
element = Lutaml::KeyValue::DataModel::Element.new("
|
|
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(
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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(
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|