lutaml-model 0.8.129 → 0.8.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/mapping_rule.rb +4 -4
- data/lib/lutaml/key_value/transform.rb +6 -5
- data/lib/lutaml/key_value/transformation.rb +21 -18
- data/lib/lutaml/model/attribute.rb +23 -1
- data/lib/lutaml/model/collection.rb +1 -5
- data/lib/lutaml/model/mapping/mapping_rule.rb +21 -2
- 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 +133 -2
- data/lib/lutaml/model/serialize/initialization.rb +12 -0
- data/lib/lutaml/model/serialize.rb +30 -10
- 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/group_instantiate_spec.rb +8 -0
- 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 +2 -2
|
@@ -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
|
|
@@ -120,6 +120,103 @@ module Lutaml
|
|
|
120
120
|
RUBY
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
+
# The walk-side seed (#1003): constructor-equivalent state in one
|
|
124
|
+
# compiled pass. Unlike the deserialization seed (sentinels, lazy
|
|
125
|
+
# collections — readers answer uninitialized), the programmatic
|
|
126
|
+
# constructor resolves defaults and leaves non-defaulted
|
|
127
|
+
# attributes nil with explicit using-default marks; the KV walk's
|
|
128
|
+
# absent-attribute rendering contract depends on exactly that.
|
|
129
|
+
def walk_state_defaults_name!(register_id)
|
|
130
|
+
@walk_state_defaults_names ||= {}
|
|
131
|
+
name = @walk_state_defaults_names[register_id]
|
|
132
|
+
return name if name
|
|
133
|
+
|
|
134
|
+
method_name = :"__init_walk_state_defaults_#{register_id}"
|
|
135
|
+
compile_walk_state_defaults!(method_name, register_id)
|
|
136
|
+
@walk_state_defaults_names[register_id] = method_name
|
|
137
|
+
method_name
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def compile_walk_state_defaults!(method_name, register_id = nil)
|
|
141
|
+
attrs = attributes(register_id)
|
|
142
|
+
marks = {}
|
|
143
|
+
simple = []
|
|
144
|
+
complex = []
|
|
145
|
+
lazy = []
|
|
146
|
+
attrs.each do |name, attr|
|
|
147
|
+
next if attr.derived?
|
|
148
|
+
|
|
149
|
+
# Inline only the provably-simple shape: no default, no
|
|
150
|
+
# value set (enum attributes materialize through their
|
|
151
|
+
# writer), plain ivar-safe name. The constructor's end
|
|
152
|
+
# state for that shape is exactly `nil` + a false mark.
|
|
153
|
+
# Everything else — defaults, enums, delegated and exotic
|
|
154
|
+
# attributes — routes through the constructor's own
|
|
155
|
+
# per-attribute code, so its state is identical by
|
|
156
|
+
# construction.
|
|
157
|
+
marks[name] = false
|
|
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
|
|
169
|
+
simple << name
|
|
170
|
+
else
|
|
171
|
+
complex << name
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
marks_line = if marks.empty?
|
|
176
|
+
"@using_default = {}"
|
|
177
|
+
else
|
|
178
|
+
pairs = marks.map { |k, v| "#{k.inspect} => #{v}" }
|
|
179
|
+
"@using_default = { #{pairs.join(', ')} }"
|
|
180
|
+
end
|
|
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
|
|
185
|
+
complex_lines = complex.map do |n|
|
|
186
|
+
"initialize_single_attribute({}, #{n.inspect}, " \
|
|
187
|
+
"self.class.attributes(@lutaml_register)[#{n.inspect}], " \
|
|
188
|
+
"Serialize::DEFAULT_VALUE_MAP)"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
unless AttributeDefinition.source_compilation?
|
|
192
|
+
define_method(method_name) do
|
|
193
|
+
@using_default = marks
|
|
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
|
|
200
|
+
complex.each do |n|
|
|
201
|
+
initialize_single_attribute(
|
|
202
|
+
{}, n, self.class.attributes(@lutaml_register)[n],
|
|
203
|
+
Serialize::DEFAULT_VALUE_MAP
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
return
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
211
|
+
def #{method_name}
|
|
212
|
+
#{marks_line}
|
|
213
|
+
#{simple_lines.join("\n")}
|
|
214
|
+
#{lazy_lines.join("\n")}
|
|
215
|
+
#{complex_lines.join("\n")}
|
|
216
|
+
end
|
|
217
|
+
RUBY
|
|
218
|
+
end
|
|
219
|
+
|
|
123
220
|
# Block-form raw-state reader (no String eval), defined on self.
|
|
124
221
|
def define_raw_state_reader(name)
|
|
125
222
|
ivar = :"@#{name}"
|
|
@@ -374,7 +471,7 @@ module Lutaml
|
|
|
374
471
|
|
|
375
472
|
if !AttributeDefinition.source_compilation?
|
|
376
473
|
define_block_attribute_setters(name, attr)
|
|
377
|
-
elsif attr.collection?
|
|
474
|
+
elsif attr.collection? && !attr.lazy_collection_default?
|
|
378
475
|
# class_eval interpolates, e.g.:
|
|
379
476
|
# def items=(value)
|
|
380
477
|
# value_set_for(:items)
|
|
@@ -415,6 +512,25 @@ module Lutaml
|
|
|
415
512
|
record_mutation_collection(:#{name}, value)
|
|
416
513
|
end
|
|
417
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
|
|
418
534
|
else
|
|
419
535
|
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
420
536
|
def #{name}=(value)
|
|
@@ -473,7 +589,7 @@ module Lutaml
|
|
|
473
589
|
name = name.to_sym # the compiled form passes :name literals
|
|
474
590
|
ivar = :"@#{name}"
|
|
475
591
|
handle = :"__attribute_definition_#{name}"
|
|
476
|
-
if attr.collection?
|
|
592
|
+
if attr.collection? && !attr.lazy_collection_default?
|
|
477
593
|
assign = lambda do |model, value|
|
|
478
594
|
current = IVAR_GET.bind_call(model, ivar)
|
|
479
595
|
unless current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
|
|
@@ -490,6 +606,21 @@ module Lutaml
|
|
|
490
606
|
value_set_for(name)
|
|
491
607
|
assign.call(self, value)
|
|
492
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
|
|
493
624
|
else
|
|
494
625
|
define_method(:"#{name}=") do |value|
|
|
495
626
|
value_set_for(name)
|
|
@@ -363,6 +363,18 @@ module Lutaml
|
|
|
363
363
|
#
|
|
364
364
|
# @param register [Symbol, nil] The register context
|
|
365
365
|
# @return [Object] The allocated instance
|
|
366
|
+
# Walk-side allocation (#1003): the KV walk's instantiation —
|
|
367
|
+
# constructor-equivalent state via the compiled walk seed
|
|
368
|
+
# (defaults resolved, non-defaulted attributes nil) without the
|
|
369
|
+
# per-attribute determine_value walk.
|
|
370
|
+
def allocate_for_walk(register = nil)
|
|
371
|
+
register_id = extract_register_id(register)
|
|
372
|
+
ensure_imports!(register_id) if finalized? && deferred_imports_pending?(register_id)
|
|
373
|
+
instance = allocate
|
|
374
|
+
instance.init_walk_state(register_id)
|
|
375
|
+
instance
|
|
376
|
+
end
|
|
377
|
+
|
|
366
378
|
def allocate_for_deserialization(register = nil)
|
|
367
379
|
register_id = extract_register_id(register)
|
|
368
380
|
# Deferred imports (symbol-form import_model_attributes) must
|
|
@@ -190,6 +190,18 @@ module Lutaml
|
|
|
190
190
|
register_in_reference_store
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# Walk-side construction (#1003): constructor-equivalent state
|
|
194
|
+
# (defaults resolved, non-defaulted attributes nil, explicit
|
|
195
|
+
# using-default marks) seeded in one compiled pass, replacing the
|
|
196
|
+
# per-attribute determine_value walk on every parsed instance.
|
|
197
|
+
def init_walk_state(register)
|
|
198
|
+
register_id = self.class.extract_register_id(register)
|
|
199
|
+
@lutaml_register = register_id
|
|
200
|
+
public_send(self.class.walk_state_defaults_name!(register_id))
|
|
201
|
+
define_singleton_attribute_methods
|
|
202
|
+
register_in_reference_store
|
|
203
|
+
end
|
|
204
|
+
|
|
193
205
|
def extract_register_id(attrs, options)
|
|
194
206
|
register = attrs&.dig(:lutaml_register) || options&.dig(:register)
|
|
195
207
|
self.class.extract_register_id(register)
|
|
@@ -378,19 +390,27 @@ module Lutaml
|
|
|
378
390
|
self.class.attributes(lutaml_register).each do |name, attr|
|
|
379
391
|
next if attr.derived?
|
|
380
392
|
|
|
381
|
-
|
|
382
|
-
default = using_default?(name)
|
|
383
|
-
value = self.class.apply_value_map(value, vmap, attr)
|
|
384
|
-
# Performance: Only call ensure_utf8 for string values
|
|
385
|
-
value = self.class.ensure_utf8(value) if value.is_a?(::String)
|
|
386
|
-
# Interpolating the writer symbol allocated a String per
|
|
387
|
-
# attribute per instance; the per-register writer table is
|
|
388
|
-
# interned once per class.
|
|
389
|
-
public_send(self.class.instantiate_writers(lutaml_register)[name], value)
|
|
390
|
-
using_default_for(name) if default
|
|
393
|
+
initialize_single_attribute(attrs, name, attr, vmap)
|
|
391
394
|
end
|
|
392
395
|
end
|
|
393
396
|
|
|
397
|
+
# The constructor's per-attribute body, callable one attribute at
|
|
398
|
+
# a time — the compiled walk seed (#1003) routes every attribute
|
|
399
|
+
# it does not inline through this exact code, so its state is
|
|
400
|
+
# identical by construction.
|
|
401
|
+
def initialize_single_attribute(attrs, name, attr, vmap)
|
|
402
|
+
value = determine_value(attrs, name, attr)
|
|
403
|
+
default = using_default?(name)
|
|
404
|
+
value = self.class.apply_value_map(value, vmap, attr)
|
|
405
|
+
# Performance: Only call ensure_utf8 for string values
|
|
406
|
+
value = self.class.ensure_utf8(value) if value.is_a?(::String)
|
|
407
|
+
# Interpolating the writer symbol allocated a String per
|
|
408
|
+
# attribute per instance; the per-register writer table is
|
|
409
|
+
# interned once per class.
|
|
410
|
+
public_send(self.class.instantiate_writers(lutaml_register)[name], value)
|
|
411
|
+
using_default_for(name) if default
|
|
412
|
+
end
|
|
413
|
+
|
|
394
414
|
def determine_value(attrs, name, attr)
|
|
395
415
|
# One hash pass: a sentinel fetch distinguishes absent keys
|
|
396
416
|
# from present-nil values, replacing two key probes plus the
|
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
|
|
|
@@ -104,6 +104,14 @@ RSpec.describe "KV group-then-bulk hydration" do
|
|
|
104
104
|
@built += 1
|
|
105
105
|
super
|
|
106
106
|
end
|
|
107
|
+
|
|
108
|
+
# The walk and group lanes construct through the compiled walk
|
|
109
|
+
# seed (#1003) rather than new; count that entry too so the
|
|
110
|
+
# wiring proof tracks the real construction path.
|
|
111
|
+
def allocate_for_walk(register = nil)
|
|
112
|
+
@built += 1
|
|
113
|
+
super
|
|
114
|
+
end
|
|
107
115
|
end
|
|
108
116
|
end
|
|
109
117
|
holder = Class.new(Lutaml::Model::Serializable) do
|
|
@@ -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
|