lutaml-model 0.8.19 → 0.9.1
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.
Potentially problematic release.
This version of lutaml-model might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +1 -1
- data/.github/workflows/rake.yml +1 -0
- data/.rubocop_todo.yml +133 -72
- data/bench/bench_common.rb +6 -3
- data/bench/bench_niso.rb +1 -2
- data/lib/compat/opal/generate_boot.rb +3 -1
- data/lib/lutaml/key_value/transform.rb +4 -1
- data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
- data/lib/lutaml/model/attribute.rb +23 -2
- data/lib/lutaml/model/global_context.rb +4 -2
- data/lib/lutaml/model/schema/class_loader.rb +2 -1
- data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
- data/lib/lutaml/model/schema/file_writer.rb +5 -2
- data/lib/lutaml/model/schema/renderers/base.rb +2 -1
- data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
- data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
- data/lib/lutaml/model/schema/renderers/model.rb +16 -4
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
- data/lib/lutaml/model/schema/renderers.rb +2 -1
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
- data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
- data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
- data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
- data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
- data/lib/lutaml/model/serialize/initialization.rb +9 -1
- data/lib/lutaml/model/serialize/model_import.rb +1 -0
- data/lib/lutaml/model/serialize.rb +5 -4
- data/lib/lutaml/model/union.rb +5 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/mapping_rule.rb +55 -1
- data/lib/lutaml/xml/model_transform.rb +53 -5
- data/lib/lutaml/xml/schema/xsd.rb +0 -2
- data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
- data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
- data/lutaml-model.gemspec +4 -1
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
- data/spec/lutaml/model/attribute_spec.rb +8 -3
- data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
- data/spec/lutaml/model/defaults_spec.rb +45 -0
- data/spec/lutaml/model/json_spec.rb +1 -2
- data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
- data/spec/lutaml/model/union_attribute_spec.rb +4 -2
- data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
- data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
- metadata +5 -3
|
@@ -183,6 +183,34 @@ module Lutaml
|
|
|
183
183
|
result
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
# Match names an attribute rule consumes during parsing, mirroring how
|
|
187
|
+
# serialization qualifies the same attribute (resolve_namespace):
|
|
188
|
+
# a rule serialized with a prefix matches only attributes in its
|
|
189
|
+
# namespace; a rule serialized unprefixed matches only namespace-less
|
|
190
|
+
# attributes.
|
|
191
|
+
#
|
|
192
|
+
# Performance: memoized on the rule with zero-allocation cache hits.
|
|
193
|
+
# The inputs are constant for a given rule, register and parent model,
|
|
194
|
+
# so every element of a collection reuses the first resolution.
|
|
195
|
+
def attribute_match_names(attr, register, parent_ns_class, form_default,
|
|
196
|
+
default_namespace)
|
|
197
|
+
if defined?(@_amn_names) && @_amn_register == register &&
|
|
198
|
+
@_amn_parent_ns == parent_ns_class &&
|
|
199
|
+
@_amn_form_default == form_default &&
|
|
200
|
+
@_amn_default_ns == default_namespace
|
|
201
|
+
return @_amn_names
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
result = compute_attribute_match_names(attr, register, parent_ns_class,
|
|
205
|
+
form_default, default_namespace)
|
|
206
|
+
@_amn_register = register
|
|
207
|
+
@_amn_parent_ns = parent_ns_class
|
|
208
|
+
@_amn_form_default = form_default
|
|
209
|
+
@_amn_default_ns = default_namespace
|
|
210
|
+
@_amn_names = result
|
|
211
|
+
result
|
|
212
|
+
end
|
|
213
|
+
|
|
186
214
|
def namespaced_name(parent_namespace = nil, name = self.name)
|
|
187
215
|
if name.to_s == "lang"
|
|
188
216
|
::Lutaml::Model::Utils.blank?(prefix) ? name.to_s : "#{prefix}:#{name}"
|
|
@@ -274,6 +302,30 @@ module Lutaml
|
|
|
274
302
|
|
|
275
303
|
private
|
|
276
304
|
|
|
305
|
+
# Extracted body of attribute_match_names for caching
|
|
306
|
+
def compute_attribute_match_names(attr, register, parent_ns_class,
|
|
307
|
+
form_default, default_namespace)
|
|
308
|
+
ns_info = resolve_namespace(
|
|
309
|
+
attr: attr,
|
|
310
|
+
register: register,
|
|
311
|
+
parent_ns_class: parent_ns_class,
|
|
312
|
+
form_default: form_default,
|
|
313
|
+
)
|
|
314
|
+
uri = ns_info[:uri]
|
|
315
|
+
# Schema-level attribute_form_default keeps the historical plain-name
|
|
316
|
+
# parse: dependents (e.g. ogc-gml) declare :qualified but their
|
|
317
|
+
# documents and expectations rely on unprefixed attributes parsing.
|
|
318
|
+
# Explicit namespaces, type namespaces and explicit form: rules
|
|
319
|
+
# mirror serialization strictly.
|
|
320
|
+
strict_match = uri && ns_info[:prefix] &&
|
|
321
|
+
!ns_info[:unqualified_same_ns] && !ns_info[:via_schema_default]
|
|
322
|
+
if strict_match
|
|
323
|
+
["#{uri}:#{name}"].freeze
|
|
324
|
+
else
|
|
325
|
+
namespaced_names(default_namespace)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
277
329
|
# Extracted body of namespaced_names for caching
|
|
278
330
|
def compute_namespaced_names(parent_namespace)
|
|
279
331
|
if multiple_mappings?
|
|
@@ -427,7 +479,9 @@ form_default = :unqualified)
|
|
|
427
479
|
# When schema specifies attributeFormDefault="qualified", attributes
|
|
428
480
|
# must be qualified (prefixed) with the parent element's namespace
|
|
429
481
|
if form_default == :qualified && parent_ns_class
|
|
430
|
-
|
|
482
|
+
result = build_namespace_result_from_class(parent_ns_class)
|
|
483
|
+
result[:via_schema_default] = true
|
|
484
|
+
return result
|
|
431
485
|
end
|
|
432
486
|
|
|
433
487
|
# 4. No namespace (W3C default for unprefixed attributes)
|
|
@@ -509,8 +509,13 @@ _effective_register)
|
|
|
509
509
|
local_name = rn[(last_colon_index + 1)..]
|
|
510
510
|
|
|
511
511
|
matched_attr = doc.root.attributes.values.find do |attr|
|
|
512
|
-
#
|
|
513
|
-
#
|
|
512
|
+
# A namespace-less, prefix-less attribute cannot belong to the
|
|
513
|
+
# rule's namespace, so it must not satisfy the fallback
|
|
514
|
+
# (lutaml-model#744). Attributes with undeclared prefixes keep
|
|
515
|
+
# their prefix in the raw name and stay eligible below.
|
|
516
|
+
next false if attr.namespace.nil? &&
|
|
517
|
+
attr.namespace_prefix.nil? && !attr.name.include?(":")
|
|
518
|
+
|
|
514
519
|
attr.namespaced_name == local_name ||
|
|
515
520
|
attr.unprefixed_name == local_name ||
|
|
516
521
|
(attr.namespace.nil? && ((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name)
|
|
@@ -544,9 +549,14 @@ _effective_register)
|
|
|
544
549
|
rule_namespace = rule_namespace_set ? rule.namespace : nil
|
|
545
550
|
options[:default_namespace]
|
|
546
551
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
552
|
+
rule_names = if rule.attribute?
|
|
553
|
+
resolve_attribute_rule_names(rule, attr, options,
|
|
554
|
+
effective_register,
|
|
555
|
+
instance, instance_is_serialize)
|
|
556
|
+
else
|
|
557
|
+
resolve_rule_names_with_type(rule, attr, options,
|
|
558
|
+
effective_register, attr_type)
|
|
559
|
+
end
|
|
550
560
|
|
|
551
561
|
return value_for_xml_attribute(doc, rule, rule_names) if rule.attribute?
|
|
552
562
|
|
|
@@ -879,6 +889,44 @@ effective_register = lutaml_register)
|
|
|
879
889
|
end
|
|
880
890
|
end
|
|
881
891
|
|
|
892
|
+
# Match names for an attribute rule, mirroring how serialization
|
|
893
|
+
# qualifies the same attribute (MappingRule#attribute_match_names):
|
|
894
|
+
# a rule serialized with a prefix matches only attributes in its
|
|
895
|
+
# namespace; a rule serialized unprefixed matches only namespace-less
|
|
896
|
+
# attributes.
|
|
897
|
+
def resolve_attribute_rule_names(rule, attr, options, effective_register,
|
|
898
|
+
instance, instance_is_serialize)
|
|
899
|
+
return rule.namespaced_names(options[:default_namespace]) if attr.nil? ||
|
|
900
|
+
rule.multiple_mappings?
|
|
901
|
+
|
|
902
|
+
if instance_is_serialize
|
|
903
|
+
parent_ns_class, form_default = parent_attribute_context(instance)
|
|
904
|
+
else
|
|
905
|
+
parent_ns_class = nil
|
|
906
|
+
form_default = :unqualified
|
|
907
|
+
end
|
|
908
|
+
rule.attribute_match_names(attr, effective_register, parent_ns_class,
|
|
909
|
+
form_default,
|
|
910
|
+
options[:default_namespace])
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
# [namespace_class, attribute_form_default] for the instance's model,
|
|
914
|
+
# keyed on the model class. Attribute rules of one element share the
|
|
915
|
+
# model, so a 1-entry cache avoids a mappings_for lookup per rule.
|
|
916
|
+
def parent_attribute_context(instance)
|
|
917
|
+
klass = instance.class
|
|
918
|
+
if @parent_ns_klass == klass && @parent_ns_value
|
|
919
|
+
return @parent_ns_value
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
ns_class = klass.mappings_for(:xml)&.namespace_class
|
|
923
|
+
value = [ns_class,
|
|
924
|
+
ns_class&.attribute_form_default || :unqualified]
|
|
925
|
+
@parent_ns_klass = klass
|
|
926
|
+
@parent_ns_value = value
|
|
927
|
+
value
|
|
928
|
+
end
|
|
929
|
+
|
|
882
930
|
# Resolve rule names with type namespace support
|
|
883
931
|
#
|
|
884
932
|
# @param rule [MappingRule] the mapping rule
|
|
@@ -141,9 +141,7 @@ module Lutaml
|
|
|
141
141
|
# Trigger autoloads for all XSD model classes to ensure they are
|
|
142
142
|
# registered before parsing begins. This is necessary because
|
|
143
143
|
# type resolution during parsing looks up classes in the register.
|
|
144
|
-
# rubocop:disable Style/RedundantFetchOverride
|
|
145
144
|
XSD_AUTOLOAD_CLASSES.each { |c| const_get(c) unless c == :VERSION }
|
|
146
|
-
# rubocop:enable Style/RedundantFetchOverride
|
|
147
145
|
|
|
148
146
|
# Validate XSD schema structure before parsing (unless disabled)
|
|
149
147
|
if validate_schema && !nested_schema
|
|
@@ -410,7 +410,8 @@ attr_name)
|
|
|
410
410
|
def self.get_attribute_xsd_type(attr, attr_type, register,
|
|
411
411
|
_mapping_rule = nil)
|
|
412
412
|
if attr.union?
|
|
413
|
-
raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name,
|
|
413
|
+
raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name,
|
|
414
|
+
"XSD")
|
|
414
415
|
end
|
|
415
416
|
|
|
416
417
|
# 1. Check for deprecated attribute-level xsd_type override
|
|
@@ -143,7 +143,8 @@ parent_element_form_default)
|
|
|
143
143
|
# Get transformation for the actual class. Unions resolve the member
|
|
144
144
|
# from the value's own class, like polymorphism.
|
|
145
145
|
child_transformation = if union
|
|
146
|
-
value.class.transformation_for(:xml,
|
|
146
|
+
value.class.transformation_for(:xml,
|
|
147
|
+
register)
|
|
147
148
|
elsif is_polymorphic || is_polymorphic_subtype
|
|
148
149
|
actual_class.transformation_for(:xml,
|
|
149
150
|
register)
|
|
@@ -326,7 +326,8 @@ compiled_rules, mapping, processed_text_nodes)
|
|
|
326
326
|
compiled_rules
|
|
327
327
|
end
|
|
328
328
|
|
|
329
|
-
emitted_counts = element_order_coverage(model_instance,
|
|
329
|
+
emitted_counts = element_order_coverage(model_instance,
|
|
330
|
+
compiled_rules)
|
|
330
331
|
|
|
331
332
|
rules_to_apply.each do |rule|
|
|
332
333
|
mapping_type = rule.option(:mapping_type)
|
data/lutaml-model.gemspec
CHANGED
|
@@ -18,7 +18,10 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
|
|
19
19
|
spec.bindir = "exe"
|
|
20
20
|
spec.require_paths = ["lib"]
|
|
21
|
-
|
|
21
|
+
# lib/ uses anonymous argument forwarding in call position, which parses
|
|
22
|
+
# only on Ruby 3.2+ (verified: 6 files fail to parse on 3.1). Without this
|
|
23
|
+
# floor, bundler resolves 0.8.x on 3.1 and it dies at require time.
|
|
24
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
|
22
25
|
|
|
23
26
|
# Specify which files should be added to the gem when it is released.
|
|
24
27
|
# The `git ls-files -z` loads the files in the
|
|
@@ -18,7 +18,8 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
18
18
|
|
|
19
19
|
it "maps nil through value_map[:nil] => :empty to [] (collection)" do
|
|
20
20
|
expect(
|
|
21
|
-
attribute(:string, collection: true).apply_value_map(nil,
|
|
21
|
+
attribute(:string, collection: true).apply_value_map(nil,
|
|
22
|
+
{ nil: :empty }),
|
|
22
23
|
).to eq([])
|
|
23
24
|
end
|
|
24
25
|
|
|
@@ -34,13 +35,15 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
34
35
|
custom_collection_class = Class.new(Lutaml::Model::Collection) do
|
|
35
36
|
instances :items, member_class
|
|
36
37
|
end
|
|
37
|
-
attr = described_class.new("items", member_class,
|
|
38
|
+
attr = described_class.new("items", member_class,
|
|
39
|
+
collection: custom_collection_class)
|
|
38
40
|
result = attr.apply_value_map(nil, { nil: :empty })
|
|
39
41
|
expect(result).to be_a(custom_collection_class)
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
it "maps nil through value_map[:nil] => :omitted to UninitializedClass" do
|
|
43
|
-
expect(attribute(:string).apply_value_map(nil,
|
|
45
|
+
expect(attribute(:string).apply_value_map(nil,
|
|
46
|
+
{ nil: :omitted })).to eq(uninit)
|
|
44
47
|
end
|
|
45
48
|
end
|
|
46
49
|
|
|
@@ -50,12 +53,14 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
it "maps '' through value_map[:empty] => :empty preserving '' for scalar" do
|
|
53
|
-
expect(attribute(:string).apply_value_map("",
|
|
56
|
+
expect(attribute(:string).apply_value_map("",
|
|
57
|
+
{ empty: :empty })).to eq("")
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
it "maps [] through value_map[:empty] => :empty preserving [] for collection" do
|
|
57
61
|
expect(
|
|
58
|
-
attribute(:string, collection: true).apply_value_map([],
|
|
62
|
+
attribute(:string, collection: true).apply_value_map([],
|
|
63
|
+
{ empty: :empty }),
|
|
59
64
|
).to eq([])
|
|
60
65
|
end
|
|
61
66
|
|
|
@@ -69,11 +74,13 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
69
74
|
|
|
70
75
|
describe "omitted handling" do
|
|
71
76
|
it "maps UninitializedClass through value_map[:omitted] => :nil" do
|
|
72
|
-
expect(attribute(:string).apply_value_map(uninit,
|
|
77
|
+
expect(attribute(:string).apply_value_map(uninit,
|
|
78
|
+
{ omitted: :nil })).to be_nil
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
it "maps UninitializedClass through value_map[:omitted] => :empty" do
|
|
76
|
-
expect(attribute(:string).apply_value_map(uninit,
|
|
82
|
+
expect(attribute(:string).apply_value_map(uninit,
|
|
83
|
+
{ omitted: :empty })).to eq("")
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
it "does NOT route UninitializedClass through the :empty branch" do
|
|
@@ -81,18 +88,21 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
81
88
|
# utils.rb:132-137. Ensure dispatch order matches the originals: uninit
|
|
82
89
|
# values go to :omitted, not :empty.
|
|
83
90
|
expect(
|
|
84
|
-
attribute(:string).apply_value_map(uninit,
|
|
91
|
+
attribute(:string).apply_value_map(uninit,
|
|
92
|
+
{ empty: :nil, omitted: :empty }),
|
|
85
93
|
).to eq("")
|
|
86
94
|
end
|
|
87
95
|
end
|
|
88
96
|
|
|
89
97
|
describe "Boolean bare-form (Boolean attribute type required)" do
|
|
90
98
|
it "returns false directly for Boolean attribute, value_map[:empty] = false" do
|
|
91
|
-
expect(attribute(:boolean).apply_value_map("",
|
|
99
|
+
expect(attribute(:boolean).apply_value_map("",
|
|
100
|
+
{ empty: false })).to be(false)
|
|
92
101
|
end
|
|
93
102
|
|
|
94
103
|
it "returns true directly for Boolean attribute, value_map[:omitted] = true" do
|
|
95
|
-
expect(attribute(:boolean).apply_value_map(uninit,
|
|
104
|
+
expect(attribute(:boolean).apply_value_map(uninit,
|
|
105
|
+
{ omitted: true })).to be(true)
|
|
96
106
|
end
|
|
97
107
|
|
|
98
108
|
it "does NOT apply bare-boolean form to a non-Boolean attribute (documented tightening)" do
|
|
@@ -106,15 +116,18 @@ RSpec.describe Lutaml::Model::Attribute, "#apply_value_map" do
|
|
|
106
116
|
|
|
107
117
|
describe "Boolean nested-form (value_map[:from][:empty] / [:omitted])" do
|
|
108
118
|
it "returns false from value_map[:from][:empty] unconditionally for Boolean attribute" do
|
|
109
|
-
expect(attribute(:boolean).apply_value_map("",
|
|
119
|
+
expect(attribute(:boolean).apply_value_map("",
|
|
120
|
+
{ from: { empty: false } })).to be(false)
|
|
110
121
|
end
|
|
111
122
|
|
|
112
123
|
it "returns true from value_map[:from][:omitted] unconditionally for Boolean attribute" do
|
|
113
|
-
expect(attribute(:boolean).apply_value_map(uninit,
|
|
124
|
+
expect(attribute(:boolean).apply_value_map(uninit,
|
|
125
|
+
{ from: { omitted: true } })).to be(true)
|
|
114
126
|
end
|
|
115
127
|
|
|
116
128
|
it "returns false from nested form even for non-Boolean attribute (no type gate)" do
|
|
117
|
-
expect(attribute(:string).apply_value_map("",
|
|
129
|
+
expect(attribute(:string).apply_value_map("",
|
|
130
|
+
{ from: { empty: false } })).to be(false)
|
|
118
131
|
end
|
|
119
132
|
end
|
|
120
133
|
|
|
@@ -358,7 +358,8 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
358
358
|
it "executes the proc in the instance context (reads instance attribute)" do
|
|
359
359
|
instance = model_class.new(name: "Alice")
|
|
360
360
|
attribute = model_class.attributes[:greeting]
|
|
361
|
-
expect(attribute.default_value(register,
|
|
361
|
+
expect(attribute.default_value(register,
|
|
362
|
+
instance)).to eq("Hello, Alice")
|
|
362
363
|
end
|
|
363
364
|
end
|
|
364
365
|
end
|
|
@@ -413,7 +414,9 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
413
414
|
end
|
|
414
415
|
instance = model_class.new(prefix: "Dr")
|
|
415
416
|
attr = model_class.attributes[:name]
|
|
416
|
-
expect
|
|
417
|
+
expect do
|
|
418
|
+
attr.validate_value!(nil, register, instance_object: instance)
|
|
419
|
+
end
|
|
417
420
|
.to raise_error(Lutaml::Model::InvalidValueError) do |error|
|
|
418
421
|
# The error carries the value that was used for validation.
|
|
419
422
|
# If instance_exec ran, value is "Dr_default"; if not, error
|
|
@@ -427,7 +430,9 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
427
430
|
it "short-circuits without raising" do
|
|
428
431
|
uninit = Lutaml::Model::UninitializedClass.instance
|
|
429
432
|
attr = described_class.new("name", :string)
|
|
430
|
-
expect
|
|
433
|
+
expect do
|
|
434
|
+
attr.validate_value!(uninit, register, instance_object: model)
|
|
435
|
+
end
|
|
431
436
|
.not_to raise_error
|
|
432
437
|
end
|
|
433
438
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#746: an empty value ("", [], {}) is a PRESENT value per
|
|
6
|
+
# docs/_guides/missing-values-handling.adoc and must reach custom `from`
|
|
7
|
+
# methods. Only nil (non-existent) and an omitted key (undefined) skip them.
|
|
8
|
+
module CustomFromEmptyValuesSpec
|
|
9
|
+
class Tagged < Lutaml::Model::Serializable
|
|
10
|
+
attribute :received, :string
|
|
11
|
+
|
|
12
|
+
key_value do
|
|
13
|
+
map "tags", to: :received, with: { to: :tags_to, from: :tags_from }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tags_to(model, doc)
|
|
17
|
+
doc["tags"] = model.received
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tags_from(model, value)
|
|
21
|
+
model.received = value.inspect
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Mapped < Lutaml::Model::Serializable
|
|
26
|
+
attribute :received, :string
|
|
27
|
+
|
|
28
|
+
key_value do
|
|
29
|
+
map "fields", to: :received, with: { to: :fields_to, from: :fields_from }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fields_to(model, doc)
|
|
33
|
+
doc["fields"] = model.received
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fields_from(model, value)
|
|
37
|
+
model.received = value.inspect
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
RSpec.describe "custom from methods and empty values" do
|
|
43
|
+
it "receives an empty collection" do
|
|
44
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags: []").received)
|
|
45
|
+
.to eq("[]")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "receives an empty string" do
|
|
49
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml('tags: ""').received)
|
|
50
|
+
.to eq("\"\"")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "receives an empty mapping" do
|
|
54
|
+
expect(CustomFromEmptyValuesSpec::Mapped.from_yaml("fields: {}").received)
|
|
55
|
+
.to eq("{}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "receives empty values from JSON too" do
|
|
59
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": []})).received)
|
|
60
|
+
.to eq("[]")
|
|
61
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_json(%({"tags": ""})).received)
|
|
62
|
+
.to eq("\"\"")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "still skips the method for a nil value" do
|
|
66
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("tags:").received)
|
|
67
|
+
.to be_nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "still skips the method for an omitted key" do
|
|
71
|
+
expect(CustomFromEmptyValuesSpec::Tagged.from_yaml("{}").received)
|
|
72
|
+
.to be_nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -146,6 +146,33 @@ module DefaultsSpec
|
|
|
146
146
|
|
|
147
147
|
class InstanceAwareGrandChild < InstanceAwareChild
|
|
148
148
|
end
|
|
149
|
+
|
|
150
|
+
# Class-method defaults must keep resolving as they did before 0.8.0
|
|
151
|
+
# (lutaml-model#745): default lambdas are evaluated against the instance,
|
|
152
|
+
# with the model class as fallback for methods the instance does not have.
|
|
153
|
+
class ClassMethodDefault < Lutaml::Model::Serializable
|
|
154
|
+
attribute :os, :string, default: -> { detect_os }
|
|
155
|
+
|
|
156
|
+
def self.detect_os
|
|
157
|
+
"from-class-method"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class ClassMethodDefaultWithArgs < Lutaml::Model::Serializable
|
|
162
|
+
attribute :label, :string, default: -> { build_label("x") }
|
|
163
|
+
|
|
164
|
+
def self.build_label(prefix)
|
|
165
|
+
"#{prefix}-label"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
class InstanceMethodDefault < Lutaml::Model::Serializable
|
|
170
|
+
attribute :value, :string, default: -> { instance_helper }
|
|
171
|
+
|
|
172
|
+
def instance_helper
|
|
173
|
+
"from-instance-method"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
149
176
|
end
|
|
150
177
|
|
|
151
178
|
RSpec.describe DefaultsSpec::Glaze do
|
|
@@ -346,3 +373,21 @@ RSpec.describe DefaultsSpec::Glaze do
|
|
|
346
373
|
end
|
|
347
374
|
end
|
|
348
375
|
end
|
|
376
|
+
|
|
377
|
+
RSpec.describe DefaultsSpec::ClassMethodDefault do
|
|
378
|
+
it "resolves class methods from default lambdas" do
|
|
379
|
+
expect(described_class.new.os).to eq("from-class-method")
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
RSpec.describe DefaultsSpec::ClassMethodDefaultWithArgs do
|
|
384
|
+
it "passes arguments through to class methods" do
|
|
385
|
+
expect(described_class.new.label).to eq("x-label")
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
RSpec.describe DefaultsSpec::InstanceMethodDefault do
|
|
390
|
+
it "still resolves instance methods in default lambdas" do
|
|
391
|
+
expect(described_class.new.value).to eq("from-instance-method")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -8,9 +8,8 @@ RSpec.describe Lutaml::Model::Json do
|
|
|
8
8
|
let(:lib_path) { File.expand_path("../../../lib", __dir__) }
|
|
9
9
|
|
|
10
10
|
it "loads require 'lutaml/json' without preloading lutaml/model" do
|
|
11
|
-
# rubocop:disable Style/CommandLiteral
|
|
11
|
+
# rubocop:disable-next Style/CommandLiteral
|
|
12
12
|
result = `#{RbConfig.ruby} -I#{lib_path} -e 'require "lutaml/json"; puts :ok' 2>&1`
|
|
13
|
-
# rubocop:enable Style/CommandLiteral
|
|
14
13
|
|
|
15
14
|
expect($?.success?).to be(true), result
|
|
16
15
|
expect(result).to include("ok")
|
|
@@ -112,7 +112,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
|
|
|
112
112
|
<items>c</items>
|
|
113
113
|
</container>
|
|
114
114
|
XML
|
|
115
|
-
expect(obj.element_order.map(&:name)).to eq(%w[singular items items
|
|
115
|
+
expect(obj.element_order.map(&:name)).to eq(%w[singular items items
|
|
116
|
+
items])
|
|
116
117
|
end
|
|
117
118
|
|
|
118
119
|
it "all direct setters: emits in call order" do
|
|
@@ -298,7 +299,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
|
|
|
298
299
|
# so both assignments are recorded. The serializer emits the current
|
|
299
300
|
# value of the attribute via element_order in recorded order; the
|
|
300
301
|
# safety net guarantees no data loss.
|
|
301
|
-
expect(obj.element_order.map(&:name)).to eq(%w[items items items items
|
|
302
|
+
expect(obj.element_order.map(&:name)).to eq(%w[items items items items
|
|
303
|
+
items])
|
|
302
304
|
xml = obj.to_xml
|
|
303
305
|
# The serialized output must contain all current items
|
|
304
306
|
expect(xml).to include("<items>c</items>")
|
|
@@ -317,7 +319,8 @@ RSpec.describe "Lutaml::Model::Serialize::Builder" do
|
|
|
317
319
|
xml = obj.to_xml
|
|
318
320
|
|
|
319
321
|
if !obj.singular.nil? && !obj.singular.to_s.empty?
|
|
320
|
-
expect(xml).to include("<singular>"),
|
|
322
|
+
expect(xml).to include("<singular>"),
|
|
323
|
+
"singular was dropped from output"
|
|
321
324
|
end
|
|
322
325
|
return if obj.items.nil? || obj.items.empty?
|
|
323
326
|
|
|
@@ -33,7 +33,8 @@ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
it "passes initialized values through unchanged" do
|
|
36
|
-
expect(transform.apply_value_map("hello", default_vmap,
|
|
36
|
+
expect(transform.apply_value_map("hello", default_vmap,
|
|
37
|
+
nil)).to eq("hello")
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -50,11 +51,13 @@ RSpec.describe Lutaml::Model::Transform, "#apply_value_map (nil attr)" do
|
|
|
50
51
|
# custom method runs), and resolved :nil/:empty consistently for empty inputs.
|
|
51
52
|
describe "non-:nil options with nil attr" do
|
|
52
53
|
it "maps a nil value to uninitialized via nil: :omitted" do
|
|
53
|
-
expect(transform.apply_value_map(nil, { nil: :omitted },
|
|
54
|
+
expect(transform.apply_value_map(nil, { nil: :omitted },
|
|
55
|
+
nil)).to eq(uninit)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
it "maps an empty value to uninitialized via empty: :omitted" do
|
|
57
|
-
expect(transform.apply_value_map("", { empty: :omitted },
|
|
59
|
+
expect(transform.apply_value_map("", { empty: :omitted },
|
|
60
|
+
nil)).to eq(uninit)
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
it "maps an empty value to nil via empty: :nil" do
|
|
@@ -311,7 +311,8 @@ RSpec.describe "Union-typed attributes (issue #190)" do
|
|
|
311
311
|
it "serializes a model member built in plain Ruby identically to a deserialized one" do
|
|
312
312
|
built = UnionAttributeSpec::Ceramic.new(
|
|
313
313
|
firing_temperature:
|
|
314
|
-
UnionAttributeSpec::TemperatureWithUnit.new(number: 1300.0,
|
|
314
|
+
UnionAttributeSpec::TemperatureWithUnit.new(number: 1300.0,
|
|
315
|
+
unit: "C"),
|
|
315
316
|
)
|
|
316
317
|
round = UnionAttributeSpec::Ceramic.from_yaml(built.to_yaml)
|
|
317
318
|
expect(round.firing_temperature)
|
|
@@ -424,7 +425,8 @@ RSpec.describe "Union-typed attributes (issue #190)" do
|
|
|
424
425
|
end
|
|
425
426
|
|
|
426
427
|
it "rejects unsupported scalar member types at definition time" do
|
|
427
|
-
%i[hash symbol time time_without_date date
|
|
428
|
+
%i[hash symbol time time_without_date date
|
|
429
|
+
date_time].each do |unsupported|
|
|
428
430
|
expect do
|
|
429
431
|
Class.new(Lutaml::Model::Serializable) do
|
|
430
432
|
attribute :bad, [unsupported, :string]
|