lutaml-model 0.8.17 → 0.8.18
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/.github/workflows/downstream-performance.yml +27 -1
- data/.github/workflows/js-pr-check.yml +102 -0
- data/.github/workflows/js-sync.yml +51 -0
- data/.github/workflows/opal.yml +26 -6
- data/.github/workflows/performance.yml +32 -7
- data/.github/workflows/rake.yml +78 -32
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +8 -0
- data/Gemfile +16 -1
- data/README.adoc +267 -0
- data/Rakefile +166 -4
- data/docs/_guides/schema-import.adoc +42 -0
- data/docs/_guides/xml-mapping.adoc +21 -0
- data/docs/_pages/validation.adoc +65 -0
- data/lib/compat/opal/generate_boot.rb +123 -0
- data/lib/compat/opal/js_bundle_entry.rb +42 -0
- data/lib/compat/opal/lutaml_model_boot.rb +497 -0
- data/lib/compat/opal/moxml_boot.rb +68 -0
- data/lib/compat/opal/yaml_compat.rb +32 -0
- data/lib/lutaml/json.rb +1 -1
- data/lib/lutaml/key_value/transform.rb +3 -8
- data/lib/lutaml/key_value/transformation/value_serializer.rb +14 -1
- data/lib/lutaml/key_value/transformation.rb +17 -5
- data/lib/lutaml/model/adapter_resolver.rb +2 -2
- data/lib/lutaml/model/attribute.rb +88 -11
- data/lib/lutaml/model/cached_type_resolver.rb +1 -1
- data/lib/lutaml/model/error/union_schema_unsupported_error.rb +11 -0
- data/lib/lutaml/model/global_context.rb +11 -8
- data/lib/lutaml/model/instrumentation.rb +2 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +0 -6
- data/lib/lutaml/model/register.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +50 -0
- data/lib/lutaml/model/schema/class_loader.rb +58 -0
- data/lib/lutaml/model/schema/compiled_output.rb +83 -0
- data/lib/lutaml/model/schema/definitions/attribute.rb +32 -0
- data/lib/lutaml/model/schema/definitions/choice.rb +20 -0
- data/lib/lutaml/model/schema/definitions/facet.rb +40 -0
- data/lib/lutaml/model/schema/definitions/group_import.rb +22 -0
- data/lib/lutaml/model/schema/definitions/member_walk.rb +33 -0
- data/lib/lutaml/model/schema/definitions/model.rb +40 -0
- data/lib/lutaml/model/schema/definitions/namespace.rb +23 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +30 -0
- data/lib/lutaml/model/schema/definitions/sequence.rb +19 -0
- data/lib/lutaml/model/schema/definitions/simple_content.rb +22 -0
- data/lib/lutaml/model/schema/definitions/transform_facet.rb +21 -0
- data/lib/lutaml/model/schema/definitions/type_ref.rb +20 -0
- data/lib/lutaml/model/schema/definitions/union_type.rb +28 -0
- data/lib/lutaml/model/schema/definitions/xml_root.rb +20 -0
- data/lib/lutaml/model/schema/definitions.rb +24 -0
- data/lib/lutaml/model/schema/file_writer.rb +70 -0
- data/lib/lutaml/model/schema/generator/definitions_collection.rb +16 -2
- data/lib/lutaml/model/schema/generator/property.rb +21 -4
- data/lib/lutaml/model/schema/module_nesting.rb +31 -0
- data/lib/lutaml/model/schema/namespace_naming.rb +46 -0
- data/lib/lutaml/model/schema/registry_generator.rb +115 -0
- data/lib/lutaml/model/schema/renderers/base.rb +40 -0
- data/lib/lutaml/model/schema/renderers/mappings.rb +72 -0
- data/lib/lutaml/model/schema/renderers/member_decls.rb +115 -0
- data/lib/lutaml/model/schema/renderers/model.rb +121 -0
- data/lib/lutaml/model/schema/renderers/namespace.rb +30 -0
- data/lib/lutaml/model/schema/renderers/registration.rb +64 -0
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +93 -0
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +88 -0
- data/lib/lutaml/model/schema/renderers/union.rb +83 -0
- data/lib/lutaml/model/schema/renderers.rb +19 -0
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +47 -0
- data/lib/lutaml/model/schema/rnc_compiler.rb +62 -0
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +91 -0
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +392 -0
- data/lib/lutaml/model/schema/rng_compiler/member_collector.rb +30 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +152 -0
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +125 -0
- data/lib/lutaml/model/schema/rng_compiler.rb +220 -0
- data/lib/lutaml/model/schema/templates.rb +162 -0
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +41 -0
- data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +31 -107
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +191 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +216 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +153 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +167 -0
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +87 -0
- data/lib/lutaml/model/schema/xml_compiler.rb +38 -517
- data/lib/lutaml/model/schema.rb +19 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +14 -2
- data/lib/lutaml/model/serialize/builder.rb +15 -2
- data/lib/lutaml/model/serialize/value_mapping.rb +9 -57
- data/lib/lutaml/model/serialize.rb +6 -9
- data/lib/lutaml/model/services/rule_value_extractor.rb +2 -3
- data/lib/lutaml/model/services.rb +0 -2
- data/lib/lutaml/model/store.rb +2 -1
- data/lib/lutaml/model/toml.rb +1 -1
- data/lib/lutaml/model/transform.rb +17 -39
- data/lib/lutaml/model/transformation_registry.rb +1 -1
- data/lib/lutaml/model/type.rb +1 -0
- data/lib/lutaml/model/union.rb +320 -0
- data/lib/lutaml/model/utils.rb +6 -0
- data/lib/lutaml/model/validation.rb +1 -4
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +18 -0
- data/lib/lutaml/toml.rb +1 -1
- data/lib/lutaml/xml/adapter.rb +1 -1
- data/lib/lutaml/xml/adapter_loader.rb +1 -1
- data/lib/lutaml/xml/builder/base.rb +1 -1
- data/lib/lutaml/xml/builder.rb +1 -1
- data/lib/lutaml/xml/error/schema_validation_error.rb +25 -0
- data/lib/lutaml/xml/model_transform.rb +5 -1
- data/lib/lutaml/xml/schema/relaxng_schema.rb +5 -0
- data/lib/lutaml/xml/schema/xsd_schema.rb +4 -0
- data/lib/lutaml/xml/serialization/format_conversion.rb +48 -0
- data/lib/lutaml/xml/serialization/instance_methods.rb +13 -0
- data/lib/lutaml/xml/transformation/element_builder.rb +21 -8
- data/lib/lutaml/xml/transformation/ordered_applier.rb +6 -0
- data/lib/lutaml/xml/xsd_validator.rb +66 -0
- data/lib/lutaml/xml.rb +53 -12
- data/lutaml-model.gemspec +4 -2
- data/spec/fixtures/xml/schema/rnc/address_book.rnc +15 -0
- data/spec/fixtures/xml/schema/rnc/book_features.rnc +18 -0
- data/spec/fixtures/xml/schema/rnc/includes/book.rnc +6 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_a.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/circular_b.rnc +8 -0
- data/spec/fixtures/xml/schema/rnc/includes/library.rnc +8 -0
- data/spec/fixtures/xml/schema/rng/address_book.rng +16 -0
- data/spec/fixtures/xml/schema/rng/book.rng +27 -0
- data/spec/fixtures/xml/schema/rng/fixed_value.rng +11 -0
- data/spec/fixtures/xml/schema/rng/fragment_in_choice.rng +30 -0
- data/spec/fixtures/xml/schema/rng/full_name.rng +39 -0
- data/spec/fixtures/xml/schema/rng/integer_range.rng +32 -0
- data/spec/fixtures/xml/schema/rng/list_type.rng +12 -0
- data/spec/fixtures/xml/schema/rng/namespaced.rng +9 -0
- data/spec/fixtures/xml/schema/rng/paragraph.rng +13 -0
- data/spec/fixtures/xml/schema/rng/person.rng +23 -0
- data/spec/fixtures/xml/schema/rng/union.rng +14 -0
- data/spec/fixtures/xml/validate_xml_with_address.xsd +13 -0
- data/spec/fixtures/xml/validate_xml_with_contact.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person.xsd +11 -0
- data/spec/fixtures/xml/validate_xml_with_person_strict.xsd +17 -0
- data/spec/fixtures/xml/validate_xml_with_product.xsd +37 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +134 -0
- data/spec/lutaml/model/attribute_spec.rb +169 -12
- data/spec/lutaml/model/boot_manifest_spec.rb +30 -0
- data/spec/lutaml/model/collection_spec.rb +18 -0
- data/spec/lutaml/model/mixed_content_spec.rb +171 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +67 -2
- data/spec/lutaml/model/ordered_content_spec.rb +30 -0
- data/spec/lutaml/model/raw_element_spec.rb +125 -0
- data/spec/lutaml/model/rule_value_extractor_spec.rb +7 -14
- data/spec/lutaml/model/runtime_compatibility_spec.rb +25 -0
- data/spec/lutaml/model/schema/renderers/model_spec.rb +149 -0
- data/spec/lutaml/model/schema/renderers/namespace_spec.rb +36 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +75 -0
- data/spec/lutaml/model/schema/renderers/union_spec.rb +58 -0
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +64 -0
- data/spec/lutaml/model/union_attribute_spec.rb +658 -0
- data/spec/lutaml/model/union_spec.rb +257 -0
- data/spec/lutaml/model/utils_spec.rb +14 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +22 -20
- data/spec/lutaml/xml/schema/compiler_spec.rb +21 -4
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +404 -0
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +893 -0
- data/spec/lutaml/xml/validate_xml_with_constructs_spec.rb +222 -0
- data/spec/lutaml/xml/validate_xml_with_spec.rb +186 -0
- data/spec/spec_helper.rb +28 -11
- data/spec/support/opal.rb +5 -2
- metadata +97 -31
- data/lib/lutaml/model/schema/xml_compiler/attribute.rb +0 -106
- data/lib/lutaml/model/schema/xml_compiler/attribute_group.rb +0 -47
- data/lib/lutaml/model/schema/xml_compiler/choice.rb +0 -67
- data/lib/lutaml/model/schema/xml_compiler/complex_content.rb +0 -27
- data/lib/lutaml/model/schema/xml_compiler/complex_content_restriction.rb +0 -34
- data/lib/lutaml/model/schema/xml_compiler/complex_type.rb +0 -173
- data/lib/lutaml/model/schema/xml_compiler/element.rb +0 -121
- data/lib/lutaml/model/schema/xml_compiler/group.rb +0 -113
- data/lib/lutaml/model/schema/xml_compiler/restriction.rb +0 -104
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -52
- data/lib/lutaml/model/schema/xml_compiler/simple_content.rb +0 -40
- data/lib/lutaml/model/schema/xml_compiler/simple_type.rb +0 -265
- data/lib/lutaml/model/schema/xml_compiler/xml_namespace_class.rb +0 -110
- data/lib/lutaml/model/services/default_value_resolver.rb +0 -60
- data/spec/lutaml/model/services/default_value_resolver_spec.rb +0 -162
- data/spec/lutaml/xml/schema/compiler/attribute_group_spec.rb +0 -71
- data/spec/lutaml/xml/schema/compiler/attribute_spec.rb +0 -67
- data/spec/lutaml/xml/schema/compiler/choice_spec.rb +0 -74
- data/spec/lutaml/xml/schema/compiler/complex_content_restriction_spec.rb +0 -60
- data/spec/lutaml/xml/schema/compiler/complex_content_spec.rb +0 -41
- data/spec/lutaml/xml/schema/compiler/complex_type_spec.rb +0 -202
- data/spec/lutaml/xml/schema/compiler/element_spec.rb +0 -68
- data/spec/lutaml/xml/schema/compiler/group_spec.rb +0 -90
- data/spec/lutaml/xml/schema/compiler/restriction_spec.rb +0 -77
- data/spec/lutaml/xml/schema/compiler/sequence_spec.rb +0 -65
- data/spec/lutaml/xml/schema/compiler/simple_content_spec.rb +0 -61
- data/spec/lutaml/xml/schema/compiler/simple_type_spec.rb +0 -184
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Behavioral contract for issue #190 union-typed attributes:
|
|
6
|
+
# - DSL: bare Array in type position -> attribute :x, [A, B, :string]
|
|
7
|
+
# - Semantics: xsd:union -> first-conforming member in declared order wins;
|
|
8
|
+
# valid if it conforms to at least one member (NO "exactly one" rule).
|
|
9
|
+
# - No-match: a real value matching no member becomes nil (like every failed
|
|
10
|
+
# cast); `required: true` then raises the standard RequiredAttributeMissingError.
|
|
11
|
+
# - Stateless serialization: the value's own class drives output, so a model
|
|
12
|
+
# built in plain Ruby serializes identically to a deserialized one.
|
|
13
|
+
module UnionAttributeSpec
|
|
14
|
+
# Type-only member (no root declared) — structured, keys {number, unit}.
|
|
15
|
+
class TemperatureWithUnit < Lutaml::Model::Serializable
|
|
16
|
+
attribute :number, :float
|
|
17
|
+
attribute :unit, :string
|
|
18
|
+
|
|
19
|
+
xml do
|
|
20
|
+
map_element "number", to: :number
|
|
21
|
+
map_element "unit", to: :unit
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
key_value do
|
|
25
|
+
map "number", to: :number
|
|
26
|
+
map "unit", to: :unit
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Type-only member — structured, disjoint key {celsius}.
|
|
31
|
+
class Temperature < Lutaml::Model::Serializable
|
|
32
|
+
attribute :celsius, :float
|
|
33
|
+
|
|
34
|
+
xml do
|
|
35
|
+
map_element "celsius", to: :celsius
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
key_value do
|
|
39
|
+
map "celsius", to: :celsius
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Mixed scalar + model union under a single shared element name.
|
|
44
|
+
class Ceramic < Lutaml::Model::Serializable
|
|
45
|
+
attribute :firing_temperature,
|
|
46
|
+
[TemperatureWithUnit, Temperature, :string]
|
|
47
|
+
|
|
48
|
+
xml do
|
|
49
|
+
element "ceramic"
|
|
50
|
+
map_element "FiringTemperature", to: :firing_temperature
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
key_value do
|
|
54
|
+
map "firing_temperature", to: :firing_temperature
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Scalar union with a catch-all — exercises first-conforming order.
|
|
59
|
+
class Reading < Lutaml::Model::Serializable
|
|
60
|
+
attribute :value, %i[integer string]
|
|
61
|
+
|
|
62
|
+
key_value do
|
|
63
|
+
map "value", to: :value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Model member whose mapping aliases one attribute to two keys.
|
|
68
|
+
class AliasedMember < Lutaml::Model::Serializable
|
|
69
|
+
attribute :name, :string
|
|
70
|
+
|
|
71
|
+
key_value do
|
|
72
|
+
map %w[name product_name], to: :name
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Union holding the aliased member (plus a scalar catch-all).
|
|
77
|
+
class AliasedHolder < Lutaml::Model::Serializable
|
|
78
|
+
attribute :value, [AliasedMember, :string]
|
|
79
|
+
|
|
80
|
+
key_value do
|
|
81
|
+
map "value", to: :value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Scalar union carrying enum + default constraints, for JSON Schema.
|
|
86
|
+
class ConstrainedReading < Lutaml::Model::Serializable
|
|
87
|
+
attribute :value, %i[integer string], values: [1, "one"], default: "one"
|
|
88
|
+
|
|
89
|
+
key_value do
|
|
90
|
+
map "value", to: :value
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Union with a :string member carrying a pattern — the pattern binds the
|
|
95
|
+
# string branch; a model member value is exempt.
|
|
96
|
+
class PatternedHolder < Lutaml::Model::Serializable
|
|
97
|
+
attribute :code, [Temperature, :string], pattern: /\A[A-Z]+\z/
|
|
98
|
+
|
|
99
|
+
key_value do
|
|
100
|
+
map "code", to: :code
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Union collection carrying a pattern — each string element is checked;
|
|
105
|
+
# non-string (integer) elements are exempt.
|
|
106
|
+
class PatternedList < Lutaml::Model::Serializable
|
|
107
|
+
attribute :codes, %i[integer string], collection: true,
|
|
108
|
+
pattern: /\A[A-Z]+\z/
|
|
109
|
+
|
|
110
|
+
key_value do
|
|
111
|
+
map "codes", to: :codes
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Scalar union, no catch-all — no-match yields nil (library default).
|
|
116
|
+
class OptionalReading < Lutaml::Model::Serializable
|
|
117
|
+
attribute :value, %i[integer float]
|
|
118
|
+
|
|
119
|
+
key_value do
|
|
120
|
+
map "value", to: :value
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Same, but required — no-match -> nil -> standard RequiredAttributeMissingError.
|
|
125
|
+
class RequiredReading < Lutaml::Model::Serializable
|
|
126
|
+
attribute :value, %i[integer float], required: true
|
|
127
|
+
|
|
128
|
+
key_value do
|
|
129
|
+
map "value", to: :value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Collection of union values (heterogeneous).
|
|
134
|
+
class ReadingSet < Lutaml::Model::Serializable
|
|
135
|
+
attribute :readings,
|
|
136
|
+
[TemperatureWithUnit, :string], collection: true
|
|
137
|
+
|
|
138
|
+
key_value do
|
|
139
|
+
map "readings", to: :readings
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Scalar union collection — for JSON Schema array-wrapping.
|
|
144
|
+
class IntStringList < Lutaml::Model::Serializable
|
|
145
|
+
attribute :vals, %i[integer string], collection: true
|
|
146
|
+
|
|
147
|
+
key_value do
|
|
148
|
+
map "vals", to: :vals
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# :decimal member — native BigDecimal + decimal string.
|
|
153
|
+
class DecimalReading < Lutaml::Model::Serializable
|
|
154
|
+
attribute :value, %i[decimal string]
|
|
155
|
+
|
|
156
|
+
key_value do
|
|
157
|
+
map "value", to: :value
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Simple-content model (text content + attribute) union member.
|
|
162
|
+
class Amount < Lutaml::Model::Serializable
|
|
163
|
+
attribute :value, :string
|
|
164
|
+
attribute :unit, :string
|
|
165
|
+
|
|
166
|
+
xml do
|
|
167
|
+
map_content to: :value
|
|
168
|
+
map_attribute "unit", to: :unit
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
key_value do
|
|
172
|
+
map "value", to: :value
|
|
173
|
+
map "unit", to: :unit
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class Measurement < Lutaml::Model::Serializable
|
|
178
|
+
attribute :amount, [Amount, :string]
|
|
179
|
+
|
|
180
|
+
xml do
|
|
181
|
+
element "measurement"
|
|
182
|
+
map_element "amount", to: :amount
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Class-based transformer that builds the union's model member itself —
|
|
187
|
+
# key_value transforms apply `transform:` BEFORE the cast, so the cast
|
|
188
|
+
# receives an already-constructed member instance.
|
|
189
|
+
class TemperatureTransformer < Lutaml::Model::ValueTransformer
|
|
190
|
+
def from_yaml(value)
|
|
191
|
+
return value unless value.is_a?(::Hash)
|
|
192
|
+
|
|
193
|
+
TemperatureWithUnit.new(value.transform_keys(&:to_sym))
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
class TransformedReading < Lutaml::Model::Serializable
|
|
198
|
+
attribute :temp, [TemperatureWithUnit, :string]
|
|
199
|
+
|
|
200
|
+
key_value do
|
|
201
|
+
map "temp", to: :temp, transform: TemperatureTransformer
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# XML collection mixing structured members and castable scalar text.
|
|
206
|
+
class TemperatureLog < Lutaml::Model::Serializable
|
|
207
|
+
attribute :entries, [TemperatureWithUnit, :integer],
|
|
208
|
+
collection: true
|
|
209
|
+
|
|
210
|
+
xml do
|
|
211
|
+
element "log"
|
|
212
|
+
map_element "entry", to: :entries
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
RSpec.describe "Union-typed attributes (issue #190)" do
|
|
218
|
+
describe "deserialization — member resolution by shape" do
|
|
219
|
+
it "resolves a scalar string to the :string member" do
|
|
220
|
+
ceramic = UnionAttributeSpec::Ceramic.from_yaml(
|
|
221
|
+
"firing_temperature: Very Hot",
|
|
222
|
+
)
|
|
223
|
+
expect(ceramic.firing_temperature).to eq("Very Hot")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "resolves a structured value to the covering model member (number+unit)" do
|
|
227
|
+
ceramic = UnionAttributeSpec::Ceramic.from_yaml(<<~YAML)
|
|
228
|
+
firing_temperature:
|
|
229
|
+
number: 1300.0
|
|
230
|
+
unit: C
|
|
231
|
+
YAML
|
|
232
|
+
expect(ceramic.firing_temperature)
|
|
233
|
+
.to be_a(UnionAttributeSpec::TemperatureWithUnit)
|
|
234
|
+
expect(ceramic.firing_temperature.number).to eq(1300.0)
|
|
235
|
+
expect(ceramic.firing_temperature.unit).to eq("C")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
it "resolves a text-only XML element with an unrelated attribute to the string member" do
|
|
239
|
+
ceramic = UnionAttributeSpec::Ceramic.from_xml(
|
|
240
|
+
'<ceramic><FiringTemperature lang="en">Very Hot</FiringTemperature></ceramic>',
|
|
241
|
+
)
|
|
242
|
+
expect(ceramic.firing_temperature).to eq("Very Hot")
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "resolves a simple-content (map_content + map_attribute) model member in XML" do
|
|
246
|
+
m = UnionAttributeSpec::Measurement.from_xml(
|
|
247
|
+
'<measurement><amount unit="kg">5</amount></measurement>',
|
|
248
|
+
)
|
|
249
|
+
expect(m.amount).to be_a(UnionAttributeSpec::Amount)
|
|
250
|
+
expect(m.amount.value).to eq("5")
|
|
251
|
+
expect(m.amount.unit).to eq("kg")
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "resolves a structured value with a disjoint key to the other model member" do
|
|
255
|
+
ceramic = UnionAttributeSpec::Ceramic.from_yaml(<<~YAML)
|
|
256
|
+
firing_temperature:
|
|
257
|
+
celsius: 1200.0
|
|
258
|
+
YAML
|
|
259
|
+
expect(ceramic.firing_temperature)
|
|
260
|
+
.to be_a(UnionAttributeSpec::Temperature)
|
|
261
|
+
expect(ceramic.firing_temperature.celsius).to eq(1200.0)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
describe "round-trip fidelity (deserialized)" do
|
|
266
|
+
%i[yaml json].each do |format|
|
|
267
|
+
it "round-trips the scalar case through #{format}" do
|
|
268
|
+
ceramic = UnionAttributeSpec::Ceramic.from_yaml(
|
|
269
|
+
"firing_temperature: Very Hot",
|
|
270
|
+
)
|
|
271
|
+
reparsed = UnionAttributeSpec::Ceramic.public_send(
|
|
272
|
+
:"from_#{format}", ceramic.public_send(:"to_#{format}")
|
|
273
|
+
)
|
|
274
|
+
expect(reparsed.firing_temperature).to eq("Very Hot")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
it "round-trips the structured case through #{format}" do
|
|
278
|
+
ceramic = UnionAttributeSpec::Ceramic.from_yaml(<<~YAML)
|
|
279
|
+
firing_temperature:
|
|
280
|
+
number: 1300.0
|
|
281
|
+
unit: C
|
|
282
|
+
YAML
|
|
283
|
+
reparsed = UnionAttributeSpec::Ceramic.public_send(
|
|
284
|
+
:"from_#{format}", ceramic.public_send(:"to_#{format}")
|
|
285
|
+
)
|
|
286
|
+
expect(reparsed.firing_temperature)
|
|
287
|
+
.to be_a(UnionAttributeSpec::TemperatureWithUnit)
|
|
288
|
+
expect(reparsed.firing_temperature.number).to eq(1300.0)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
it "round-trips both XML cases (text-only vs child elements)" do
|
|
293
|
+
scalar = UnionAttributeSpec::Ceramic.from_xml(
|
|
294
|
+
"<ceramic><FiringTemperature>Very Hot</FiringTemperature></ceramic>",
|
|
295
|
+
)
|
|
296
|
+
expect(scalar.firing_temperature).to eq("Very Hot")
|
|
297
|
+
expect(UnionAttributeSpec::Ceramic.from_xml(scalar.to_xml)
|
|
298
|
+
.firing_temperature).to eq("Very Hot")
|
|
299
|
+
|
|
300
|
+
structured = UnionAttributeSpec::Ceramic.from_xml(<<~XML)
|
|
301
|
+
<ceramic><FiringTemperature><number>1300.0</number><unit>C</unit></FiringTemperature></ceramic>
|
|
302
|
+
XML
|
|
303
|
+
expect(structured.firing_temperature)
|
|
304
|
+
.to be_a(UnionAttributeSpec::TemperatureWithUnit)
|
|
305
|
+
reparsed = UnionAttributeSpec::Ceramic.from_xml(structured.to_xml)
|
|
306
|
+
expect(reparsed.firing_temperature.unit).to eq("C")
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
describe "plain-Ruby construction (stateless serialization — the v1 bug class)" do
|
|
311
|
+
it "serializes a model member built in plain Ruby identically to a deserialized one" do
|
|
312
|
+
built = UnionAttributeSpec::Ceramic.new(
|
|
313
|
+
firing_temperature:
|
|
314
|
+
UnionAttributeSpec::TemperatureWithUnit.new(number: 1300.0, unit: "C"),
|
|
315
|
+
)
|
|
316
|
+
round = UnionAttributeSpec::Ceramic.from_yaml(built.to_yaml)
|
|
317
|
+
expect(round.firing_temperature)
|
|
318
|
+
.to be_a(UnionAttributeSpec::TemperatureWithUnit)
|
|
319
|
+
expect(round.firing_temperature.number).to eq(1300.0)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "does NOT double-encode a plain-Ruby scalar member" do
|
|
323
|
+
built = UnionAttributeSpec::Ceramic.new(firing_temperature: "Very Hot")
|
|
324
|
+
expect(JSON.parse(built.to_json)["firing_temperature"]).to eq("Very Hot")
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it "does not raise serializing a plain-Ruby scalar to TOML" do
|
|
328
|
+
built = UnionAttributeSpec::Ceramic.new(firing_temperature: "Very Hot")
|
|
329
|
+
expect { built.to_toml }.not_to raise_error
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it "builds a model member from a plain-Ruby hash (attribute-name keys)" do
|
|
333
|
+
built = UnionAttributeSpec::Ceramic.new(
|
|
334
|
+
firing_temperature: { number: 1300.0, unit: "C" },
|
|
335
|
+
)
|
|
336
|
+
expect(built.firing_temperature)
|
|
337
|
+
.to be_a(UnionAttributeSpec::TemperatureWithUnit)
|
|
338
|
+
expect(built.firing_temperature.number).to eq(1300.0)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
describe "first-conforming-in-declared-order (xsd:union)" do
|
|
343
|
+
it "prefers the earlier member when several could match" do
|
|
344
|
+
# "42" conforms to Integer's lexical space, so Integer wins over :string.
|
|
345
|
+
expect(UnionAttributeSpec::Reading.from_yaml("value: '42'").value).to eq(42)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
it "falls through to the catch-all when earlier members reject the value" do
|
|
349
|
+
expect(UnionAttributeSpec::Reading.from_yaml("value: hello").value)
|
|
350
|
+
.to eq("hello")
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
describe "no-match handling (lenient by default, like every cast)" do
|
|
355
|
+
it "yields nil when no member conforms" do
|
|
356
|
+
reading = UnionAttributeSpec::OptionalReading.new(value: "not a number")
|
|
357
|
+
expect(reading.value).to be_nil
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
it "raises the standard RequiredAttributeMissingError when required and nothing conforms" do
|
|
361
|
+
reading = UnionAttributeSpec::RequiredReading.new(value: "not a number")
|
|
362
|
+
# `validate!` aggregates per-attribute errors into a ValidationError (the
|
|
363
|
+
# library's universal contract for every required attribute, union or not);
|
|
364
|
+
# the underlying error is the standard RequiredAttributeMissingError.
|
|
365
|
+
expect { reading.validate! }.to raise_error(
|
|
366
|
+
Lutaml::Model::ValidationError,
|
|
367
|
+
) { |error| expect(error).to include(Lutaml::Model::RequiredAttributeMissingError) }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "never trips on nil" do
|
|
371
|
+
expect { UnionAttributeSpec::OptionalReading.new(value: nil) }
|
|
372
|
+
.not_to raise_error
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
describe "definition-time member validation" do
|
|
377
|
+
it "raises on an empty member list (attribute :x, [])" do
|
|
378
|
+
expect do
|
|
379
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
380
|
+
attribute :empty_union, []
|
|
381
|
+
end
|
|
382
|
+
end.to raise_error(ArgumentError)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
it "raises when the array contains no valid member type" do
|
|
386
|
+
expect do
|
|
387
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
388
|
+
attribute :bad_union, [Object]
|
|
389
|
+
end
|
|
390
|
+
end.to raise_error(ArgumentError)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it "raises when ANY member is invalid (not only when all are)" do
|
|
394
|
+
expect do
|
|
395
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
396
|
+
attribute :mixed_union, [UnionAttributeSpec::Temperature, Object]
|
|
397
|
+
end
|
|
398
|
+
end.to raise_error(ArgumentError)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it "raises when a catch-all :string is not the last union member" do
|
|
402
|
+
expect do
|
|
403
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
404
|
+
attribute :bad_order, %i[string integer]
|
|
405
|
+
end
|
|
406
|
+
end.to raise_error(ArgumentError)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
it "raises when a catch-all :string precedes a model member" do
|
|
410
|
+
expect do
|
|
411
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
412
|
+
attribute :bad_order,
|
|
413
|
+
[:string, UnionAttributeSpec::Temperature]
|
|
414
|
+
end
|
|
415
|
+
end.to raise_error(ArgumentError, /last union member/)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "raises clearly when a member is an option hash (e.g. { ref: ... })" do
|
|
419
|
+
expect do
|
|
420
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
421
|
+
attribute :ref_union, [{ ref: %w[Target id] }, :string]
|
|
422
|
+
end
|
|
423
|
+
end.to raise_error(ArgumentError, /not supported as union members/)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
it "rejects unsupported scalar member types at definition time" do
|
|
427
|
+
%i[hash symbol time time_without_date date date_time].each do |unsupported|
|
|
428
|
+
expect do
|
|
429
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
430
|
+
attribute :bad, [unsupported, :string]
|
|
431
|
+
end
|
|
432
|
+
end.to raise_error(ArgumentError, /unsupported union member/),
|
|
433
|
+
"expected #{unsupported.inspect} to be rejected"
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
describe "decimal and numeric scalar members" do
|
|
439
|
+
it "accepts a native BigDecimal for a :decimal member" do
|
|
440
|
+
require "bigdecimal"
|
|
441
|
+
reading = UnionAttributeSpec::DecimalReading.new(value: BigDecimal("1.5"))
|
|
442
|
+
expect(reading.value).to eq(BigDecimal("1.5"))
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
it "resolves a decimal string to the :decimal member" do
|
|
446
|
+
reading = UnionAttributeSpec::DecimalReading.from_yaml("value: '1.5'")
|
|
447
|
+
expect(reading.value).to eq(BigDecimal("1.5"))
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
it "serializes a scalar decimal through its type, not as a raw Ruby object" do
|
|
451
|
+
yaml = UnionAttributeSpec::DecimalReading.new(value: BigDecimal("1.5")).to_yaml
|
|
452
|
+
expect(yaml).not_to include("ruby/object")
|
|
453
|
+
expect(UnionAttributeSpec::DecimalReading.from_yaml(yaml).value)
|
|
454
|
+
.to eq(BigDecimal("1.5"))
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it "resolves XSD-style boolean literals (1/yes) to :boolean, else :string" do
|
|
458
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
459
|
+
attribute :value, %i[boolean string]
|
|
460
|
+
key_value { map "value", to: :value }
|
|
461
|
+
end
|
|
462
|
+
expect(klass.from_yaml("value: '1'").value).to be(true)
|
|
463
|
+
expect(klass.from_yaml("value: 'maybe'").value).to eq("maybe")
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
it "accepts a native Integer for a :float member (lossless widening)" do
|
|
467
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
468
|
+
attribute :value, %i[float string]
|
|
469
|
+
key_value { map "value", to: :value }
|
|
470
|
+
end
|
|
471
|
+
value = klass.new(value: 42).value
|
|
472
|
+
expect(value).to be_a(Float)
|
|
473
|
+
expect(value).to eq(42.0)
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
it "accepts a native Integer for a :decimal member" do
|
|
477
|
+
require "bigdecimal"
|
|
478
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
479
|
+
attribute :value, %i[decimal string]
|
|
480
|
+
key_value { map "value", to: :value }
|
|
481
|
+
end
|
|
482
|
+
expect(klass.new(value: 42).value).to eq(BigDecimal(42))
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it "rejects a lossy native numeric for :integer (3.7 falls through to :float)" do
|
|
486
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
487
|
+
attribute :value, %i[integer float]
|
|
488
|
+
key_value { map "value", to: :value }
|
|
489
|
+
end
|
|
490
|
+
value = klass.new(value: 3.7).value
|
|
491
|
+
expect(value).to be_a(Float)
|
|
492
|
+
expect(value).to eq(3.7)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
it "keeps a whole-valued native Float a Float for :integer (3.0 stays 3.0)" do
|
|
496
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
497
|
+
attribute :value, %i[integer float]
|
|
498
|
+
key_value { map "value", to: :value }
|
|
499
|
+
end
|
|
500
|
+
obj = klass.new(value: 3.0)
|
|
501
|
+
expect(obj.value).to be_a(Float)
|
|
502
|
+
expect(obj.value).to eq(3.0)
|
|
503
|
+
expect(obj.to_json).to eq('{"value":3.0}')
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
describe "JSON Schema export" do
|
|
508
|
+
it "wraps a collection union as an array of anyOf" do
|
|
509
|
+
attr = UnionAttributeSpec::IntStringList.attributes[:vals]
|
|
510
|
+
schema = Lutaml::Model::Schema::Generator::Property.new(
|
|
511
|
+
:vals, attr, register: Lutaml::Model::Config.default_register
|
|
512
|
+
).to_schema
|
|
513
|
+
expect(schema["vals"]["type"]).to eq("array")
|
|
514
|
+
expect(schema["vals"]["items"]).to have_key("anyOf")
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
it "exports an optional scalar union as anyOf including a null branch" do
|
|
518
|
+
attr = UnionAttributeSpec::Reading.attributes[:value]
|
|
519
|
+
schema = Lutaml::Model::Schema::Generator::Property.new(
|
|
520
|
+
:value, attr, register: Lutaml::Model::Config.default_register
|
|
521
|
+
).to_schema
|
|
522
|
+
expect(schema["value"]).to have_key("anyOf")
|
|
523
|
+
expect(schema["value"]["anyOf"]).to include("type" => "null")
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
it "includes $defs entries for a union's model members (no dangling $ref)" do
|
|
527
|
+
schema = JSON.parse(
|
|
528
|
+
Lutaml::Model::Schema::JsonSchema.generate(UnionAttributeSpec::Ceramic),
|
|
529
|
+
)
|
|
530
|
+
defs = schema["$defs"] || schema["definitions"] || {}
|
|
531
|
+
refs = JSON.generate(schema)
|
|
532
|
+
.scan(%r{#/(?:\$defs|definitions)/([^"]+)}).flatten
|
|
533
|
+
expect(refs).not_to be_empty
|
|
534
|
+
expect(refs - defs.keys).to be_empty
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
it "carries enum and default constraints onto a union schema" do
|
|
538
|
+
attr = UnionAttributeSpec::ConstrainedReading.attributes[:value]
|
|
539
|
+
schema = Lutaml::Model::Schema::Generator::Property.new(
|
|
540
|
+
:value, attr, register: Lutaml::Model::Config.default_register
|
|
541
|
+
).to_schema
|
|
542
|
+
expect(schema["value"]).to have_key("anyOf")
|
|
543
|
+
expect(schema["value"]["enum"]).to eq([1, "one"])
|
|
544
|
+
expect(schema["value"]["default"]).to eq("one")
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# A :decimal (BigDecimal) serializes to JSON as a string (to preserve
|
|
548
|
+
# arbitrary precision), so its union schema branch is "string" — matching
|
|
549
|
+
# the actual output rather than the README's number classification.
|
|
550
|
+
it "maps a decimal union member to a string branch matching its output" do
|
|
551
|
+
attr = UnionAttributeSpec::DecimalReading.attributes[:value]
|
|
552
|
+
schema = Lutaml::Model::Schema::Generator::Property.new(
|
|
553
|
+
:value, attr, register: Lutaml::Model::Config.default_register
|
|
554
|
+
).to_schema
|
|
555
|
+
types = schema["value"]["anyOf"].map { |member| member["type"] }
|
|
556
|
+
expect(types).to include("string")
|
|
557
|
+
json = UnionAttributeSpec::DecimalReading.new(value: BigDecimal("1.5"))
|
|
558
|
+
.to_json
|
|
559
|
+
expect(JSON.parse(json)["value"]).to be_a(String)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
describe "pattern on a union with a :string member" do
|
|
564
|
+
it "accepts a string value matching the pattern" do
|
|
565
|
+
expect(UnionAttributeSpec::PatternedHolder.new(code: "ABC")
|
|
566
|
+
.validate).to be_empty
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it "rejects a string value violating the pattern" do
|
|
570
|
+
expect(UnionAttributeSpec::PatternedHolder.new(code: "ab1")
|
|
571
|
+
.validate).not_to be_empty
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
it "exempts a non-string member value from the pattern" do
|
|
575
|
+
temp = UnionAttributeSpec::Temperature.new(celsius: 12.0)
|
|
576
|
+
expect(UnionAttributeSpec::PatternedHolder.new(code: temp)
|
|
577
|
+
.validate).to be_empty
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
it "checks each string element of a union collection" do
|
|
581
|
+
expect(UnionAttributeSpec::PatternedList.new(codes: %w[ABC DEF])
|
|
582
|
+
.validate).to be_empty
|
|
583
|
+
expect(UnionAttributeSpec::PatternedList.new(codes: ["ABC", "bad"])
|
|
584
|
+
.validate).not_to be_empty
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
it "exempts non-string elements of a union collection" do
|
|
588
|
+
expect(UnionAttributeSpec::PatternedList.new(codes: [1, "ABC"])
|
|
589
|
+
.validate).to be_empty
|
|
590
|
+
expect(UnionAttributeSpec::PatternedList.new(codes: [1, "bad"])
|
|
591
|
+
.validate).not_to be_empty
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
describe "model member with aliased serialization keys" do
|
|
596
|
+
it "resolves the member through its aliased key" do
|
|
597
|
+
holder = UnionAttributeSpec::AliasedHolder.from_yaml(<<~YAML)
|
|
598
|
+
value:
|
|
599
|
+
product_name: Vase
|
|
600
|
+
YAML
|
|
601
|
+
expect(holder.value).to be_a(UnionAttributeSpec::AliasedMember)
|
|
602
|
+
expect(holder.value.name).to eq("Vase")
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
it "resolves the member through its primary key" do
|
|
606
|
+
holder = UnionAttributeSpec::AliasedHolder.from_yaml(<<~YAML)
|
|
607
|
+
value:
|
|
608
|
+
name: Vase
|
|
609
|
+
YAML
|
|
610
|
+
expect(holder.value).to be_a(UnionAttributeSpec::AliasedMember)
|
|
611
|
+
expect(holder.value.name).to eq("Vase")
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
describe "collections of union values" do
|
|
616
|
+
it "resolves each element independently (mixed model + scalar)" do
|
|
617
|
+
set = UnionAttributeSpec::ReadingSet.from_yaml(<<~YAML)
|
|
618
|
+
readings:
|
|
619
|
+
- number: 1300.0
|
|
620
|
+
unit: C
|
|
621
|
+
- Very Hot
|
|
622
|
+
YAML
|
|
623
|
+
expect(set.readings.first)
|
|
624
|
+
.to be_a(UnionAttributeSpec::TemperatureWithUnit)
|
|
625
|
+
expect(set.readings.last).to eq("Very Hot")
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
it "casts scalar elements in an XML collection mixing model members" do
|
|
629
|
+
log = UnionAttributeSpec::TemperatureLog.from_xml(<<~XML)
|
|
630
|
+
<log>
|
|
631
|
+
<entry>42</entry>
|
|
632
|
+
<entry><number>1300.0</number><unit>C</unit></entry>
|
|
633
|
+
<entry>7</entry>
|
|
634
|
+
</log>
|
|
635
|
+
XML
|
|
636
|
+
expect(log.entries.map(&:class))
|
|
637
|
+
.to eq([Integer, UnionAttributeSpec::TemperatureWithUnit, Integer])
|
|
638
|
+
expect(log.entries.first).to eq(42)
|
|
639
|
+
|
|
640
|
+
reparsed = UnionAttributeSpec::TemperatureLog.from_xml(log.to_xml)
|
|
641
|
+
expect(reparsed.entries.map(&:class)).to eq(log.entries.map(&:class))
|
|
642
|
+
expect(reparsed.entries.first).to eq(42)
|
|
643
|
+
expect(reparsed.entries[1].number).to eq(1300.0)
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
describe "value transformers (cast idempotence)" do
|
|
648
|
+
it "keeps a model member built by transform: before the cast runs" do
|
|
649
|
+
reading = UnionAttributeSpec::TransformedReading.from_yaml(<<~YAML)
|
|
650
|
+
temp:
|
|
651
|
+
number: 7.0
|
|
652
|
+
unit: C
|
|
653
|
+
YAML
|
|
654
|
+
expect(reading.temp).to be_a(UnionAttributeSpec::TemperatureWithUnit)
|
|
655
|
+
expect(reading.temp.number).to eq(7.0)
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
end
|