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,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "whiteSpace facet (cast-time normalization)" do
|
|
6
|
+
describe ":collapse mode" do
|
|
7
|
+
before do
|
|
8
|
+
stub_const("CollapsedToken", Class.new(Lutaml::Model::Type::String) do
|
|
9
|
+
white_space :collapse
|
|
10
|
+
end)
|
|
11
|
+
stub_const("Doc", Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :token, CollapsedToken
|
|
13
|
+
end)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "collapses internal runs and strips edges at cast time" do
|
|
17
|
+
expect(CollapsedToken.cast(" a\t b\n\n c ")).to eq("a b c")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "normalizes the value stored on the model instance" do
|
|
21
|
+
expect(Doc.new(token: " hello world ").token).to eq("hello world")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "normalizes values loaded from a serialized document" do
|
|
25
|
+
expect(Doc.from_yaml("token: \" hello world \"\n").token)
|
|
26
|
+
.to eq("hello world")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe ":replace mode" do
|
|
31
|
+
before do
|
|
32
|
+
stub_const("ReplacedToken", Class.new(Lutaml::Model::Type::String) do
|
|
33
|
+
white_space :replace
|
|
34
|
+
end)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "replaces tab/newline/carriage-return with a single space" do
|
|
38
|
+
expect(ReplacedToken.cast("a\tb\nc\rd")).to eq("a b c d")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "keeps existing spaces and edges (no collapse, no strip)" do
|
|
42
|
+
expect(ReplacedToken.cast(" a\tb ")).to eq(" a b ")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe ":preserve mode" do
|
|
47
|
+
before do
|
|
48
|
+
stub_const("PreservedToken", Class.new(Lutaml::Model::Type::String) do
|
|
49
|
+
white_space :preserve
|
|
50
|
+
end)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "leaves the value untouched" do
|
|
54
|
+
expect(PreservedToken.cast(" a\t b ")).to eq(" a\t b ")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "declaration guards" do
|
|
59
|
+
it "rejects an unknown mode" do
|
|
60
|
+
expect do
|
|
61
|
+
Class.new(Lutaml::Model::Type::String) { white_space :squash }
|
|
62
|
+
end.to raise_error(ArgumentError, /must be one of/)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "rejects white_space on a non-string type at declaration" do
|
|
66
|
+
expect do
|
|
67
|
+
Class.new(Lutaml::Model::Type::Integer) { white_space :collapse }
|
|
68
|
+
end.to raise_error(ArgumentError, /string-derived/)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "inheritance (stricter wins)" do
|
|
73
|
+
before do
|
|
74
|
+
stub_const("ReplaceBase", Class.new(Lutaml::Model::Type::String) do
|
|
75
|
+
white_space :replace
|
|
76
|
+
end)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "lets a child tighten to a stricter mode" do
|
|
80
|
+
child = Class.new(ReplaceBase) { white_space :collapse }
|
|
81
|
+
|
|
82
|
+
expect(child.facets[:white_space]).to eq(:collapse)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "raises when a child loosens the inherited mode" do
|
|
86
|
+
child = Class.new(ReplaceBase) { white_space :preserve }
|
|
87
|
+
|
|
88
|
+
expect { child.facets }.to raise_error(ArgumentError, /cannot widen/)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "computes each subclass's effective mode independently" do
|
|
92
|
+
strict = Class.new(ReplaceBase) { white_space :collapse }
|
|
93
|
+
plain = Class.new(ReplaceBase)
|
|
94
|
+
|
|
95
|
+
expect(strict.white_space_mode).to eq(:collapse)
|
|
96
|
+
expect(plain.white_space_mode).to eq(:replace)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "memo invalidation when a facet is declared after a cast" do
|
|
101
|
+
it "recomputes the effective mode for a cast-before-declare sequence" do
|
|
102
|
+
cls = Class.new(Lutaml::Model::Type::String)
|
|
103
|
+
expect(cls.cast("a\tb")).to eq("a\tb")
|
|
104
|
+
|
|
105
|
+
cls.white_space :collapse
|
|
106
|
+
|
|
107
|
+
expect(cls.white_space_mode).to eq(:collapse)
|
|
108
|
+
expect(cls.cast(" a\t b ")).to eq("a b")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe "plain :string is unaffected (fast path intact)" do
|
|
113
|
+
before do
|
|
114
|
+
stub_const("Plain", Class.new(Lutaml::Model::Serializable) do
|
|
115
|
+
attribute :s, :string
|
|
116
|
+
end)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "does not normalize a plain string attribute" do
|
|
120
|
+
expect(Plain.new(s: " a b ").s).to eq(" a b ")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "returns the identical object via the EMPTY_OPTIONS fast path" do
|
|
124
|
+
raw = " a b "
|
|
125
|
+
cast = Lutaml::Model::Type::String.cast(
|
|
126
|
+
raw, Lutaml::Model::Type::Value::EMPTY_OPTIONS
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
expect(cast).to equal(raw)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -146,6 +146,308 @@ RSpec.describe Lutaml::Model::Schema::XmlCompiler do
|
|
|
146
146
|
end
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
+
context "when compiling constraining facets (issue #191 from_xsd)" do
|
|
150
|
+
let!(:facets_dir) { Dir.mktmpdir }
|
|
151
|
+
let(:item) { RestrictionFacetsSpec::CTItem }
|
|
152
|
+
|
|
153
|
+
before do
|
|
154
|
+
described_class.to_models(
|
|
155
|
+
File.read("spec/fixtures/xml/restriction_facets.xsd"),
|
|
156
|
+
output_dir: facets_dir,
|
|
157
|
+
create_files: true,
|
|
158
|
+
module_namespace: "RestrictionFacetsSpec",
|
|
159
|
+
)
|
|
160
|
+
require File.join(facets_dir, "restrictionfacetsspec_registry.rb")
|
|
161
|
+
RestrictionFacetsSpec.register_all
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
after do
|
|
165
|
+
FileUtils.rm_rf(facets_dir)
|
|
166
|
+
if defined?(RestrictionFacetsSpec)
|
|
167
|
+
Object.send(:remove_const, :RestrictionFacetsSpec)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "validates a fully in-range item with no errors" do
|
|
172
|
+
model = item.new(code: "ab", ratio: 50, price: "123.45",
|
|
173
|
+
moment: "2020-06-15T12:00:00")
|
|
174
|
+
expect(model.validate).to be_empty
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "collects a MinLengthError for a too-short string (minLength)" do
|
|
178
|
+
expect(item.new(code: "a").validate.first)
|
|
179
|
+
.to be_a(Lutaml::Model::MinLengthError)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "collects a MaxLengthError for a too-long string (maxLength)" do
|
|
183
|
+
expect(item.new(code: "toolongvalue").validate.first)
|
|
184
|
+
.to be_a(Lutaml::Model::MaxLengthError)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "collects a MaxExclusiveError at the exclusive upper bound" do
|
|
188
|
+
expect(item.new(ratio: 100).validate.first)
|
|
189
|
+
.to be_a(Lutaml::Model::MaxExclusiveError)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "collects a MinExclusiveError at the exclusive lower bound" do
|
|
193
|
+
expect(item.new(ratio: 0).validate.first)
|
|
194
|
+
.to be_a(Lutaml::Model::MinExclusiveError)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "collects a TotalDigitsError beyond the total-digit cap" do
|
|
198
|
+
expect(item.new(price: "1234.56").validate.first)
|
|
199
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "collects a FractionDigitsError beyond the fraction-digit cap" do
|
|
203
|
+
expect(item.new(price: "1.234").validate.first)
|
|
204
|
+
.to be_a(Lutaml::Model::FractionDigitsError)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "collects a MinInclusiveError before the temporal lower bound" do
|
|
208
|
+
expect(item.new(moment: "2019-12-31T00:00:00").validate.first)
|
|
209
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "accepts a boolean value allowed by a cast enumeration" do
|
|
213
|
+
expect(item.new(flag: true).validate).to be_empty
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "collects an InvalidValueError for a disallowed boolean enum value" do
|
|
217
|
+
expect(item.new(flag: false).validate.first)
|
|
218
|
+
.to be_a(Lutaml::Model::InvalidValueError)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "normalizes whitespace at cast time (whiteSpace collapse)" do
|
|
222
|
+
expect(item.new(label: " a b ").label).to eq("a b")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "carries the error attribute name" do
|
|
226
|
+
error = item.new(code: "a").validate.first
|
|
227
|
+
expect(error.to_s).to include("code")
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
context "when compiling a built-in interpolated pattern (issue #191 anyURI)" do
|
|
232
|
+
let!(:uri_dir) { Dir.mktmpdir }
|
|
233
|
+
let(:link) { AnyUriPatternSpec::CTLink }
|
|
234
|
+
let(:uri_schema) do
|
|
235
|
+
<<~XSD
|
|
236
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
237
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
238
|
+
<xsd:complexType name="CT_Link">
|
|
239
|
+
<xsd:attribute name="href" type="xsd:anyURI" />
|
|
240
|
+
</xsd:complexType>
|
|
241
|
+
<xsd:element name="Link" type="CT_Link" />
|
|
242
|
+
</xsd:schema>
|
|
243
|
+
XSD
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
before do
|
|
247
|
+
described_class.to_models(
|
|
248
|
+
uri_schema,
|
|
249
|
+
output_dir: uri_dir,
|
|
250
|
+
create_files: true,
|
|
251
|
+
module_namespace: "AnyUriPatternSpec",
|
|
252
|
+
)
|
|
253
|
+
require File.join(uri_dir, "anyuripatternspec_registry.rb")
|
|
254
|
+
AnyUriPatternSpec.register_all
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
after do
|
|
258
|
+
FileUtils.rm_rf(uri_dir)
|
|
259
|
+
if defined?(AnyUriPatternSpec)
|
|
260
|
+
Object.send(:remove_const, :AnyUriPatternSpec)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Regression: the lazy pattern facet must keep the built-in anyURI
|
|
265
|
+
# pattern's live `#{...}` interpolation, else every valid URI is
|
|
266
|
+
# falsely rejected at validate time.
|
|
267
|
+
it "accepts a valid URI through the lazy interpolated pattern facet" do
|
|
268
|
+
expect(link.new(href: "http://example.com/a?b=1").validate)
|
|
269
|
+
.to be_empty
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "rejects an invalid URI at cast time" do
|
|
273
|
+
expect { link.new(href: "has spaces here") }
|
|
274
|
+
.to raise_error(Lutaml::Model::Type::PatternNotMatchedError)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
context "when a restriction repeats a bound (issue #191 tightest-wins)" do
|
|
279
|
+
let(:tight_schema) do
|
|
280
|
+
<<~XSD
|
|
281
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
282
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
283
|
+
<xsd:simpleType name="Tight">
|
|
284
|
+
<xsd:restriction base="xsd:integer">
|
|
285
|
+
<xsd:minExclusive value="5" />
|
|
286
|
+
<xsd:minExclusive value="10" />
|
|
287
|
+
<xsd:maxExclusive value="99" />
|
|
288
|
+
<xsd:maxExclusive value="90" />
|
|
289
|
+
</xsd:restriction>
|
|
290
|
+
</xsd:simpleType>
|
|
291
|
+
</xsd:schema>
|
|
292
|
+
XSD
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Exclusive bounds must tighten like the inclusive ones: the largest
|
|
296
|
+
# lower bound and smallest upper bound win, so no out-of-range value
|
|
297
|
+
# slips through the generated facets.
|
|
298
|
+
it "carries the tightest exclusive bounds" do
|
|
299
|
+
code = described_class.to_models(tight_schema).fetch("Tight")
|
|
300
|
+
expect(code).to include("exclusive min: 10, max: 90")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it "orders repeated decimal bounds numerically, not lexically" do
|
|
304
|
+
schema = <<~XSD
|
|
305
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
306
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
307
|
+
<xsd:simpleType name="D">
|
|
308
|
+
<xsd:restriction base="xsd:decimal">
|
|
309
|
+
<xsd:minInclusive value="9.5" />
|
|
310
|
+
<xsd:minInclusive value="10.5" />
|
|
311
|
+
</xsd:restriction>
|
|
312
|
+
</xsd:simpleType>
|
|
313
|
+
</xsd:schema>
|
|
314
|
+
XSD
|
|
315
|
+
code = described_class.to_models(schema).fetch("D")
|
|
316
|
+
expect(code).to include('inclusive min: BigDecimal("10.5")')
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
context "when a restriction derives from a user-defined atomic type " \
|
|
321
|
+
"(issue #191 chained base)" do
|
|
322
|
+
# Deriving a tighter type from a named numeric/decimal/temporal type is
|
|
323
|
+
# valid, common XSD. The further restriction's base is the user type
|
|
324
|
+
# (not a built-in), so its bound/enumeration must render through the
|
|
325
|
+
# parent's `cast` — a bare string would crash `<=>`/never equal the
|
|
326
|
+
# cast value at validation.
|
|
327
|
+
let(:chained_schema) do
|
|
328
|
+
<<~XSD
|
|
329
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
330
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
331
|
+
<xsd:simpleType name="Amount">
|
|
332
|
+
<xsd:restriction base="xsd:integer">
|
|
333
|
+
<xsd:minInclusive value="0" />
|
|
334
|
+
</xsd:restriction>
|
|
335
|
+
</xsd:simpleType>
|
|
336
|
+
<xsd:simpleType name="SmallAmount">
|
|
337
|
+
<xsd:restriction base="Amount">
|
|
338
|
+
<xsd:maxInclusive value="100" />
|
|
339
|
+
</xsd:restriction>
|
|
340
|
+
</xsd:simpleType>
|
|
341
|
+
</xsd:schema>
|
|
342
|
+
XSD
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
it "renders the chained bound through the parent type's cast" do
|
|
346
|
+
code = described_class.to_models(chained_schema).fetch("SmallAmount")
|
|
347
|
+
expect(code).to include('inclusive max: Amount.cast("100")')
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it "renders a chained enumeration through the parent type's cast" do
|
|
351
|
+
schema = <<~XSD
|
|
352
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
353
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
354
|
+
<xsd:simpleType name="Amount">
|
|
355
|
+
<xsd:restriction base="xsd:integer">
|
|
356
|
+
<xsd:minInclusive value="0" />
|
|
357
|
+
</xsd:restriction>
|
|
358
|
+
</xsd:simpleType>
|
|
359
|
+
<xsd:simpleType name="Choice">
|
|
360
|
+
<xsd:restriction base="Amount">
|
|
361
|
+
<xsd:enumeration value="5" />
|
|
362
|
+
<xsd:enumeration value="10" />
|
|
363
|
+
</xsd:restriction>
|
|
364
|
+
</xsd:simpleType>
|
|
365
|
+
</xsd:schema>
|
|
366
|
+
XSD
|
|
367
|
+
code = described_class.to_models(schema).fetch("Choice")
|
|
368
|
+
expect(code).to include("enumeration(Amount.cast(\"5\"), Amount.cast(\"10\"))")
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
context "when compiling non-integer atomic bases (issue #191)" do
|
|
373
|
+
let!(:atomic_dir) { Dir.mktmpdir }
|
|
374
|
+
let(:atomic_schema) do
|
|
375
|
+
<<~XSD
|
|
376
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
377
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
378
|
+
<xsd:simpleType name="ST_Money">
|
|
379
|
+
<xsd:restriction base="xsd:decimal">
|
|
380
|
+
<xsd:minExclusive value="1.5" />
|
|
381
|
+
<xsd:maxExclusive value="9.5" />
|
|
382
|
+
</xsd:restriction>
|
|
383
|
+
</xsd:simpleType>
|
|
384
|
+
<xsd:simpleType name="ST_Rate">
|
|
385
|
+
<xsd:restriction base="xsd:float">
|
|
386
|
+
<xsd:minInclusive value="1.25" />
|
|
387
|
+
<xsd:maxInclusive value="9.75" />
|
|
388
|
+
</xsd:restriction>
|
|
389
|
+
</xsd:simpleType>
|
|
390
|
+
<xsd:simpleType name="ST_Day">
|
|
391
|
+
<xsd:restriction base="xsd:date">
|
|
392
|
+
<xsd:minInclusive value="2020-01-01" />
|
|
393
|
+
</xsd:restriction>
|
|
394
|
+
</xsd:simpleType>
|
|
395
|
+
<xsd:complexType name="CT_Rec">
|
|
396
|
+
<xsd:attribute name="money" type="ST_Money" />
|
|
397
|
+
<xsd:attribute name="rate" type="ST_Rate" />
|
|
398
|
+
<xsd:attribute name="day" type="ST_Day" />
|
|
399
|
+
</xsd:complexType>
|
|
400
|
+
<xsd:element name="Rec" type="CT_Rec" />
|
|
401
|
+
</xsd:schema>
|
|
402
|
+
XSD
|
|
403
|
+
end
|
|
404
|
+
let(:rec) { AtomicBasesSpec::CTRec }
|
|
405
|
+
|
|
406
|
+
before do
|
|
407
|
+
described_class.to_models(
|
|
408
|
+
atomic_schema,
|
|
409
|
+
output_dir: atomic_dir,
|
|
410
|
+
create_files: true,
|
|
411
|
+
module_namespace: "AtomicBasesSpec",
|
|
412
|
+
)
|
|
413
|
+
require File.join(atomic_dir, "atomicbasesspec_registry.rb")
|
|
414
|
+
AtomicBasesSpec.register_all
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
after do
|
|
418
|
+
FileUtils.rm_rf(atomic_dir)
|
|
419
|
+
if defined?(AtomicBasesSpec)
|
|
420
|
+
Object.send(:remove_const, :AtomicBasesSpec)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Regression: decimal facet values must keep their exact lexical form
|
|
425
|
+
# (the exclusive facets used to parse `value` as an integer, truncating
|
|
426
|
+
# 1.5 to 1). At the exact bound 1.5 the eager inclusive cast passes and
|
|
427
|
+
# the lazy exclusive facet rejects — proving the bound is 1.5, not 1.
|
|
428
|
+
it "preserves decimal precision on exclusive bounds" do
|
|
429
|
+
expect(rec.new(money: "5.0").validate).to be_empty
|
|
430
|
+
expect(rec.new(money: "1.5").validate.first)
|
|
431
|
+
.to be_a(Lutaml::Model::MinExclusiveError)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Regression: xsd:float/xsd:date were absent from the supported-type map,
|
|
435
|
+
# so a faceted restriction generated a class inheriting Ruby-core
|
|
436
|
+
# Float/Date and failed to load. They now map to the Lutaml types. A
|
|
437
|
+
# below-range float is rejected by the eager numeric cast.
|
|
438
|
+
it "compiles a float restriction that loads and enforces bounds" do
|
|
439
|
+
expect(rec.new(rate: 5.0).validate).to be_empty
|
|
440
|
+
expect { rec.new(rate: 0.5) }
|
|
441
|
+
.to raise_error(Lutaml::Model::Type::MinBoundError)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it "compiles a date restriction that loads and enforces bounds" do
|
|
445
|
+
expect(rec.new(day: "2020-06-01").validate).to be_empty
|
|
446
|
+
expect(rec.new(day: "2019-06-01").validate.first)
|
|
447
|
+
.to be_a(Lutaml::Model::MinInclusiveError)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
149
451
|
context "when processing example from lutaml-model#260" do
|
|
150
452
|
let!(:address_dir) { Dir.mktmpdir }
|
|
151
453
|
let(:address) do
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/model/schema"
|
|
5
|
+
require "bigdecimal"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
# End-to-end round-trip for issue #191 constraining facets: a constrained type
|
|
10
|
+
# survives model -> XSD -> model (its facet macro is regenerated) and an XSD's
|
|
11
|
+
# facets survive XSD -> model -> XSD (each facet element reappears).
|
|
12
|
+
RSpec.describe "Issue #191 facet round-trip" do
|
|
13
|
+
# Generate XSD from a one-attribute model of the constrained type, compile it
|
|
14
|
+
# back, and return the regenerated source of the facet-bearing ST_* type.
|
|
15
|
+
def round_trip_source(constrained_type)
|
|
16
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
17
|
+
attribute :value, constrained_type
|
|
18
|
+
xml do
|
|
19
|
+
root "R"
|
|
20
|
+
map_element "value", to: :value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
xsd = Lutaml::Xml::Schema::XsdSchema.generate(model, pretty: true)
|
|
24
|
+
compiled = Lutaml::Model::Schema::XmlCompiler.to_models(xsd)
|
|
25
|
+
compiled.fetch(compiled.keys.grep(/\AST_/).first)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "model -> XSD -> model regenerates each facet macro" do
|
|
29
|
+
{
|
|
30
|
+
"integer inclusive bounds" => [
|
|
31
|
+
-> { Class.new(Lutaml::Model::Type::Integer) { inclusive min: 0, max: 100 } },
|
|
32
|
+
"inclusive min: 0, max: 100",
|
|
33
|
+
],
|
|
34
|
+
"integer exclusive bounds" => [
|
|
35
|
+
-> { Class.new(Lutaml::Model::Type::Integer) { exclusive min: 0, max: 100 } },
|
|
36
|
+
"exclusive min: 0, max: 100",
|
|
37
|
+
],
|
|
38
|
+
"exact length" => [
|
|
39
|
+
-> { Class.new(Lutaml::Model::Type::String) { length 5 } },
|
|
40
|
+
"length 5",
|
|
41
|
+
],
|
|
42
|
+
"length range" => [
|
|
43
|
+
-> { Class.new(Lutaml::Model::Type::String) { length min: 2, max: 8 } },
|
|
44
|
+
"length min: 2, max: 8",
|
|
45
|
+
],
|
|
46
|
+
"pattern" => [
|
|
47
|
+
-> { Class.new(Lutaml::Model::Type::String) { pattern(/[A-Z]+/) } },
|
|
48
|
+
"pattern(%r{[A-Z]+})",
|
|
49
|
+
],
|
|
50
|
+
"enumeration" => [
|
|
51
|
+
-> { Class.new(Lutaml::Model::Type::String) { enumeration("AB", "CD") } },
|
|
52
|
+
'enumeration("AB", "CD")',
|
|
53
|
+
],
|
|
54
|
+
"whiteSpace" => [
|
|
55
|
+
-> { Class.new(Lutaml::Model::Type::String) { white_space :collapse } },
|
|
56
|
+
"white_space :collapse",
|
|
57
|
+
],
|
|
58
|
+
"total and fraction digits" => [
|
|
59
|
+
lambda {
|
|
60
|
+
Class.new(Lutaml::Model::Type::Decimal) do
|
|
61
|
+
total_digits 5
|
|
62
|
+
fraction_digits 2
|
|
63
|
+
end
|
|
64
|
+
},
|
|
65
|
+
["total_digits 5", "fraction_digits 2"],
|
|
66
|
+
],
|
|
67
|
+
"decimal inclusive bounds" => [
|
|
68
|
+
-> { Class.new(Lutaml::Model::Type::Decimal) { inclusive min: BigDecimal("1.5") } },
|
|
69
|
+
'inclusive min: BigDecimal("1.5")',
|
|
70
|
+
],
|
|
71
|
+
"float inclusive bounds" => [
|
|
72
|
+
-> { Class.new(Lutaml::Model::Type::Float) { inclusive min: 1.25, max: 9.75 } },
|
|
73
|
+
'inclusive min: Lutaml::Model::Type::Float.cast("1.25")',
|
|
74
|
+
],
|
|
75
|
+
"date inclusive bound" => [
|
|
76
|
+
lambda {
|
|
77
|
+
Class.new(Lutaml::Model::Type::Date) do
|
|
78
|
+
inclusive min: Lutaml::Model::Type::Date.cast("2020-01-01")
|
|
79
|
+
end
|
|
80
|
+
},
|
|
81
|
+
'inclusive min: Lutaml::Model::Type::Date.cast("2020-01-01")',
|
|
82
|
+
],
|
|
83
|
+
"boolean enumeration" => [
|
|
84
|
+
-> { Class.new(Lutaml::Model::Type::Boolean) { enumeration(true) } },
|
|
85
|
+
'enumeration(Lutaml::Model::Type::Boolean.cast("true"))',
|
|
86
|
+
],
|
|
87
|
+
}.each do |facet, (build, macros)|
|
|
88
|
+
it "regenerates #{facet}" do
|
|
89
|
+
source = round_trip_source(build.call)
|
|
90
|
+
Array(macros).each { |macro| expect(source).to include(macro) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Layer-1 attribute facets (issue #191 F3) must also survive the round-trip:
|
|
95
|
+
# `min`/`max` on the attribute emit an xs:restriction that recompiles to the
|
|
96
|
+
# canonical facet macro.
|
|
97
|
+
it "regenerates Layer-1 attribute min/max bounds" do
|
|
98
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
99
|
+
attribute :value, :integer, min: 0, max: 100
|
|
100
|
+
xml do
|
|
101
|
+
root "R"
|
|
102
|
+
map_element "value", to: :value
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
xsd = Lutaml::Xml::Schema::XsdSchema.generate(model, pretty: true)
|
|
106
|
+
compiled = Lutaml::Model::Schema::XmlCompiler.to_models(xsd)
|
|
107
|
+
source = compiled.fetch(compiled.keys.grep(/\AST_/).first)
|
|
108
|
+
|
|
109
|
+
expect(source).to include("inclusive min: 0, max: 100")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "XSD -> model -> XSD regenerates each facet element" do
|
|
114
|
+
let!(:dir) { Dir.mktmpdir }
|
|
115
|
+
|
|
116
|
+
let(:regenerated) do
|
|
117
|
+
Lutaml::Model::Schema::XmlCompiler.to_models(
|
|
118
|
+
File.read("spec/fixtures/xml/restriction_facets.xsd"),
|
|
119
|
+
output_dir: dir, create_files: true, module_namespace: "FacetRtSpec",
|
|
120
|
+
)
|
|
121
|
+
require File.join(dir, "facetrtspec_registry.rb")
|
|
122
|
+
FacetRtSpec.register_all
|
|
123
|
+
Lutaml::Xml::Schema::XsdSchema.generate(FacetRtSpec::CTItem, pretty: true)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
after do
|
|
127
|
+
FileUtils.rm_rf(dir)
|
|
128
|
+
# The compiled fixture must load as a real module for register_all; drop it
|
|
129
|
+
# so repeated runs do not accumulate constants.
|
|
130
|
+
# rubocop:disable-next RSpec/RemoveConst
|
|
131
|
+
Object.send(:remove_const, :FacetRtSpec) if defined?(FacetRtSpec)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
"minLength/maxLength" =>
|
|
136
|
+
['<xs:minLength value="2"/>', '<xs:maxLength value="8"/>'],
|
|
137
|
+
"min/maxExclusive" =>
|
|
138
|
+
['<xs:minExclusive value="0"/>', '<xs:maxExclusive value="100"/>'],
|
|
139
|
+
"total/fractionDigits" =>
|
|
140
|
+
['<xs:totalDigits value="5"/>', '<xs:fractionDigits value="2"/>'],
|
|
141
|
+
"whiteSpace" => ['<xs:whiteSpace value="collapse"/>'],
|
|
142
|
+
"min/maxInclusive" => [
|
|
143
|
+
'<xs:minInclusive value="2020-01-01T00:00:00+00:00"/>',
|
|
144
|
+
'<xs:maxInclusive value="2020-12-31T23:59:59+00:00"/>',
|
|
145
|
+
],
|
|
146
|
+
"enumeration" => ['<xs:enumeration value="true"/>'],
|
|
147
|
+
}.each do |facet, fragments|
|
|
148
|
+
it "regenerates #{facet}" do
|
|
149
|
+
fragments.each { |fragment| expect(regenerated).to include(fragment) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|