lutaml-model 0.8.32 → 0.8.34
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/.rubocop.yml +5 -0
- data/Gemfile +4 -0
- data/README.adoc +39 -0
- data/docs/conversion_caching.adoc +143 -0
- data/lib/compat/opal/lutaml_model_boot.rb +15 -0
- data/lib/lutaml/model/attribute.rb +89 -0
- data/lib/lutaml/model/config.rb +13 -0
- data/lib/lutaml/model/configuration.rb +23 -0
- data/lib/lutaml/model/error/fraction_digits_error.rb +20 -0
- data/lib/lutaml/model/error/length_error.rb +19 -0
- data/lib/lutaml/model/error/max_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/max_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/max_length_error.rb +20 -0
- data/lib/lutaml/model/error/min_exclusive_error.rb +19 -0
- data/lib/lutaml/model/error/min_inclusive_error.rb +20 -0
- data/lib/lutaml/model/error/min_length_error.rb +20 -0
- data/lib/lutaml/model/error/restriction_error.rb +8 -0
- data/lib/lutaml/model/error/total_digits_error.rb +20 -0
- data/lib/lutaml/model/error/type/max_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type/min_exclusive_error.rb +20 -0
- data/lib/lutaml/model/error/type.rb +2 -0
- data/lib/lutaml/model/restriction_validation.rb +437 -0
- data/lib/lutaml/model/schema/definitions/restricted_type.rb +4 -2
- data/lib/lutaml/model/schema/renderers/restricted_type.rb +134 -4
- data/lib/lutaml/model/schema/rng_compiler/define_classifier.rb +3 -0
- data/lib/lutaml/model/schema/rng_compiler/rng_helpers.rb +9 -1
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +2 -0
- data/lib/lutaml/model/schema/templates.rb +2 -0
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/simple_types.rb +39 -7
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +3 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +18 -7
- data/lib/lutaml/model/serialize/builder.rb +4 -5
- data/lib/lutaml/model/serialize/conversion_caching.rb +124 -0
- data/lib/lutaml/model/serialize/enum_handling.rb +50 -11
- data/lib/lutaml/model/serialize/format_conversion.rb +46 -38
- data/lib/lutaml/model/serialize/initialization.rb +4 -2
- data/lib/lutaml/model/serialize.rb +61 -0
- data/lib/lutaml/model/services/type/validator.rb +23 -0
- data/lib/lutaml/model/type/string.rb +28 -3
- data/lib/lutaml/model/type/value.rb +231 -0
- data/lib/lutaml/model/type.rb +2 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +16 -0
- data/lib/lutaml/xml/model_transform.rb +47 -6
- data/lib/lutaml/xml/schema/xsd/max_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/min_exclusive.rb +1 -1
- data/lib/lutaml/xml/schema/xsd_schema.rb +183 -11
- data/lib/lutaml/xml/transformation/order_reconciler.rb +267 -0
- data/lib/lutaml/xml/transformation/ordered_applier.rb +80 -122
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
- data/lib/lutaml/xml/transformation.rb +2 -0
- data/lib/lutaml/xml/transformation_support.rb +1 -0
- data/spec/fixtures/xml/restriction_facets.xsd +46 -0
- data/spec/lutaml/model/attribute_restriction_spec.rb +472 -0
- data/spec/lutaml/model/attribute_spec.rb +7 -2
- data/spec/lutaml/model/collection_reader_liveness_spec.rb +516 -0
- data/spec/lutaml/model/conversion_caching_spec.rb +280 -0
- data/spec/lutaml/model/lazy_collection_spec.rb +28 -3
- data/spec/lutaml/model/parsed_model_mutation_spec.rb +486 -0
- data/spec/lutaml/model/phantom_value_spec.rb +799 -0
- data/spec/lutaml/model/schema/renderers/restricted_type_spec.rb +1 -0
- data/spec/lutaml/model/schema/xsd_schema_spec.rb +340 -695
- data/spec/lutaml/model/serialize_perf_guard_spec.rb +53 -1
- data/spec/lutaml/model/type_digit_facet_spec.rb +229 -0
- data/spec/lutaml/model/type_facet_restriction_spec.rb +667 -0
- data/spec/lutaml/model/type_white_space_facet_spec.rb +132 -0
- data/spec/lutaml/xml/schema/compiler_spec.rb +302 -0
- data/spec/lutaml/xml/schema/facet_round_trip_spec.rb +153 -0
- metadata +27 -1
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# An attribute whose type defines its own `self.cast` used to gain a value
|
|
6
|
+
# nobody wrote.
|
|
7
|
+
#
|
|
8
|
+
# The pipeline hands an attribute "no data arrived" — nil, or the uninitialized
|
|
9
|
+
# sentinel — and casts it anyway. Every built-in type returns that input
|
|
10
|
+
# unchanged, so nothing showed. A custom `self.cast` returns a real instance,
|
|
11
|
+
# and that instance became a value the source never carried: a phantom element
|
|
12
|
+
# in the document, or a one-item collection nobody created.
|
|
13
|
+
#
|
|
14
|
+
# There are more ways to reach a cast than the four reader shapes
|
|
15
|
+
# define_attribute_methods dispatches on, so each shape gets its own example
|
|
16
|
+
# here, plus a built-in-typed control that must stay clean either way.
|
|
17
|
+
module PhantomValueSpec
|
|
18
|
+
# The differentiator: a type with its own cast that manufactures an instance
|
|
19
|
+
# for anything, including nil and the sentinel.
|
|
20
|
+
class Eager < Lutaml::Model::Type::Value
|
|
21
|
+
# Value#initialize casts what it is given, so a cast that constructs would
|
|
22
|
+
# recurse forever. Store the value straight, the way a Serializable-typed
|
|
23
|
+
# element (the shape this bug was found on) does.
|
|
24
|
+
def initialize(value) # rubocop:disable Lint/MissingSuper -- see above
|
|
25
|
+
@value = value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.cast(value)
|
|
29
|
+
return value if value.is_a?(Eager)
|
|
30
|
+
|
|
31
|
+
new("E(#{value.inspect})")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.serialize(value)
|
|
35
|
+
value.is_a?(Eager) ? value.value.to_s : value.to_s
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_s
|
|
39
|
+
value.to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# S1 / S1c — enum scalar.
|
|
44
|
+
class EnumScalar < Lutaml::Model::Serializable
|
|
45
|
+
attribute :lead, :string
|
|
46
|
+
attribute :role, Eager, values: %w[a b]
|
|
47
|
+
|
|
48
|
+
xml do
|
|
49
|
+
root "r"
|
|
50
|
+
map_element "lead", to: :lead
|
|
51
|
+
map_element "role", to: :role
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
key_value do
|
|
55
|
+
map "lead", to: :lead
|
|
56
|
+
map "role", to: :role
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class EnumScalarBuiltIn < Lutaml::Model::Serializable
|
|
61
|
+
attribute :lead, :string
|
|
62
|
+
attribute :role, :string, values: %w[a b]
|
|
63
|
+
|
|
64
|
+
xml do
|
|
65
|
+
root "r"
|
|
66
|
+
map_element "lead", to: :lead
|
|
67
|
+
map_element "role", to: :role
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
key_value do
|
|
71
|
+
map "lead", to: :lead
|
|
72
|
+
map "role", to: :role
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# S2 / S2c — enum collection.
|
|
77
|
+
class EnumCollection < Lutaml::Model::Serializable
|
|
78
|
+
attribute :lead, :string
|
|
79
|
+
attribute :roles, Eager, values: %w[a b], collection: true
|
|
80
|
+
|
|
81
|
+
xml do
|
|
82
|
+
root "r"
|
|
83
|
+
map_element "lead", to: :lead
|
|
84
|
+
map_element "roles", to: :roles
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
key_value do
|
|
88
|
+
map "lead", to: :lead
|
|
89
|
+
map "roles", to: :roles
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class EnumCollectionBuiltIn < Lutaml::Model::Serializable
|
|
94
|
+
attribute :lead, :string
|
|
95
|
+
attribute :roles, :string, values: %w[a b c], collection: true
|
|
96
|
+
|
|
97
|
+
xml do
|
|
98
|
+
root "r"
|
|
99
|
+
map_element "lead", to: :lead
|
|
100
|
+
map_element "roles", to: :roles
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# S3 — enum whose reader the model already defines.
|
|
105
|
+
class EnumPredefinedReader < Lutaml::Model::Serializable
|
|
106
|
+
def role
|
|
107
|
+
"mine"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
attribute :lead, :string
|
|
111
|
+
attribute :role, Eager, values: %w[a b]
|
|
112
|
+
|
|
113
|
+
xml do
|
|
114
|
+
root "r"
|
|
115
|
+
map_element "lead", to: :lead
|
|
116
|
+
map_element "role", to: :role
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# S4 — enum with non-String values, so no shorthand methods are generated.
|
|
121
|
+
class EnumNonString < Lutaml::Model::Serializable
|
|
122
|
+
attribute :lead, :string
|
|
123
|
+
attribute :level, :integer, values: [1, 2]
|
|
124
|
+
|
|
125
|
+
xml do
|
|
126
|
+
root "r"
|
|
127
|
+
map_element "lead", to: :lead
|
|
128
|
+
map_element "level", to: :level
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
key_value do
|
|
132
|
+
map "lead", to: :lead
|
|
133
|
+
map "level", to: :level
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# S5 — derived scalar, name != method_name. The reader casts on every call.
|
|
138
|
+
class DerivedScalar < Lutaml::Model::Serializable
|
|
139
|
+
attribute :lead, :string
|
|
140
|
+
attribute :thing, Eager, method: :source_thing
|
|
141
|
+
|
|
142
|
+
xml do
|
|
143
|
+
root "r"
|
|
144
|
+
map_element "lead", to: :lead
|
|
145
|
+
map_element "thing", to: :thing
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
key_value do
|
|
149
|
+
map "lead", to: :lead
|
|
150
|
+
map "thing", to: :thing
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def source_thing
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
class DerivedScalarWithValue < DerivedScalar
|
|
159
|
+
def source_thing
|
|
160
|
+
"real"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# S6 — derived collection.
|
|
165
|
+
class DerivedCollection < Lutaml::Model::Serializable
|
|
166
|
+
attribute :lead, :string
|
|
167
|
+
attribute :things, Eager, method: :source_things, collection: true
|
|
168
|
+
|
|
169
|
+
xml do
|
|
170
|
+
root "r"
|
|
171
|
+
map_element "lead", to: :lead
|
|
172
|
+
map_element "things", to: :things
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
key_value do
|
|
176
|
+
map "lead", to: :lead
|
|
177
|
+
map "things", to: :things
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def source_things
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
class DerivedCollectionWithValues < DerivedCollection
|
|
186
|
+
def source_things
|
|
187
|
+
%w[x y]
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# S6b — a derived collection whose source method returns one bare value
|
|
192
|
+
# rather than a list. This is the only path that reaches the collection
|
|
193
|
+
# wrap in cast_derived; an Array source goes through cast_value instead.
|
|
194
|
+
class DerivedCollectionScalarSource < DerivedCollection
|
|
195
|
+
def source_things
|
|
196
|
+
"x"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# S6c — a derived collection that declares its own collection class. An
|
|
201
|
+
# ordinary Array source is a list of elements here too, but
|
|
202
|
+
# collection_instance? only recognises the declared class.
|
|
203
|
+
class EagerCollection < Lutaml::Model::Collection
|
|
204
|
+
instances :items, Eager
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
class DerivedCustomCollection < Lutaml::Model::Serializable
|
|
208
|
+
attribute :lead, :string
|
|
209
|
+
attribute :things, Eager, collection: EagerCollection,
|
|
210
|
+
method: :source_things
|
|
211
|
+
|
|
212
|
+
def source_things
|
|
213
|
+
%w[x y]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# S7 — derived where name == method_name, so the branch predicate is false
|
|
218
|
+
# and the regular generator runs instead.
|
|
219
|
+
class DerivedSameName < Lutaml::Model::Serializable
|
|
220
|
+
attribute :lead, :string
|
|
221
|
+
attribute :thing, Eager, method: :thing
|
|
222
|
+
|
|
223
|
+
xml do
|
|
224
|
+
root "r"
|
|
225
|
+
map_element "lead", to: :lead
|
|
226
|
+
map_element "thing", to: :thing
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# S8 — derived whose reader is already defined, so nothing is generated.
|
|
231
|
+
class DerivedPredefinedReader < Lutaml::Model::Serializable
|
|
232
|
+
def thing
|
|
233
|
+
"mine"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
attribute :lead, :string
|
|
237
|
+
attribute :thing, Eager, method: :source_thing
|
|
238
|
+
|
|
239
|
+
xml do
|
|
240
|
+
root "r"
|
|
241
|
+
map_element "lead", to: :lead
|
|
242
|
+
map_element "thing", to: :thing
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def source_thing
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# S9 / S10 — Reference attributes. cast_element short-circuits into
|
|
251
|
+
# cast_with_metadata before the type check, so these fabricate with a
|
|
252
|
+
# built-in type too.
|
|
253
|
+
class ReferenceScalar < Lutaml::Model::Serializable
|
|
254
|
+
attribute :lead, :string
|
|
255
|
+
attribute :thing, Lutaml::Model::Type::Reference,
|
|
256
|
+
ref_model_class: "PhantomValueSpec::Target",
|
|
257
|
+
ref_key_attribute: :id
|
|
258
|
+
|
|
259
|
+
xml do
|
|
260
|
+
root "r"
|
|
261
|
+
map_element "lead", to: :lead
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
class ReferenceCollection < Lutaml::Model::Serializable
|
|
266
|
+
attribute :lead, :string
|
|
267
|
+
attribute :things, Lutaml::Model::Type::Reference,
|
|
268
|
+
ref_model_class: "PhantomValueSpec::Target",
|
|
269
|
+
ref_key_attribute: :id, collection: true
|
|
270
|
+
|
|
271
|
+
xml do
|
|
272
|
+
root "r"
|
|
273
|
+
map_element "lead", to: :lead
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# S11 — regular scalar.
|
|
278
|
+
class RegularScalar < Lutaml::Model::Serializable
|
|
279
|
+
attribute :lead, :string
|
|
280
|
+
attribute :thing, Eager
|
|
281
|
+
|
|
282
|
+
xml do
|
|
283
|
+
root "r"
|
|
284
|
+
map_element "lead", to: :lead
|
|
285
|
+
map_element "thing", to: :thing
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
key_value do
|
|
289
|
+
map "lead", to: :lead
|
|
290
|
+
map "thing", to: :thing
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# S12 — regular collection, custom element type. The shape the downstream
|
|
295
|
+
# phantom <w:t/> came from.
|
|
296
|
+
class RegularCollection < Lutaml::Model::Serializable
|
|
297
|
+
attribute :lead, :string
|
|
298
|
+
attribute :things, Eager, collection: true
|
|
299
|
+
|
|
300
|
+
xml do
|
|
301
|
+
root "r"
|
|
302
|
+
ordered
|
|
303
|
+
map_element "lead", to: :lead
|
|
304
|
+
map_element "things", to: :things
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
key_value do
|
|
308
|
+
map "lead", to: :lead
|
|
309
|
+
map "things", to: :things
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Same shape without `ordered`, because rounds 1 and 2 both mistook this for
|
|
314
|
+
# an ordering problem.
|
|
315
|
+
class RegularCollectionUnordered < Lutaml::Model::Serializable
|
|
316
|
+
attribute :lead, :string
|
|
317
|
+
attribute :things, Eager, collection: true
|
|
318
|
+
|
|
319
|
+
xml do
|
|
320
|
+
root "r"
|
|
321
|
+
map_element "lead", to: :lead
|
|
322
|
+
map_element "things", to: :things
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# S12b — the control. Identical but for the element type.
|
|
327
|
+
class RegularCollectionBuiltIn < Lutaml::Model::Serializable
|
|
328
|
+
attribute :lead, :string
|
|
329
|
+
attribute :things, :string, collection: true
|
|
330
|
+
|
|
331
|
+
xml do
|
|
332
|
+
root "r"
|
|
333
|
+
ordered
|
|
334
|
+
map_element "lead", to: :lead
|
|
335
|
+
map_element "things", to: :things
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
key_value do
|
|
339
|
+
map "lead", to: :lead
|
|
340
|
+
map "things", to: :things
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# S13 — regular attribute whose writer the model already defines. The value
|
|
345
|
+
# is cast before it reaches that writer, so a phantom can be laundered
|
|
346
|
+
# through third-party code.
|
|
347
|
+
class RegularPredefinedWriter < Lutaml::Model::Serializable
|
|
348
|
+
def thing=(value)
|
|
349
|
+
@thing = "seen(#{value.inspect})"
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
attribute :lead, :string
|
|
353
|
+
attribute :thing, Eager
|
|
354
|
+
|
|
355
|
+
xml do
|
|
356
|
+
root "r"
|
|
357
|
+
map_element "lead", to: :lead
|
|
358
|
+
map_element "thing", to: :thing
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# S14 — regular attribute whose name collides with an enum value string, so
|
|
363
|
+
# the writer is regenerated even though a setter already exists.
|
|
364
|
+
class RegularEnumShorthandCollision < Lutaml::Model::Serializable
|
|
365
|
+
attribute :lead, :string
|
|
366
|
+
attribute :align, :string, values: %w[char left]
|
|
367
|
+
attribute :char, Eager
|
|
368
|
+
|
|
369
|
+
xml do
|
|
370
|
+
root "r"
|
|
371
|
+
map_element "lead", to: :lead
|
|
372
|
+
map_element "align", to: :align
|
|
373
|
+
map_element "char", to: :char
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# S17 — regular collection with a custom Collection class.
|
|
378
|
+
class ThingCollection < Lutaml::Model::Collection
|
|
379
|
+
instances :items, Eager
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
class RegularCustomCollection < Lutaml::Model::Serializable
|
|
383
|
+
attribute :lead, :string
|
|
384
|
+
attribute :things, Eager, collection: ThingCollection
|
|
385
|
+
|
|
386
|
+
xml do
|
|
387
|
+
root "r"
|
|
388
|
+
map_element "lead", to: :lead
|
|
389
|
+
map_element "things", to: :things
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# S18 — a collection with a real default. The fix must not eat these.
|
|
394
|
+
class SeededCollection < Lutaml::Model::Serializable
|
|
395
|
+
attribute :lead, :string
|
|
396
|
+
attribute :things, Eager, collection: true, default: -> { ["seed"] }
|
|
397
|
+
|
|
398
|
+
xml do
|
|
399
|
+
root "r"
|
|
400
|
+
map_element "lead", to: :lead
|
|
401
|
+
map_element "things", to: :things
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# S20 — a scalar default the mapping asks for explicitly.
|
|
406
|
+
class RenderedDefault < Lutaml::Model::Serializable
|
|
407
|
+
attribute :lead, :string
|
|
408
|
+
attribute :thing, Eager, default: -> { "dflt" }
|
|
409
|
+
|
|
410
|
+
xml do
|
|
411
|
+
root "r"
|
|
412
|
+
map_element "lead", to: :lead
|
|
413
|
+
map_element "thing", to: :thing, render_default: true
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
RSpec.describe "a value nobody wrote" do
|
|
419
|
+
def compact(xml)
|
|
420
|
+
xml.to_s.gsub(/<\?xml[^>]*\?>/, "").gsub(/\s*\n\s*/, "").strip
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
let(:register) { Lutaml::Model::Config.default_register }
|
|
424
|
+
let(:uninitialized) { Lutaml::Model::UninitializedClass.instance }
|
|
425
|
+
let(:bare_xml) { "<r><lead>L</lead></r>" }
|
|
426
|
+
let(:bare_json) { '{"lead":"L"}' }
|
|
427
|
+
|
|
428
|
+
# Each format is asserted against its own output. A single combined
|
|
429
|
+
# assertion would hide the shapes where the formats disagree.
|
|
430
|
+
def emits_nothing_extra(klass, attr_name)
|
|
431
|
+
aggregate_failures do
|
|
432
|
+
expect(compact(klass.new(lead: "L").to_xml)).to eq("<r><lead>L</lead></r>")
|
|
433
|
+
expect(klass.new(lead: "L").to_json).to eq('{"lead":"L"}')
|
|
434
|
+
expect(klass.new(lead: "L").to_yaml).to eq("---\nlead: L\n")
|
|
435
|
+
expect(compact(klass.from_xml(bare_xml).to_xml))
|
|
436
|
+
.to eq("<r><lead>L</lead></r>")
|
|
437
|
+
expect(klass.from_json(bare_json).to_json).to eq('{"lead":"L"}')
|
|
438
|
+
expect(klass.from_yaml("lead: L\n").to_yaml).to eq("---\nlead: L\n")
|
|
439
|
+
expect(klass.attributes[attr_name].cast_element(uninitialized, register))
|
|
440
|
+
.to be(uninitialized)
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
describe "the root cause" do
|
|
445
|
+
# The module claims to protect self.cast for every subclass. It prepends
|
|
446
|
+
# instance methods, so it never sees a class-level cast, which is why the
|
|
447
|
+
# guard has to live in the attribute layer instead.
|
|
448
|
+
it "is not covered by UninitializedClassGuard" do
|
|
449
|
+
expect(Lutaml::Model::Type::Value.singleton_class.ancestors)
|
|
450
|
+
.not_to include(Lutaml::Model::Type::UninitializedClassGuard)
|
|
451
|
+
expect(PhantomValueSpec::Eager.cast(uninitialized))
|
|
452
|
+
.to be_a(PhantomValueSpec::Eager)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
it "leaves the sentinel alone at the one place a type is handed a value" do
|
|
456
|
+
attr = PhantomValueSpec::RegularCollection.attributes[:things]
|
|
457
|
+
|
|
458
|
+
expect(attr.cast_element(uninitialized, register)).to be(uninitialized)
|
|
459
|
+
expect(attr.cast_value(uninitialized, register)).to be(uninitialized)
|
|
460
|
+
expect(attr.default(register)).to be(uninitialized)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it "leaves nil alone there too" do
|
|
464
|
+
attr = PhantomValueSpec::RegularCollection.attributes[:things]
|
|
465
|
+
|
|
466
|
+
expect(attr.cast_element(nil, register)).to be_nil
|
|
467
|
+
expect(attr.cast(nil, :json, register)).to be_nil
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
it "still casts real data" do
|
|
471
|
+
attr = PhantomValueSpec::RegularCollection.attributes[:things]
|
|
472
|
+
|
|
473
|
+
expect(attr.cast_element("x", register)).to be_a(PhantomValueSpec::Eager)
|
|
474
|
+
expect(attr.cast_value(%w[x y], register).size).to eq(2)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
describe "S1c enum scalar, custom element type" do
|
|
479
|
+
it "emits no element and no key for a source that carried neither" do
|
|
480
|
+
emits_nothing_extra(PhantomValueSpec::EnumScalar, :role)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
it "does not fail its own validator over a value nobody set" do
|
|
484
|
+
expect { PhantomValueSpec::EnumScalar.new(lead: "L").validate! }
|
|
485
|
+
.not_to raise_error
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
describe "S1 enum scalar, built-in type (control)" do
|
|
490
|
+
it "stays clean" do
|
|
491
|
+
emits_nothing_extra(PhantomValueSpec::EnumScalarBuiltIn, :role)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
describe "S2c enum collection, custom element type" do
|
|
496
|
+
it "emits no element and no key for a source that carried neither" do
|
|
497
|
+
emits_nothing_extra(PhantomValueSpec::EnumCollection, :roles)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
describe "S2 enum collection, built-in type" do
|
|
502
|
+
it "keeps an item pushed through the reader" do
|
|
503
|
+
model = PhantomValueSpec::EnumCollectionBuiltIn.new(lead: "L",
|
|
504
|
+
roles: ["a"])
|
|
505
|
+
model.roles << "b"
|
|
506
|
+
|
|
507
|
+
expect(model.roles).to eq(%w[a b])
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
it "carries that item into the document" do
|
|
511
|
+
model = PhantomValueSpec::EnumCollectionBuiltIn.new(lead: "L",
|
|
512
|
+
roles: ["a"])
|
|
513
|
+
model.roles << "b"
|
|
514
|
+
|
|
515
|
+
expect(compact(model.to_xml))
|
|
516
|
+
.to eq("<r><lead>L</lead><roles>a</roles><roles>b</roles></r>")
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
it "keeps an item pushed onto a collection the document omitted" do
|
|
520
|
+
model = PhantomValueSpec::EnumCollectionBuiltIn.from_xml(bare_xml)
|
|
521
|
+
model.roles << "a"
|
|
522
|
+
|
|
523
|
+
expect(model.roles).to eq(["a"])
|
|
524
|
+
expect(compact(model.to_xml))
|
|
525
|
+
.to eq("<r><lead>L</lead><roles>a</roles></r>")
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
it "still keeps duplicates out" do
|
|
529
|
+
model = PhantomValueSpec::EnumCollectionBuiltIn.new(lead: "L")
|
|
530
|
+
model.roles = ["a"]
|
|
531
|
+
model.roles = %w[a b]
|
|
532
|
+
|
|
533
|
+
expect(model.roles).to eq(%w[a b])
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
it "still keeps duplicates out through the shorthand writer" do
|
|
537
|
+
model = PhantomValueSpec::EnumCollectionBuiltIn.new(lead: "L")
|
|
538
|
+
model.a!
|
|
539
|
+
model.a!
|
|
540
|
+
model.b!
|
|
541
|
+
|
|
542
|
+
expect(model.roles).to eq(%w[a b])
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
it "still removes a value through the shorthand writer" do
|
|
546
|
+
model = PhantomValueSpec::EnumCollectionBuiltIn.new(lead: "L",
|
|
547
|
+
roles: %w[a b])
|
|
548
|
+
model.a = false
|
|
549
|
+
|
|
550
|
+
expect(model.roles).to eq(["b"])
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
describe "S3 enum whose reader the model already defines" do
|
|
555
|
+
it "leaves the model's own reader in charge" do
|
|
556
|
+
expect(PhantomValueSpec::EnumPredefinedReader.new(lead: "L").role)
|
|
557
|
+
.to eq("mine")
|
|
558
|
+
expect(compact(PhantomValueSpec::EnumPredefinedReader.new(lead: "L").to_xml))
|
|
559
|
+
.to eq("<r><lead>L</lead><role>mine</role></r>")
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
it "does not fabricate at the attribute layer behind it" do
|
|
563
|
+
attr = PhantomValueSpec::EnumPredefinedReader.attributes[:role]
|
|
564
|
+
|
|
565
|
+
expect(attr.cast_element(uninitialized, register)).to be(uninitialized)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
describe "S4 enum with non-String values (control)" do
|
|
570
|
+
it "stays clean" do
|
|
571
|
+
emits_nothing_extra(PhantomValueSpec::EnumNonString, :level)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
it "generates no shorthand methods" do
|
|
575
|
+
expect(PhantomValueSpec::EnumNonString.new(lead: "L")).not_to respond_to(:"1?")
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
describe "S5 derived scalar" do
|
|
580
|
+
# The reader calls into the cast on every call, with no writer in the
|
|
581
|
+
# path, so a writer-side guard cannot reach this shape at all.
|
|
582
|
+
it "emits no element and no key when the source method returns nothing" do
|
|
583
|
+
emits_nothing_extra(PhantomValueSpec::DerivedScalar, :thing)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
it "reads as nil rather than as a manufactured instance" do
|
|
587
|
+
expect(PhantomValueSpec::DerivedScalar.new(lead: "L").thing).to be_nil
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
it "still casts what the source method does return" do
|
|
591
|
+
model = PhantomValueSpec::DerivedScalarWithValue.new(lead: "L")
|
|
592
|
+
|
|
593
|
+
expect(model.thing).to be_a(PhantomValueSpec::Eager)
|
|
594
|
+
expect(compact(model.to_xml))
|
|
595
|
+
.to eq(%(<r><lead>L</lead><thing>E("real")</thing></r>))
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
describe "S6 derived collection" do
|
|
600
|
+
it "emits no element and no key when the source method returns nothing" do
|
|
601
|
+
emits_nothing_extra(PhantomValueSpec::DerivedCollection, :things)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
it "reads as a collection, not as a bare instance" do
|
|
605
|
+
expect(PhantomValueSpec::DerivedCollection.new(lead: "L").things).to eq([])
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
it "still casts every item the source method returns" do
|
|
609
|
+
model = PhantomValueSpec::DerivedCollectionWithValues.new(lead: "L")
|
|
610
|
+
|
|
611
|
+
expect(model.things.size).to eq(2)
|
|
612
|
+
expect(model.things).to all(be_a(PhantomValueSpec::Eager))
|
|
613
|
+
expect(compact(model.to_xml))
|
|
614
|
+
.to eq(%(<r><lead>L</lead><things>E("x")</things><things>E("y")</things></r>))
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
it "reads an array source as many elements even with its own collection class" do
|
|
618
|
+
model = PhantomValueSpec::DerivedCustomCollection.new(lead: "L")
|
|
619
|
+
|
|
620
|
+
expect(model.things.size).to eq(2)
|
|
621
|
+
expect(model.things.map(&:to_s)).to eq(['E("x")', 'E("y")'])
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
it "reads a single source value as a one-item collection, not a bare instance" do
|
|
625
|
+
model = PhantomValueSpec::DerivedCollectionScalarSource.new(lead: "L")
|
|
626
|
+
|
|
627
|
+
expect(model.things).to be_a(Array)
|
|
628
|
+
expect(model.things.size).to eq(1)
|
|
629
|
+
expect(model.things.first).to be_a(PhantomValueSpec::Eager)
|
|
630
|
+
expect(compact(model.to_xml))
|
|
631
|
+
.to eq(%(<r><lead>L</lead><things>E("x")</things></r>))
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
describe "S7 derived where the name equals the method name" do
|
|
636
|
+
it "falls through to the regular generator and stays clean" do
|
|
637
|
+
attr = PhantomValueSpec::DerivedSameName.attributes[:thing]
|
|
638
|
+
|
|
639
|
+
expect(attr.cast_element(uninitialized, register)).to be(uninitialized)
|
|
640
|
+
expect(compact(PhantomValueSpec::DerivedSameName.new(lead: "L").to_xml))
|
|
641
|
+
.to eq("<r><lead>L</lead></r>")
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
describe "S8 derived whose reader the model already defines" do
|
|
646
|
+
it "leaves the model's own reader in charge" do
|
|
647
|
+
expect(compact(PhantomValueSpec::DerivedPredefinedReader.new(lead: "L").to_xml))
|
|
648
|
+
.to eq("<r><lead>L</lead><thing>mine</thing></r>")
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
it "does not fabricate at the attribute layer behind it" do
|
|
652
|
+
attr = PhantomValueSpec::DerivedPredefinedReader.attributes[:thing]
|
|
653
|
+
|
|
654
|
+
expect(attr.cast_element(uninitialized, register)).to be(uninitialized)
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
describe "S9 Reference scalar" do
|
|
659
|
+
# cast_element returns before validate_attr_type!, so this shape
|
|
660
|
+
# fabricated a live Reference even with a built-in type.
|
|
661
|
+
it "does not build a Reference out of the sentinel" do
|
|
662
|
+
attr = PhantomValueSpec::ReferenceScalar.attributes[:thing]
|
|
663
|
+
|
|
664
|
+
expect(attr.cast_element(uninitialized, register)).to be(uninitialized)
|
|
665
|
+
expect(attr.cast_value(uninitialized, register)).to be(uninitialized)
|
|
666
|
+
expect(attr.default(register)).to be(uninitialized)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
it "still builds one out of a real key" do
|
|
670
|
+
attr = PhantomValueSpec::ReferenceScalar.attributes[:thing]
|
|
671
|
+
|
|
672
|
+
expect(attr.cast_element("k1", register))
|
|
673
|
+
.to be_a(Lutaml::Model::Type::Reference)
|
|
674
|
+
end
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
describe "S10 Reference collection" do
|
|
678
|
+
it "does not build a Reference out of the sentinel" do
|
|
679
|
+
attr = PhantomValueSpec::ReferenceCollection.attributes[:things]
|
|
680
|
+
|
|
681
|
+
expect(attr.cast_element(uninitialized, register)).to be(uninitialized)
|
|
682
|
+
expect(attr.default(register)).to be(uninitialized)
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
describe "the cast guard itself" do
|
|
687
|
+
it "still reports an undeclared type when the value carries nothing" do
|
|
688
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
689
|
+
attribute :thing, :no_such_type_is_registered
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
expect { klass.new(thing: nil) }
|
|
693
|
+
.to raise_error(Lutaml::Model::UnknownTypeError)
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
describe "S11 regular scalar" do
|
|
698
|
+
it "emits no element and no key for a source that carried neither" do
|
|
699
|
+
emits_nothing_extra(PhantomValueSpec::RegularScalar, :thing)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
it "reads as nil on a model built with no value for it" do
|
|
703
|
+
expect(PhantomValueSpec::RegularScalar.new(lead: "L").thing).to be_nil
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
describe "S12 regular collection, custom element type" do
|
|
708
|
+
it "emits no element and no key for a source that carried neither" do
|
|
709
|
+
emits_nothing_extra(PhantomValueSpec::RegularCollection, :things)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
it "emits nothing on a plain mapping either" do
|
|
713
|
+
expect(compact(PhantomValueSpec::RegularCollectionUnordered.from_xml(bare_xml).to_xml))
|
|
714
|
+
.to eq("<r><lead>L</lead></r>")
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
it "leaves the collection empty rather than holding a made-up item" do
|
|
718
|
+
expect(PhantomValueSpec::RegularCollection.from_xml(bare_xml).things)
|
|
719
|
+
.to eq([])
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
it "still emits what the document did contain" do
|
|
723
|
+
model = PhantomValueSpec::RegularCollection
|
|
724
|
+
.from_xml("<r><lead>L</lead><things>P</things></r>")
|
|
725
|
+
|
|
726
|
+
expect(compact(model.to_xml))
|
|
727
|
+
.to eq(%(<r><lead>L</lead><things>E("P")</things></r>))
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
it "appends through the builder argument instead of replacing" do
|
|
731
|
+
model = PhantomValueSpec::RegularCollection.new(lead: "L")
|
|
732
|
+
model.things("x")
|
|
733
|
+
model.things("y")
|
|
734
|
+
|
|
735
|
+
expect(model.things.size).to eq(2)
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
describe "S12b regular collection, built-in element type (control)" do
|
|
740
|
+
it "was clean before and stays clean" do
|
|
741
|
+
emits_nothing_extra(PhantomValueSpec::RegularCollectionBuiltIn, :things)
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
describe "S13 regular attribute whose writer the model already defines" do
|
|
746
|
+
# The value is cast before it reaches the model's own writer, so a
|
|
747
|
+
# manufactured one gets laundered through code the library does not own.
|
|
748
|
+
it "hands that writer nothing rather than a made-up value" do
|
|
749
|
+
model = PhantomValueSpec::RegularPredefinedWriter.from_json(bare_json)
|
|
750
|
+
|
|
751
|
+
expect(model.thing).to eq("seen(nil)")
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
it "does not fabricate at the attribute layer behind it" do
|
|
755
|
+
attr = PhantomValueSpec::RegularPredefinedWriter.attributes[:thing]
|
|
756
|
+
|
|
757
|
+
expect(attr.cast_element(uninitialized, register)).to be(uninitialized)
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
describe "S14 regular attribute colliding with an enum value name" do
|
|
762
|
+
it "emits no element for a source that carried none" do
|
|
763
|
+
expect(compact(PhantomValueSpec::RegularEnumShorthandCollision.new(lead: "L").to_xml))
|
|
764
|
+
.to eq("<r><lead>L</lead></r>")
|
|
765
|
+
expect(compact(PhantomValueSpec::RegularEnumShorthandCollision.from_xml(bare_xml).to_xml))
|
|
766
|
+
.to eq("<r><lead>L</lead></r>")
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
describe "S17 regular collection with a custom Collection class" do
|
|
771
|
+
it "emits no element for a source that carried none" do
|
|
772
|
+
expect(compact(PhantomValueSpec::RegularCustomCollection.from_xml(bare_xml).to_xml))
|
|
773
|
+
.to eq("<r><lead>L</lead></r>")
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
it "does not fail its own validator over a collection nobody filled" do
|
|
777
|
+
expect { PhantomValueSpec::RegularCustomCollection.new(lead: "L").validate! }
|
|
778
|
+
.not_to raise_error
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
describe "S18 collection with a real default" do
|
|
783
|
+
it "still emits the default" do
|
|
784
|
+
expect(compact(PhantomValueSpec::SeededCollection.new(lead: "L").to_xml))
|
|
785
|
+
.to eq(%(<r><lead>L</lead><things>E("seed")</things></r>))
|
|
786
|
+
expect(compact(PhantomValueSpec::SeededCollection.from_xml(bare_xml).to_xml))
|
|
787
|
+
.to eq(%(<r><lead>L</lead><things>E("seed")</things></r>))
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
describe "S20 scalar with render_default on the mapping" do
|
|
792
|
+
it "still emits the default" do
|
|
793
|
+
expect(compact(PhantomValueSpec::RenderedDefault.new(lead: "L").to_xml))
|
|
794
|
+
.to include("dflt")
|
|
795
|
+
expect(compact(PhantomValueSpec::RenderedDefault.from_xml(bare_xml).to_xml))
|
|
796
|
+
.to include("dflt")
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
end
|