lutaml-model 0.8.33 → 0.8.35
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/.rspec +1 -0
- 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 +95 -4
- 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/register.rb +12 -5
- 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/transformation_registry.rb +13 -7
- 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/adapter_element.rb +17 -4
- data/lib/lutaml/xml/model_transform.rb +4 -1
- 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/benchmarks/xml_parsing_benchmark_spec.rb +70 -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 +28 -1
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# A collection reader has to hand back a collection the caller can push onto.
|
|
6
|
+
#
|
|
7
|
+
# Four different readers broke that in four different ways, and only one of
|
|
8
|
+
# them made any noise:
|
|
9
|
+
# - the regular reader handed out LAZY_EMPTY_COLLECTION, one frozen Array
|
|
10
|
+
# shared by every instance, so `model.items << x` raised FrozenError;
|
|
11
|
+
# - the register-scoped reader substituted a fresh [] on every call, so the
|
|
12
|
+
# push landed on a throwaway and vanished without a word;
|
|
13
|
+
# - the enum reader returned `i.uniq`, also a fresh Array, same silence;
|
|
14
|
+
# - the reference reader resolved into a fresh Array, same silence again.
|
|
15
|
+
module CollectionReaderLivenessSpec
|
|
16
|
+
class Doc < Lutaml::Model::Serializable
|
|
17
|
+
attribute :name, :string
|
|
18
|
+
attribute :items, :string, collection: true
|
|
19
|
+
attribute :others, :string, collection: true
|
|
20
|
+
|
|
21
|
+
xml do
|
|
22
|
+
root "doc"
|
|
23
|
+
map_element "name", to: :name
|
|
24
|
+
map_element "item", to: :items
|
|
25
|
+
map_element "other", to: :others
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Filled < Lutaml::Model::Serializable
|
|
30
|
+
attribute :name, :string
|
|
31
|
+
attribute :items, :string, collection: true, initialize_empty: true
|
|
32
|
+
|
|
33
|
+
xml do
|
|
34
|
+
root "doc"
|
|
35
|
+
map_element "name", to: :name
|
|
36
|
+
map_element "item", to: :items
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
key_value do
|
|
40
|
+
map "name", to: :name
|
|
41
|
+
map "item", to: :items
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A model is allowed to define its own writer, and the generated reader has
|
|
46
|
+
# to cope with whatever that writer stored.
|
|
47
|
+
class RolesOwnWriter < Lutaml::Model::Serializable
|
|
48
|
+
attribute :roles, :integer, values: [1, 2, 3], collection: true
|
|
49
|
+
|
|
50
|
+
def roles=(value)
|
|
51
|
+
@roles = Set.new(value)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A model whose own writer keeps the value somewhere else, so the enum ivar
|
|
56
|
+
# is never written and the reader meets a bare nil.
|
|
57
|
+
class RolesLazyWriter < Lutaml::Model::Serializable
|
|
58
|
+
attribute :roles, :string, values: %w[reader writer admin],
|
|
59
|
+
collection: true
|
|
60
|
+
|
|
61
|
+
def roles=(value)
|
|
62
|
+
@stashed = value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A reader the model defines itself can hand back anything at all, including
|
|
67
|
+
# one array shared by every instance. Deliberately not duplicated — sharing
|
|
68
|
+
# is the whole point, and a writer that mutated it would reach every model.
|
|
69
|
+
SHARED_ROLES = [] # rubocop:disable Style/MutableConstant -- shared on purpose
|
|
70
|
+
|
|
71
|
+
class RolesOwnReader < Lutaml::Model::Serializable
|
|
72
|
+
attribute :roles, :string, values: %w[reader writer admin],
|
|
73
|
+
collection: true
|
|
74
|
+
|
|
75
|
+
def roles
|
|
76
|
+
@roles ||= SHARED_ROLES
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class RolesSetReader < Lutaml::Model::Serializable
|
|
81
|
+
attribute :roles, :string, values: %w[reader writer admin],
|
|
82
|
+
collection: true
|
|
83
|
+
|
|
84
|
+
def roles
|
|
85
|
+
@roles ||= Set.new
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class RolesFrozenDuplicates < Lutaml::Model::Serializable
|
|
90
|
+
attribute :roles, :string, values: %w[reader writer admin],
|
|
91
|
+
collection: true
|
|
92
|
+
|
|
93
|
+
def roles=(_value)
|
|
94
|
+
@roles = %w[admin admin].freeze
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class RefAuthor < Lutaml::Model::Serializable
|
|
99
|
+
attribute :id, :string
|
|
100
|
+
attribute :name, :string
|
|
101
|
+
|
|
102
|
+
xml do
|
|
103
|
+
root "author"
|
|
104
|
+
map_element "id", to: :id
|
|
105
|
+
map_element "name", to: :name
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class RefBook < Lutaml::Model::Serializable
|
|
110
|
+
attribute :id, :string
|
|
111
|
+
attribute :co_authors, { ref: ["CollectionReaderLivenessSpec::RefAuthor", :id] },
|
|
112
|
+
collection: true, default: []
|
|
113
|
+
|
|
114
|
+
xml do
|
|
115
|
+
root "book"
|
|
116
|
+
map_element "id", to: :id
|
|
117
|
+
map_element "coAuthor", to: :co_authors
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Roles < Lutaml::Model::Serializable
|
|
122
|
+
attribute :name, :string
|
|
123
|
+
attribute :roles, :string, values: %w[reader writer admin], collection: true
|
|
124
|
+
|
|
125
|
+
xml do
|
|
126
|
+
root "roles"
|
|
127
|
+
map_element "name", to: :name
|
|
128
|
+
map_element "role", to: :roles
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
RSpec.describe "what a collection reader hands back" do
|
|
134
|
+
let(:sentinel) { Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION }
|
|
135
|
+
let(:bare) { "<doc><name>n</name></doc>" }
|
|
136
|
+
|
|
137
|
+
describe "the regular collection reader" do
|
|
138
|
+
it "keeps an item pushed onto a collection the document omitted" do
|
|
139
|
+
model = CollectionReaderLivenessSpec::Doc.from_xml(bare)
|
|
140
|
+
|
|
141
|
+
model.items << "kept"
|
|
142
|
+
|
|
143
|
+
expect(model.items).to eq(["kept"])
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "carries that item into the document" do
|
|
147
|
+
model = CollectionReaderLivenessSpec::Doc.from_xml(bare)
|
|
148
|
+
model.items << "kept"
|
|
149
|
+
|
|
150
|
+
expect(model.to_xml.to_s).to include("<item>kept</item>")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "gives each instance its own array" do
|
|
154
|
+
one = CollectionReaderLivenessSpec::Doc.from_xml(bare)
|
|
155
|
+
two = CollectionReaderLivenessSpec::Doc.from_xml(bare)
|
|
156
|
+
|
|
157
|
+
one.items << "mine"
|
|
158
|
+
|
|
159
|
+
expect(two.items).to eq([])
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "leaves the collections nobody read on the shared sentinel" do
|
|
163
|
+
model = CollectionReaderLivenessSpec::Doc.from_xml(bare)
|
|
164
|
+
|
|
165
|
+
model.items
|
|
166
|
+
|
|
167
|
+
expect(model.instance_variable_get(:@others)).to be(sentinel)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "keeps the sentinel on a frozen model rather than raising" do
|
|
171
|
+
model = CollectionReaderLivenessSpec::Doc.from_xml(bare).freeze
|
|
172
|
+
|
|
173
|
+
expect(model.items).to be(sentinel)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Reading must not be observable in the output. Every earlier round of
|
|
177
|
+
# this fix was refuted by a change in what got emitted.
|
|
178
|
+
%w[xml json yaml].each do |format|
|
|
179
|
+
it "does not change to_#{format} just because something read it" do
|
|
180
|
+
quiet = CollectionReaderLivenessSpec::Doc.from_xml(bare)
|
|
181
|
+
peeked = CollectionReaderLivenessSpec::Doc.from_xml(bare)
|
|
182
|
+
peeked.items
|
|
183
|
+
peeked.others
|
|
184
|
+
|
|
185
|
+
expect(peeked.public_send(:"to_#{format}").to_s)
|
|
186
|
+
.to eq(quiet.public_send(:"to_#{format}").to_s)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe "the enum collection reader" do
|
|
192
|
+
it "keeps an item pushed onto an empty enum collection" do
|
|
193
|
+
model = CollectionReaderLivenessSpec::Roles.from_xml(
|
|
194
|
+
"<roles><name>n</name></roles>",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
model.roles << "admin"
|
|
198
|
+
|
|
199
|
+
expect(model.roles).to eq(["admin"])
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "keeps an item pushed onto a populated enum collection" do
|
|
203
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
204
|
+
roles: ["reader"])
|
|
205
|
+
|
|
206
|
+
model.roles << "writer"
|
|
207
|
+
|
|
208
|
+
expect(model.roles).to eq(%w[reader writer])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "carries that item into the document" do
|
|
212
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
213
|
+
roles: ["reader"])
|
|
214
|
+
model.roles << "writer"
|
|
215
|
+
|
|
216
|
+
expect(model.to_xml.to_s).to include("<role>writer</role>")
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
it "still hides duplicates the way the reader used to" do
|
|
220
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n")
|
|
221
|
+
model.roles = ["reader"]
|
|
222
|
+
model.roles = %w[reader writer]
|
|
223
|
+
|
|
224
|
+
expect(model.roles).to eq(%w[reader writer])
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "still answers the shorthand predicate" do
|
|
228
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
229
|
+
roles: ["admin"])
|
|
230
|
+
|
|
231
|
+
expect(model.admin?).to be(true)
|
|
232
|
+
expect(model.reader?).to be(false)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Handing back the stored Array must not cost the guarantees the old
|
|
236
|
+
# `uniq` reader gave.
|
|
237
|
+
it "still reads unique after a duplicate is pushed through the reader" do
|
|
238
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
239
|
+
roles: ["admin"])
|
|
240
|
+
|
|
241
|
+
model.roles << "admin"
|
|
242
|
+
|
|
243
|
+
expect(model.roles).to eq(["admin"])
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# The reader hands out the stored Array, so a writer must not mutate it.
|
|
247
|
+
# Every write builds a new Array, the way it did before the reader went
|
|
248
|
+
# live, which keeps a shared or frozen backing value safe.
|
|
249
|
+
it "does not write through into an array a caller already holds" do
|
|
250
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
251
|
+
roles: ["admin"])
|
|
252
|
+
held = model.roles
|
|
253
|
+
|
|
254
|
+
model.roles = ["reader"]
|
|
255
|
+
|
|
256
|
+
expect(held).to eq(["admin"])
|
|
257
|
+
expect(model.roles).to eq(%w[admin reader])
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "does not duplicate into a held array through the shorthand writer" do
|
|
261
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
262
|
+
roles: ["admin"])
|
|
263
|
+
held = model.roles
|
|
264
|
+
|
|
265
|
+
model.admin = true
|
|
266
|
+
|
|
267
|
+
expect(held).to eq(["admin"])
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "hands back a stored array even when nothing ever wrote the ivar" do
|
|
271
|
+
model = CollectionReaderLivenessSpec::RolesLazyWriter.new
|
|
272
|
+
|
|
273
|
+
model.roles << "admin"
|
|
274
|
+
|
|
275
|
+
expect(model.roles).to eq(["admin"])
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
it "does not write through into a reader the model defines itself" do
|
|
279
|
+
CollectionReaderLivenessSpec::SHARED_ROLES.clear
|
|
280
|
+
one = CollectionReaderLivenessSpec::RolesOwnReader.new
|
|
281
|
+
two = CollectionReaderLivenessSpec::RolesOwnReader.new
|
|
282
|
+
|
|
283
|
+
one.roles = ["admin"]
|
|
284
|
+
|
|
285
|
+
expect(one.roles).to eq(["admin"])
|
|
286
|
+
expect(two.roles).to eq([])
|
|
287
|
+
expect(CollectionReaderLivenessSpec::SHARED_ROLES).to eq([])
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "assigns through a reader that hands back something other than an array" do
|
|
291
|
+
model = CollectionReaderLivenessSpec::RolesSetReader.new
|
|
292
|
+
|
|
293
|
+
model.roles = ["admin"]
|
|
294
|
+
|
|
295
|
+
expect(model.roles).to eq(Set.new(["admin"]))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
it "does not remove through into a held array via the shorthand writer" do
|
|
299
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
300
|
+
roles: %w[admin reader])
|
|
301
|
+
held = model.roles
|
|
302
|
+
|
|
303
|
+
model.admin = false
|
|
304
|
+
|
|
305
|
+
expect(held).to eq(%w[admin reader])
|
|
306
|
+
expect(model.roles).to eq(["reader"])
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it "does not mutate through the shorthand writer on a frozen model" do
|
|
310
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
311
|
+
roles: ["admin"])
|
|
312
|
+
held = model.roles
|
|
313
|
+
model.freeze
|
|
314
|
+
|
|
315
|
+
expect { model.writer = true }.to raise_error(FrozenError)
|
|
316
|
+
expect(held).to eq(["admin"])
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it "reads a frozen backing array as unique" do
|
|
320
|
+
model = CollectionReaderLivenessSpec::RolesFrozenDuplicates.new
|
|
321
|
+
model.roles = ["admin"]
|
|
322
|
+
|
|
323
|
+
expect(model.roles).to eq(["admin"])
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "leaves a backing value its own writer stored alone" do
|
|
327
|
+
model = CollectionReaderLivenessSpec::RolesOwnWriter.new
|
|
328
|
+
model.roles = [1, 2]
|
|
329
|
+
|
|
330
|
+
expect(model.roles).to eq([1, 2])
|
|
331
|
+
expect(model.instance_variable_get(:@roles)).to eq(Set.new([1, 2]))
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it "does not touch the collection when a frozen model rejects the write" do
|
|
335
|
+
model = CollectionReaderLivenessSpec::Roles.new(name: "n",
|
|
336
|
+
roles: ["admin"])
|
|
337
|
+
model.roles
|
|
338
|
+
model.freeze
|
|
339
|
+
|
|
340
|
+
expect { model.roles = ["reader"] }.to raise_error(FrozenError)
|
|
341
|
+
expect(model.instance_variable_get(:@roles)).to eq(["admin"])
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
describe "the register-scoped collection reader" do
|
|
346
|
+
# initialization.rb defines its own pair of collection methods. Round 5
|
|
347
|
+
# could not reach them through the public API, so drive the generator
|
|
348
|
+
# directly rather than assume the row is unreachable and therefore fine.
|
|
349
|
+
let(:host) do
|
|
350
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
351
|
+
def self.name
|
|
352
|
+
"CollectionReaderLivenessSpec::RegisterScopedHost"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
attribute :notes, :string, collection: true
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
let(:instance) do
|
|
360
|
+
host.send(:remove_method, :notes)
|
|
361
|
+
host.send(:remove_method, :notes=)
|
|
362
|
+
host.singleton_class.send(:public, :define_collection_register_methods)
|
|
363
|
+
host.define_collection_register_methods(:notes)
|
|
364
|
+
|
|
365
|
+
inst = host.allocate
|
|
366
|
+
inst.send(:finalize_deserialization, nil)
|
|
367
|
+
inst
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
it "is the reader under test" do
|
|
371
|
+
expect(instance.method(:notes).source_location.first)
|
|
372
|
+
.to end_with("serialize/initialization.rb")
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it "keeps an item pushed through it" do
|
|
376
|
+
instance.notes << "kept"
|
|
377
|
+
|
|
378
|
+
expect(instance.notes).to eq(["kept"])
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
it "keeps an item appended builder-style" do
|
|
382
|
+
instance.notes("kept")
|
|
383
|
+
|
|
384
|
+
expect(instance.notes).to eq(["kept"])
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
describe "the reference collection reader" do
|
|
389
|
+
let(:author) do
|
|
390
|
+
CollectionReaderLivenessSpec::RefAuthor.new(id: "a2", name: "Two")
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it "keeps an item pushed onto an empty reference collection" do
|
|
394
|
+
book = CollectionReaderLivenessSpec::RefBook.new(id: "b")
|
|
395
|
+
|
|
396
|
+
book.co_authors << author
|
|
397
|
+
|
|
398
|
+
expect(book.co_authors).to eq([author])
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
it "keeps an item pushed onto a populated reference collection" do
|
|
402
|
+
CollectionReaderLivenessSpec::RefAuthor.new(id: "a1", name: "One")
|
|
403
|
+
book = CollectionReaderLivenessSpec::RefBook.new(id: "b",
|
|
404
|
+
co_authors: ["a1"])
|
|
405
|
+
|
|
406
|
+
book.co_authors << author
|
|
407
|
+
|
|
408
|
+
expect(book.co_authors.map(&:id)).to eq(%w[a1 a2])
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it "carries that item into the document" do
|
|
412
|
+
book = CollectionReaderLivenessSpec::RefBook.new(id: "b")
|
|
413
|
+
book.co_authors << author
|
|
414
|
+
|
|
415
|
+
expect(book.to_xml.to_s).to include("<coAuthor>a2</coAuthor>")
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
it "still reads the keys back once the objects are stored" do
|
|
419
|
+
CollectionReaderLivenessSpec::RefAuthor.new(id: "a1", name: "One")
|
|
420
|
+
book = CollectionReaderLivenessSpec::RefBook.new(id: "b",
|
|
421
|
+
co_authors: ["a1"])
|
|
422
|
+
|
|
423
|
+
book.co_authors # resolves, and stores what it resolved
|
|
424
|
+
|
|
425
|
+
expect(book.co_authors_ids).to eq(["a1"])
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
it "stays live when the collection also holds a raw nil" do
|
|
429
|
+
CollectionReaderLivenessSpec::RefAuthor.new(id: "a1", name: "One")
|
|
430
|
+
book = CollectionReaderLivenessSpec::RefBook.new(
|
|
431
|
+
id: "b", co_authors: ["a1", nil],
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
book.co_authors << author
|
|
435
|
+
|
|
436
|
+
expect(book.co_authors.size).to eq(3)
|
|
437
|
+
expect(book.co_authors[1]).to be_nil
|
|
438
|
+
expect(book.co_authors.compact.map(&:id)).to eq(%w[a1 a2])
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
it "keeps re-resolving while any reference is still dangling" do
|
|
442
|
+
book = CollectionReaderLivenessSpec::RefBook.new(id: "b",
|
|
443
|
+
co_authors: ["late"])
|
|
444
|
+
|
|
445
|
+
expect(book.co_authors).to eq([nil])
|
|
446
|
+
|
|
447
|
+
CollectionReaderLivenessSpec::RefAuthor.new(id: "late", name: "Late")
|
|
448
|
+
|
|
449
|
+
expect(book.co_authors.map(&:id)).to eq(["late"])
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
describe "every construction entry point" do
|
|
454
|
+
# The readers above are reached through from_xml. A collection that the
|
|
455
|
+
# model actually holds has to come back live from all of them, not just
|
|
456
|
+
# that one.
|
|
457
|
+
let(:entry_points) do
|
|
458
|
+
{
|
|
459
|
+
"new" => -> { CollectionReaderLivenessSpec::Filled.new },
|
|
460
|
+
"new with attributes" => lambda {
|
|
461
|
+
CollectionReaderLivenessSpec::Filled.new(name: "n")
|
|
462
|
+
},
|
|
463
|
+
"from_xml" => lambda {
|
|
464
|
+
CollectionReaderLivenessSpec::Filled.from_xml(bare)
|
|
465
|
+
},
|
|
466
|
+
"from_json" => lambda {
|
|
467
|
+
CollectionReaderLivenessSpec::Filled.from_json('{"name":"n"}')
|
|
468
|
+
},
|
|
469
|
+
"from_yaml" => lambda {
|
|
470
|
+
CollectionReaderLivenessSpec::Filled.from_yaml("name: n\n")
|
|
471
|
+
},
|
|
472
|
+
"from_hash" => lambda {
|
|
473
|
+
CollectionReaderLivenessSpec::Filled.from_hash({ "name" => "n" })
|
|
474
|
+
},
|
|
475
|
+
"from_toml" => lambda {
|
|
476
|
+
CollectionReaderLivenessSpec::Filled.from_toml(%(name = "n"\n))
|
|
477
|
+
},
|
|
478
|
+
"dup" => lambda {
|
|
479
|
+
CollectionReaderLivenessSpec::Filled.from_xml(bare).dup
|
|
480
|
+
},
|
|
481
|
+
"clone" => lambda {
|
|
482
|
+
CollectionReaderLivenessSpec::Filled.from_xml(bare).clone
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
it "hands back the same array on every read" do
|
|
488
|
+
results = entry_points.transform_values do |build|
|
|
489
|
+
model = build.call
|
|
490
|
+
model.items.equal?(model.items)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
expect(results).to all(satisfy { |_name, same| same })
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
it "keeps an item pushed through the reader" do
|
|
497
|
+
results = entry_points.transform_values do |build|
|
|
498
|
+
model = build.call
|
|
499
|
+
model.items << "kept"
|
|
500
|
+
model.items
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
expect(results.values).to all(eq(["kept"]))
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
it "carries that item into the document" do
|
|
507
|
+
results = entry_points.transform_values do |build|
|
|
508
|
+
model = build.call
|
|
509
|
+
model.items << "kept"
|
|
510
|
+
model.to_xml.to_s.include?("<item>kept</item>")
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
expect(results).to all(satisfy { |_name, reached| reached })
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|