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,486 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require_relative "../../../lib/lutaml/model"
|
|
3
|
+
|
|
4
|
+
# Regression coverage for the parsed-vs-built divergence: a model parsed from
|
|
5
|
+
# a document carries element_order, and mutations made after parsing used to
|
|
6
|
+
# be dropped during serialization.
|
|
7
|
+
#
|
|
8
|
+
# Every example here PARSES input, mutates, then serializes, and asserts on
|
|
9
|
+
# the emitted document. Asserting on attribute reads is what hid these bugs —
|
|
10
|
+
# the value always reads back correctly.
|
|
11
|
+
module ParsedModelMutationSpec
|
|
12
|
+
class Child < Lutaml::Model::Serializable
|
|
13
|
+
attribute :v, :string
|
|
14
|
+
|
|
15
|
+
xml do
|
|
16
|
+
root "child"
|
|
17
|
+
map_attribute "v", to: :v
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Mixed < Lutaml::Model::Serializable
|
|
22
|
+
attribute :a, Child
|
|
23
|
+
attribute :b, Child
|
|
24
|
+
|
|
25
|
+
xml do
|
|
26
|
+
root "p"
|
|
27
|
+
mixed_content
|
|
28
|
+
map_element "a", to: :a
|
|
29
|
+
map_element "b", to: :b
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class MixColl < Lutaml::Model::Serializable
|
|
34
|
+
attribute :a, Child, collection: true
|
|
35
|
+
attribute :b, Child
|
|
36
|
+
|
|
37
|
+
xml do
|
|
38
|
+
root "p"
|
|
39
|
+
mixed_content
|
|
40
|
+
map_element "a", to: :a
|
|
41
|
+
map_element "b", to: :b
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class EmptyColl < Lutaml::Model::Serializable
|
|
46
|
+
attribute :lead, :string
|
|
47
|
+
attribute :blank_topic, :string, collection: true
|
|
48
|
+
attribute :nil_topic, :string, collection: true
|
|
49
|
+
attribute :plain_topic, :string, collection: true
|
|
50
|
+
|
|
51
|
+
xml do
|
|
52
|
+
root "s"
|
|
53
|
+
ordered
|
|
54
|
+
map_element "lead", to: :lead
|
|
55
|
+
map_element "blank", to: :blank_topic, render_empty: :as_blank
|
|
56
|
+
map_element "nil", to: :nil_topic, render_empty: :as_nil
|
|
57
|
+
map_element "plain", to: :plain_topic
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class NilColl < Lutaml::Model::Serializable
|
|
62
|
+
attribute :lead, :string
|
|
63
|
+
attribute :items, :string, collection: true
|
|
64
|
+
|
|
65
|
+
xml do
|
|
66
|
+
root "s"
|
|
67
|
+
ordered
|
|
68
|
+
map_element "lead", to: :lead
|
|
69
|
+
map_element "item", to: :items, render_nil: :as_nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class DefaultColl < Lutaml::Model::Serializable
|
|
74
|
+
attribute :lead, :string
|
|
75
|
+
attribute :items, :string, collection: true, default: -> { [] }
|
|
76
|
+
|
|
77
|
+
xml do
|
|
78
|
+
root "s"
|
|
79
|
+
ordered
|
|
80
|
+
map_element "lead", to: :lead
|
|
81
|
+
map_element "item", to: :items
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class DerivedOrdered < Lutaml::Model::Serializable
|
|
86
|
+
attribute :lead, :string
|
|
87
|
+
attribute :calc, :string, method: :calc_value
|
|
88
|
+
|
|
89
|
+
xml do
|
|
90
|
+
root "s"
|
|
91
|
+
ordered
|
|
92
|
+
map_element "lead", to: :lead
|
|
93
|
+
map_element "calc", to: :calc
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def calc_value
|
|
97
|
+
"D"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class Aliased < Lutaml::Model::Serializable
|
|
102
|
+
attribute :items, :string, collection: true
|
|
103
|
+
|
|
104
|
+
xml do
|
|
105
|
+
root "p"
|
|
106
|
+
ordered
|
|
107
|
+
map_element %w[item old-item], to: :items
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class Glaze < Lutaml::Model::Serializable
|
|
112
|
+
attribute :color, :string
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class Delegating < Lutaml::Model::Serializable
|
|
116
|
+
attribute :glaze, Glaze
|
|
117
|
+
attribute :other, :string
|
|
118
|
+
|
|
119
|
+
xml do
|
|
120
|
+
root "d"
|
|
121
|
+
ordered
|
|
122
|
+
map_element "color", to: :color, delegate: :glaze
|
|
123
|
+
map_element "other", to: :other
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class CustomColl < Lutaml::Model::Serializable
|
|
128
|
+
attribute :items, :string, collection: true
|
|
129
|
+
|
|
130
|
+
xml do
|
|
131
|
+
root "p"
|
|
132
|
+
ordered
|
|
133
|
+
map_element "item", to: :items, with: { to: :items_to, from: :items_from }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def items_to(model, parent, doc)
|
|
137
|
+
Array(model.items).each do |v|
|
|
138
|
+
el = doc.create_element("item")
|
|
139
|
+
doc.add_text(el, v)
|
|
140
|
+
parent.add_child(el)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def items_from(model, values)
|
|
145
|
+
model.items = Array(values).map(&:to_s)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class Inner < Lutaml::Model::Serializable
|
|
150
|
+
attribute :x, :string
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
class DelegatingAlias < Lutaml::Model::Serializable
|
|
154
|
+
attribute :inner, Inner
|
|
155
|
+
attribute :other, :string
|
|
156
|
+
|
|
157
|
+
xml do
|
|
158
|
+
root "d"
|
|
159
|
+
ordered
|
|
160
|
+
map_element %w[x old-x], to: :x, delegate: :inner
|
|
161
|
+
map_element "other", to: :other
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class SameNameA < Lutaml::Model::Serializable
|
|
166
|
+
attribute :v, :string
|
|
167
|
+
|
|
168
|
+
xml do
|
|
169
|
+
root "sa"
|
|
170
|
+
map_attribute "v", to: :v
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
class SameNameB < Lutaml::Model::Serializable
|
|
175
|
+
attribute :w, :string
|
|
176
|
+
|
|
177
|
+
xml do
|
|
178
|
+
root "sb"
|
|
179
|
+
map_attribute "w", to: :w
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class AmbiguousNames < Lutaml::Model::Serializable
|
|
184
|
+
attribute :a, SameNameA
|
|
185
|
+
attribute :b, SameNameB
|
|
186
|
+
|
|
187
|
+
xml do
|
|
188
|
+
root "p"
|
|
189
|
+
ordered
|
|
190
|
+
map_element "same", to: :a
|
|
191
|
+
map_element "same", to: :b
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
class CustomMethod < Lutaml::Model::Serializable
|
|
196
|
+
attribute :name, :string
|
|
197
|
+
|
|
198
|
+
xml do
|
|
199
|
+
root "c"
|
|
200
|
+
ordered
|
|
201
|
+
map_element "label", with: { to: :label_to_xml, from: :label_from_xml }
|
|
202
|
+
map_element "name", to: :name
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def label_to_xml(model, parent, doc)
|
|
206
|
+
el = doc.create_element("label")
|
|
207
|
+
doc.add_text(el, "L:#{model.name}")
|
|
208
|
+
parent.add_child(el)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def label_from_xml(model, value)
|
|
212
|
+
model.name = Array(value).first.to_s.sub(/^L:/, "")
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
RSpec.describe "parsed model mutation" do
|
|
218
|
+
# Defect 1: mixed_content dropped an attribute set after parse.
|
|
219
|
+
describe "singular element set after parse" do
|
|
220
|
+
it "emits the element that was set" do
|
|
221
|
+
model = ParsedModelMutationSpec::Mixed.from_xml('<p><a v="1"/></p>')
|
|
222
|
+
model.b = ParsedModelMutationSpec::Child.new(v: "2")
|
|
223
|
+
|
|
224
|
+
expect(model.to_xml).to include('<b v="2"/>').or include('<b v="2"></b>')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "emits it in mapping-declaration order" do
|
|
228
|
+
model = ParsedModelMutationSpec::Mixed.from_xml('<p><b v="2"/></p>')
|
|
229
|
+
model.a = ParsedModelMutationSpec::Child.new(v: "1")
|
|
230
|
+
|
|
231
|
+
expect(model.to_xml.index("<a ")).to be < model.to_xml.index("<b ")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "leaves an untouched round-trip unchanged" do
|
|
235
|
+
xml = '<p><a v="1"/></p>'
|
|
236
|
+
model = ParsedModelMutationSpec::Mixed.from_xml(xml)
|
|
237
|
+
|
|
238
|
+
expect(model.to_xml).not_to include("<b")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "is idempotent across repeated serialization" do
|
|
242
|
+
model = ParsedModelMutationSpec::Mixed.from_xml('<p><a v="1"/></p>')
|
|
243
|
+
model.b = ParsedModelMutationSpec::Child.new(v: "2")
|
|
244
|
+
|
|
245
|
+
first = model.to_xml
|
|
246
|
+
second = model.to_xml
|
|
247
|
+
|
|
248
|
+
expect(second).to eq(first)
|
|
249
|
+
expect(second.scan("<b").size).to eq(1)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Defect 2: mixed_content ignored collection growth.
|
|
254
|
+
describe "collection grown after parse" do
|
|
255
|
+
it "emits every item" do
|
|
256
|
+
model = ParsedModelMutationSpec::MixColl.from_xml('<p><a v="1"/></p>')
|
|
257
|
+
model.a = [
|
|
258
|
+
ParsedModelMutationSpec::Child.new(v: "1"),
|
|
259
|
+
ParsedModelMutationSpec::Child.new(v: "2"),
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
expect(model.to_xml.scan("<a ").size).to eq(2)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "still emits the right count when the collection shrinks" do
|
|
266
|
+
model = ParsedModelMutationSpec::MixColl
|
|
267
|
+
.from_xml('<p><a v="1"/><a v="2"/></p>')
|
|
268
|
+
model.a = [ParsedModelMutationSpec::Child.new(v: "9")]
|
|
269
|
+
|
|
270
|
+
expect(model.to_xml.scan("<a ").size).to eq(1)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
it "appends new items after existing ones, not before an interleaved element" do
|
|
274
|
+
model = ParsedModelMutationSpec::MixColl
|
|
275
|
+
.from_xml('<p><a v="1"/><b v="B"/><a v="2"/></p>')
|
|
276
|
+
model.a = [
|
|
277
|
+
ParsedModelMutationSpec::Child.new(v: "1"),
|
|
278
|
+
ParsedModelMutationSpec::Child.new(v: "2"),
|
|
279
|
+
ParsedModelMutationSpec::Child.new(v: "3"),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
xml = model.to_xml
|
|
283
|
+
expect(xml.index('v="3"')).to be > xml.index('v="2"')
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it "shares one counter across canonical and alias element names" do
|
|
287
|
+
model = ParsedModelMutationSpec::Aliased
|
|
288
|
+
.from_xml("<p><old-item>x</old-item></p>")
|
|
289
|
+
model.items = %w[x y]
|
|
290
|
+
|
|
291
|
+
xml = model.to_xml
|
|
292
|
+
expect(xml).to include("x")
|
|
293
|
+
expect(xml).to include("y")
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Defect 3: element_order was frozen on parsed models.
|
|
298
|
+
describe "element_order on a parsed model" do
|
|
299
|
+
it "is mutable" do
|
|
300
|
+
model = ParsedModelMutationSpec::Mixed.from_xml('<p><a v="1"/></p>')
|
|
301
|
+
|
|
302
|
+
expect { model.element_order << "x" }.not_to raise_error
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
describe "empty collection assigned after parse" do
|
|
307
|
+
it "emits a blank element under render_empty: :as_blank" do
|
|
308
|
+
model = ParsedModelMutationSpec::EmptyColl.from_xml("<s><lead>L</lead></s>")
|
|
309
|
+
model.blank_topic = []
|
|
310
|
+
|
|
311
|
+
expect(model.to_xml).to include("<blank")
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "emits a nil element under render_empty: :as_nil" do
|
|
315
|
+
model = ParsedModelMutationSpec::EmptyColl.from_xml("<s><lead>L</lead></s>")
|
|
316
|
+
model.nil_topic = []
|
|
317
|
+
|
|
318
|
+
expect(model.to_xml).to include("<nil")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
it "emits nothing without render_empty" do
|
|
322
|
+
model = ParsedModelMutationSpec::EmptyColl.from_xml("<s><lead>L</lead></s>")
|
|
323
|
+
model.plain_topic = []
|
|
324
|
+
|
|
325
|
+
expect(model.to_xml).not_to include("<plain")
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# A collection set to nil is never iterated, so it must be counted as one
|
|
330
|
+
# element rather than measured for length.
|
|
331
|
+
describe "collection set to nil after parse" do
|
|
332
|
+
it "renders it as a nil element under render_nil" do
|
|
333
|
+
model = ParsedModelMutationSpec::NilColl
|
|
334
|
+
.from_xml("<s><lead>L</lead><item>x</item></s>")
|
|
335
|
+
model.items = nil
|
|
336
|
+
|
|
337
|
+
expect(model.to_xml).to include("nil=\"true\"")
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it "emits nothing when the element was absent" do
|
|
341
|
+
model = ParsedModelMutationSpec::NilColl.from_xml("<s><lead>L</lead></s>")
|
|
342
|
+
model.items = nil
|
|
343
|
+
|
|
344
|
+
expect(model.to_xml).not_to include("<item")
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Pushing onto a default collection never reaches the setter, so the
|
|
349
|
+
# attribute still reads as "using default" while holding real data.
|
|
350
|
+
describe "default collection mutated in place" do
|
|
351
|
+
it "emits the pushed item on a built model" do
|
|
352
|
+
model = ParsedModelMutationSpec::DefaultColl.new { |m| m.lead = "L" }
|
|
353
|
+
model.items << "x"
|
|
354
|
+
|
|
355
|
+
expect(model.to_xml).to include("<item>x</item>")
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
it "emits the pushed item on a parsed model" do
|
|
359
|
+
model = ParsedModelMutationSpec::DefaultColl
|
|
360
|
+
.from_xml("<s><lead>L</lead></s>")
|
|
361
|
+
model.items << "y"
|
|
362
|
+
|
|
363
|
+
expect(model.to_xml).to include("<item>y</item>")
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it "emits nothing when the default collection stays empty" do
|
|
367
|
+
model = ParsedModelMutationSpec::DefaultColl
|
|
368
|
+
.from_xml("<s><lead>L</lead></s>")
|
|
369
|
+
|
|
370
|
+
expect(model.to_xml).not_to include("<item")
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# A derived attribute always reads as "using default", so the default
|
|
375
|
+
# guard has to exempt it the way RenderPolicy does.
|
|
376
|
+
describe "derived attribute on an ordered mapping" do
|
|
377
|
+
it "is emitted whether or not element_order is populated" do
|
|
378
|
+
empty_order = ParsedModelMutationSpec::DerivedOrdered.from_xml("<s/>")
|
|
379
|
+
with_order = ParsedModelMutationSpec::DerivedOrdered
|
|
380
|
+
.from_xml("<s><lead>L</lead></s>")
|
|
381
|
+
|
|
382
|
+
expect(empty_order.to_xml).to include("<calc>D</calc>")
|
|
383
|
+
expect(with_order.to_xml).to include("<calc>D</calc>")
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
describe "delegated element set after parse" do
|
|
388
|
+
it "emits the delegated value" do
|
|
389
|
+
model = ParsedModelMutationSpec::Delegating.from_xml("<d><other>o</other></d>")
|
|
390
|
+
model.glaze = ParsedModelMutationSpec::Glaze.new(color: "red")
|
|
391
|
+
|
|
392
|
+
expect(model.to_xml).to include("red")
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
describe "custom-method element rule on an ordered mapping" do
|
|
397
|
+
it "serializes without raising" do
|
|
398
|
+
model = ParsedModelMutationSpec::CustomMethod
|
|
399
|
+
.from_xml("<c><label>L:x</label><name>x</name></c>")
|
|
400
|
+
|
|
401
|
+
expect { model.to_xml }.not_to raise_error
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# A custom `to:` emits the whole collection itself and is invoked once
|
|
405
|
+
# per matching entry, so one entry must mean one invocation.
|
|
406
|
+
it "invokes a custom collection transform once, not once per item" do
|
|
407
|
+
model = ParsedModelMutationSpec::CustomColl.from_xml("<p><item>a</item></p>")
|
|
408
|
+
model.items = %w[a b]
|
|
409
|
+
|
|
410
|
+
expect(model.to_xml.scan("<item>").size).to eq(2)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Two rules mapping one element name cannot be told apart by the ordered
|
|
415
|
+
# dispatcher, which resolves an entry to the first matching rule. That is
|
|
416
|
+
# a pre-existing limitation, so these specs pin the boundaries of what
|
|
417
|
+
# reconciliation may do around it rather than the dispatcher's output.
|
|
418
|
+
describe "two element rules sharing one serialized name" do
|
|
419
|
+
it "emits both values when built" do
|
|
420
|
+
model = ParsedModelMutationSpec::AmbiguousNames.new do |m|
|
|
421
|
+
m.a = ParsedModelMutationSpec::SameNameA.new(v: "1")
|
|
422
|
+
m.b = ParsedModelMutationSpec::SameNameB.new(w: "2")
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
expect(model.to_xml).to include('v="1"')
|
|
426
|
+
expect(model.to_xml).to include('w="2"')
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# A parsed entry stands for every rule that shares its name, so
|
|
430
|
+
# reconciliation must not emit the ones the dispatcher skipped —
|
|
431
|
+
# doing so duplicates the element on a plain round-trip.
|
|
432
|
+
it "does not duplicate the element on a round-trip" do
|
|
433
|
+
xml = '<p><same v="1"/></p>'
|
|
434
|
+
model = ParsedModelMutationSpec::AmbiguousNames.from_xml(xml)
|
|
435
|
+
|
|
436
|
+
expect(model.to_xml.scan("<same").size).to eq(1)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
describe "delegated element with name aliases" do
|
|
441
|
+
it "keeps the parsed document order" do
|
|
442
|
+
model = ParsedModelMutationSpec::DelegatingAlias
|
|
443
|
+
.from_xml("<d><other>o</other><old-x>V</old-x></d>")
|
|
444
|
+
|
|
445
|
+
xml = model.to_xml
|
|
446
|
+
expect(xml).to include("V")
|
|
447
|
+
expect(xml.index("other")).to be < xml.index("V")
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
describe "builder-block construction" do
|
|
452
|
+
it "emits attributes supplied by hash in declaration order" do
|
|
453
|
+
model = ParsedModelMutationSpec::EmptyColl.new(lead: "L") do |m|
|
|
454
|
+
m.plain_topic = ["t"]
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
xml = model.to_xml
|
|
458
|
+
expect(xml.index("<lead")).to be < xml.index("<plain")
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
describe "foreign entries in element_order" do
|
|
463
|
+
# element_order is a plain public accessor, so arrays holding objects
|
|
464
|
+
# that are not order entries (no #type) exist in the wild. Regression:
|
|
465
|
+
# reconciliation used to call #type on every entry and crash with
|
|
466
|
+
# NoMethodError for an instance of Integer.
|
|
467
|
+
it "serializes without raising when element_order holds an Integer" do
|
|
468
|
+
model = ParsedModelMutationSpec::NilColl
|
|
469
|
+
.from_xml("<s><lead>L</lead><item>a</item></s>")
|
|
470
|
+
model.element_order << 42
|
|
471
|
+
|
|
472
|
+
expect { model.to_xml }.not_to raise_error
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
it "still reconciles the well-formed entries around a foreign one" do
|
|
476
|
+
model = ParsedModelMutationSpec::NilColl
|
|
477
|
+
.from_xml("<s><lead>L</lead><item>a</item></s>")
|
|
478
|
+
model.element_order.insert(1, 42)
|
|
479
|
+
model.items << "b"
|
|
480
|
+
|
|
481
|
+
xml = model.to_xml
|
|
482
|
+
expect(xml.scan(%r{<lead>L</lead>|<item>a</item>|<item>b</item>}))
|
|
483
|
+
.to eq(["<lead>L</lead>", "<item>a</item>", "<item>b</item>"])
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|