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
|
@@ -35,9 +35,61 @@ RSpec.describe "Lazy nil deserialization state guard specs" do
|
|
|
35
35
|
instance.send(:init_deserialization_state, nil)
|
|
36
36
|
# Collections are initialized with LAZY_EMPTY_COLLECTION (frozen shared [])
|
|
37
37
|
# instead of per-instance Array.new — avoids allocation overhead
|
|
38
|
+
expect(instance.instance_variable_get(:@tags))
|
|
39
|
+
.to be(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "allocates a per-instance array only once the collection is read" do
|
|
43
|
+
instance = model_class.allocate
|
|
44
|
+
instance.send(:init_deserialization_state, nil)
|
|
45
|
+
|
|
38
46
|
tags = instance.tags
|
|
47
|
+
|
|
39
48
|
expect(tags).to eq([])
|
|
40
|
-
expect(tags).
|
|
49
|
+
expect(tags).not_to be(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION)
|
|
50
|
+
expect(instance.instance_variable_get(:@tags)).to be(tags)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "enum collection assignment cost" do
|
|
55
|
+
# Duplicates are the collection reader's job, so this writer stays a plain
|
|
56
|
+
# `+`. The tempting alternative — deduping here with an Array#include? per
|
|
57
|
+
# appended item — makes assignment quadratic, and an enum collection has no
|
|
58
|
+
# size limit. Measured where this guard was written: the rescan form takes
|
|
59
|
+
# 6.35s at this size, against well under a millisecond for the `+`. The
|
|
60
|
+
# budget is deliberately coarse; it exists to catch a quadratic writer, not
|
|
61
|
+
# to police small regressions.
|
|
62
|
+
let(:enum_values) { (1..64_000).to_a }
|
|
63
|
+
|
|
64
|
+
let(:enum_model_class) do
|
|
65
|
+
values = enum_values
|
|
66
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
67
|
+
attribute :codes, :integer, values: values, collection: true
|
|
68
|
+
|
|
69
|
+
def self.name
|
|
70
|
+
"LargeEnumCollectionModel"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "assigns a large collection without rescanning what it already holds" do
|
|
76
|
+
model = enum_model_class.new
|
|
77
|
+
|
|
78
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
79
|
+
model.codes = enum_values
|
|
80
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
81
|
+
|
|
82
|
+
expect(model.codes.size).to eq(64_000)
|
|
83
|
+
expect(elapsed).to be < 0.75
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "still reads back without duplicates after repeated assignment" do
|
|
87
|
+
model = enum_model_class.new
|
|
88
|
+
|
|
89
|
+
model.codes = [1, 2, 2, 3]
|
|
90
|
+
model.codes = [3, 4]
|
|
91
|
+
|
|
92
|
+
expect(model.codes).to eq([1, 2, 3, 4])
|
|
41
93
|
end
|
|
42
94
|
end
|
|
43
95
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "bigdecimal"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "totalDigits / fractionDigits facets" do
|
|
7
|
+
def build_model(base, total: nil, fraction: nil)
|
|
8
|
+
type = Class.new(base)
|
|
9
|
+
type.total_digits(total) unless total.nil?
|
|
10
|
+
type.fraction_digits(fraction) unless fraction.nil?
|
|
11
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :n, type
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def errors_for(base, value, total: nil, fraction: nil)
|
|
17
|
+
build_model(base, total: total, fraction: fraction).new(n: value).validate
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:decimal) { Lutaml::Model::Type::Decimal }
|
|
21
|
+
let(:integer) { Lutaml::Model::Type::Integer }
|
|
22
|
+
|
|
23
|
+
describe "digit counting convention" do
|
|
24
|
+
# base, lexical value, expected total digits, expected fraction digits
|
|
25
|
+
[
|
|
26
|
+
[Lutaml::Model::Type::Decimal, "123.45", 5, 2],
|
|
27
|
+
[Lutaml::Model::Type::Decimal, "100", 3, 0],
|
|
28
|
+
[Lutaml::Model::Type::Decimal, "0.1", 1, 1],
|
|
29
|
+
[Lutaml::Model::Type::Decimal, "0.100", 1, 1],
|
|
30
|
+
[Lutaml::Model::Type::Decimal, "100.00", 3, 0],
|
|
31
|
+
[Lutaml::Model::Type::Decimal, "-123.45", 5, 2],
|
|
32
|
+
[Lutaml::Model::Type::Decimal, "12.340", 4, 2],
|
|
33
|
+
[Lutaml::Model::Type::Decimal, "0.05", 1, 2],
|
|
34
|
+
[Lutaml::Model::Type::Decimal, "0.001", 1, 3],
|
|
35
|
+
[Lutaml::Model::Type::Decimal, "0.5", 1, 1],
|
|
36
|
+
[Lutaml::Model::Type::Integer, 12345, 5, 0],
|
|
37
|
+
].each do |base, value, total, fraction|
|
|
38
|
+
it "counts #{value.inspect} as #{total} total / #{fraction} fraction" do
|
|
39
|
+
expect(errors_for(base, value, total: total)).to be_empty
|
|
40
|
+
expect(errors_for(base, value, fraction: fraction)).to be_empty
|
|
41
|
+
|
|
42
|
+
if total > 1
|
|
43
|
+
expect(errors_for(base, value, total: total - 1).first)
|
|
44
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
45
|
+
end
|
|
46
|
+
if fraction.positive?
|
|
47
|
+
expect(errors_for(base, value, fraction: fraction - 1).first)
|
|
48
|
+
.to be_a(Lutaml::Model::FractionDigitsError)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "counts only significant digits toward the total (XSD)" do
|
|
54
|
+
# `0.05` has one significant digit (`5`), so a `total_digits 1` type
|
|
55
|
+
# accepts it: the leading zero before the point is insignificant. This
|
|
56
|
+
# is required for XSD round-trip conformance -- a model generated from
|
|
57
|
+
# `<xs:totalDigits value="1"/>` must accept 0.05 and reject 12.
|
|
58
|
+
model = build_model(decimal, total: 1)
|
|
59
|
+
|
|
60
|
+
expect(model.new(n: "0.05").validate).to be_empty
|
|
61
|
+
expect(model.new(n: "12").validate.first)
|
|
62
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "treats integer zero as zero significant digits" do
|
|
66
|
+
expect(errors_for(integer, 0, total: 1)).to be_empty
|
|
67
|
+
expect(errors_for(integer, 0, fraction: 0)).to be_empty
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "treats decimal zero as zero significant digits" do
|
|
71
|
+
expect(errors_for(decimal, "0", total: 1)).to be_empty
|
|
72
|
+
expect(errors_for(decimal, "0.0", fraction: 0)).to be_empty
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe "total_digits enforcement" do
|
|
77
|
+
it "passes a Decimal within the digit budget" do
|
|
78
|
+
expect(errors_for(decimal, "123.45", total: 8)).to be_empty
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "collects a TotalDigitsError when a Decimal exceeds the budget" do
|
|
82
|
+
expect(errors_for(decimal, "123.456", total: 5).first)
|
|
83
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "passes an Integer within the digit budget" do
|
|
87
|
+
expect(errors_for(integer, 12345, total: 5)).to be_empty
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "collects a TotalDigitsError when an Integer exceeds the budget" do
|
|
91
|
+
expect(errors_for(integer, 123456, total: 5).first)
|
|
92
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "fraction_digits enforcement" do
|
|
97
|
+
it "passes a Decimal within the fraction budget" do
|
|
98
|
+
expect(errors_for(decimal, "1.23", fraction: 2)).to be_empty
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "collects a FractionDigitsError when the fraction is too long" do
|
|
102
|
+
expect(errors_for(decimal, "1.234", fraction: 2).first)
|
|
103
|
+
.to be_a(Lutaml::Model::FractionDigitsError)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "enforces both facets together (Money-style)" do
|
|
107
|
+
model = build_model(decimal, total: 8, fraction: 2)
|
|
108
|
+
|
|
109
|
+
expect(model.new(n: "123456.78").validate).to be_empty
|
|
110
|
+
expect(model.new(n: "1234567.89").validate.first)
|
|
111
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
112
|
+
expect(model.new(n: "1.234").validate.first)
|
|
113
|
+
.to be_a(Lutaml::Model::FractionDigitsError)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
describe "laziness" do
|
|
118
|
+
it "does not raise until validate!" do
|
|
119
|
+
model = build_model(decimal, total: 3)
|
|
120
|
+
|
|
121
|
+
expect { model.new(n: "123456") }.not_to raise_error
|
|
122
|
+
expect(model.new(n: "123456").validate.first)
|
|
123
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe "declaration guards" do
|
|
128
|
+
it "rejects a negative total_digits" do
|
|
129
|
+
expect { Class.new(decimal) { total_digits(-1) } }
|
|
130
|
+
.to raise_error(ArgumentError)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "rejects total_digits 0" do
|
|
134
|
+
expect { Class.new(decimal) { total_digits 0 } }
|
|
135
|
+
.to raise_error(ArgumentError)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "rejects a non-integer total_digits" do
|
|
139
|
+
expect { Class.new(decimal) { total_digits 2.5 } }
|
|
140
|
+
.to raise_error(ArgumentError)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "rejects a negative fraction_digits" do
|
|
144
|
+
expect { Class.new(decimal) { fraction_digits(-1) } }
|
|
145
|
+
.to raise_error(ArgumentError)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "allows fraction_digits 0" do
|
|
149
|
+
expect { Class.new(decimal) { fraction_digits 0 } }.not_to raise_error
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
describe "applicability guard" do
|
|
154
|
+
it "rejects digit facets on a Float subclass" do
|
|
155
|
+
expect { errors_for(Lutaml::Model::Type::Float, 1.5, total: 5) }
|
|
156
|
+
.to raise_error(ArgumentError, /:integer and :decimal/)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it "rejects digit facets on a String subclass" do
|
|
160
|
+
expect { errors_for(Lutaml::Model::Type::String, "12", total: 5) }
|
|
161
|
+
.to raise_error(ArgumentError, /:integer and :decimal/)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "rejects digit facets on a temporal subclass" do
|
|
165
|
+
expect do
|
|
166
|
+
errors_for(Lutaml::Model::Type::Date, "2020-01-01", total: 5)
|
|
167
|
+
end.to raise_error(ArgumentError, /:integer and :decimal/)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
describe "consistency guard" do
|
|
172
|
+
it "rejects fraction_digits exceeding total_digits" do
|
|
173
|
+
expect { errors_for(decimal, "1.5", total: 3, fraction: 5) }
|
|
174
|
+
.to raise_error(ArgumentError, /fraction_digits.*exceeds.*total_digits/)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "accepts fraction_digits equal to total_digits" do
|
|
178
|
+
expect(errors_for(decimal, "0.12", total: 2, fraction: 2)).to be_empty
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe "MAX-like merge and inheritance" do
|
|
183
|
+
it "keeps the smaller total_digits when re-declared" do
|
|
184
|
+
type = Class.new(decimal) do
|
|
185
|
+
total_digits 8
|
|
186
|
+
total_digits 5
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
expect(type.facets).to eq(total_digits: 5)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "lets a child tighten total_digits" do
|
|
193
|
+
base = Class.new(decimal) { total_digits 8 }
|
|
194
|
+
child = Class.new(base) { total_digits 5 }
|
|
195
|
+
|
|
196
|
+
expect(child.facets).to eq(total_digits: 5)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it "raises when a child widens total_digits" do
|
|
200
|
+
base = Class.new(decimal) { total_digits 5 }
|
|
201
|
+
child = Class.new(base) { total_digits 8 }
|
|
202
|
+
|
|
203
|
+
expect { child.facets }.to raise_error(ArgumentError, /cannot widen/)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "lets a child tighten fraction_digits" do
|
|
207
|
+
base = Class.new(decimal) { fraction_digits 4 }
|
|
208
|
+
child = Class.new(base) { fraction_digits 2 }
|
|
209
|
+
|
|
210
|
+
expect(child.facets).to eq(fraction_digits: 2)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "raises when a child widens fraction_digits" do
|
|
214
|
+
base = Class.new(decimal) { fraction_digits 2 }
|
|
215
|
+
child = Class.new(base) { fraction_digits 4 }
|
|
216
|
+
|
|
217
|
+
expect { child.facets }.to raise_error(ArgumentError, /cannot widen/)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it "enforces the tightest inherited total_digits" do
|
|
221
|
+
base = Class.new(decimal) { total_digits 8 }
|
|
222
|
+
child = Class.new(base) { total_digits 6 }
|
|
223
|
+
|
|
224
|
+
expect(errors_for(child, "12345.6")).to be_empty
|
|
225
|
+
expect(errors_for(child, "1234567").first)
|
|
226
|
+
.to be_a(Lutaml::Model::TotalDigitsError)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|