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,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/store"
|
|
5
|
+
|
|
6
|
+
module ConversionCachingSpec
|
|
7
|
+
class CachedModel < Lutaml::Model::Serializable
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :value, :string
|
|
10
|
+
|
|
11
|
+
cache_conversions
|
|
12
|
+
|
|
13
|
+
xml do
|
|
14
|
+
root "cached-model"
|
|
15
|
+
map_element "name", to: :name
|
|
16
|
+
map_element "value", to: :value
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Same root and shape as CachedModel: proves two cached classes
|
|
21
|
+
# parsing identical input do not collide in the store.
|
|
22
|
+
class TwinCachedModel < Lutaml::Model::Serializable
|
|
23
|
+
attribute :name, :string
|
|
24
|
+
attribute :value, :string
|
|
25
|
+
|
|
26
|
+
cache_conversions
|
|
27
|
+
|
|
28
|
+
xml do
|
|
29
|
+
root "cached-model"
|
|
30
|
+
map_element "name", to: :name
|
|
31
|
+
map_element "value", to: :value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class UncachedModel < Lutaml::Model::Serializable
|
|
36
|
+
attribute :name, :string
|
|
37
|
+
|
|
38
|
+
xml do
|
|
39
|
+
root "uncached-model"
|
|
40
|
+
map_element "name", to: :name
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class InheritedModel < CachedModel
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
RSpec.describe "Conversion caching" do
|
|
49
|
+
let(:store) { Lutaml::Store::BasicStore.new(adapter: { type: :memory }) }
|
|
50
|
+
|
|
51
|
+
after do
|
|
52
|
+
Lutaml::Model::Config.conversion_cache = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe "Config.conversion_cache" do
|
|
56
|
+
it "defaults to a memory-backed BasicStore when lutaml-store is loaded" do
|
|
57
|
+
expect(Lutaml::Model::Config.conversion_cache)
|
|
58
|
+
.to be_a(Lutaml::Store::BasicStore)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "is settable via the configure block" do
|
|
62
|
+
Lutaml::Model::Config.configure do |config|
|
|
63
|
+
config.conversion_cache = store
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
expect(Lutaml::Model::Config.conversion_cache).to be(store)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "is disabled by assigning false" do
|
|
70
|
+
Lutaml::Model::Config.conversion_cache = false
|
|
71
|
+
|
|
72
|
+
expect(Lutaml::Model::Config.conversion_cache).to be_nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe ".cache_conversions" do
|
|
77
|
+
it "is disabled by default" do
|
|
78
|
+
expect(ConversionCachingSpec::UncachedModel.conversion_caching_enabled?)
|
|
79
|
+
.to be(false)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "is enabled by the macro" do
|
|
83
|
+
expect(ConversionCachingSpec::CachedModel.conversion_caching_enabled?)
|
|
84
|
+
.to be(true)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "is inherited by subclasses" do
|
|
88
|
+
expect(ConversionCachingSpec::InheritedModel.conversion_caching_enabled?)
|
|
89
|
+
.to be(true)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
describe "deserialization caching" do
|
|
94
|
+
let(:xml) { "<cached-model><name>a</name><value>1</value></cached-model>" }
|
|
95
|
+
|
|
96
|
+
before do
|
|
97
|
+
Lutaml::Model::Config.conversion_cache = store
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "returns the cached instance for repeated identical input" do
|
|
101
|
+
first = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
102
|
+
second = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
103
|
+
|
|
104
|
+
expect(second).to be(first)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "uses the auto-detected default store when none is assigned" do
|
|
108
|
+
Lutaml::Model::Config.conversion_cache = nil
|
|
109
|
+
|
|
110
|
+
first = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
111
|
+
second = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
112
|
+
|
|
113
|
+
expect(second).to be(first)
|
|
114
|
+
expect(Lutaml::Model::Config.conversion_cache.size).to eq(1)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "does not cache when disabled with false" do
|
|
118
|
+
Lutaml::Model::Config.conversion_cache = false
|
|
119
|
+
|
|
120
|
+
first = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
121
|
+
second = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
122
|
+
|
|
123
|
+
expect(second).not_to be(first)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "parses different input separately" do
|
|
127
|
+
other = "<cached-model><name>b</name><value>2</value></cached-model>"
|
|
128
|
+
|
|
129
|
+
first = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
130
|
+
second = ConversionCachingSpec::CachedModel.from_xml(other)
|
|
131
|
+
|
|
132
|
+
expect(second.name).to eq("b")
|
|
133
|
+
expect(second).not_to be(first)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "keeps same-named input separate per class" do
|
|
137
|
+
cached = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
138
|
+
twin = ConversionCachingSpec::TwinCachedModel.from_xml(xml)
|
|
139
|
+
|
|
140
|
+
expect(twin).to be_a(ConversionCachingSpec::TwinCachedModel)
|
|
141
|
+
expect(cached).to be_a(ConversionCachingSpec::CachedModel)
|
|
142
|
+
expect(store.size).to eq(2)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "caches calls that pass only the register option" do
|
|
146
|
+
first = ConversionCachingSpec::CachedModel.from_xml(xml,
|
|
147
|
+
register: :default)
|
|
148
|
+
second = ConversionCachingSpec::CachedModel.from_xml(xml,
|
|
149
|
+
register: :default)
|
|
150
|
+
|
|
151
|
+
expect(second).to be(first)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "caches calls with extra options under distinct keys" do
|
|
155
|
+
plain = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
156
|
+
opted = ConversionCachingSpec::CachedModel.from_xml(xml,
|
|
157
|
+
encoding: "UTF-8")
|
|
158
|
+
again = ConversionCachingSpec::CachedModel.from_xml(xml,
|
|
159
|
+
encoding: "UTF-8")
|
|
160
|
+
|
|
161
|
+
expect(store.size).to eq(2)
|
|
162
|
+
expect(again).to be(opted)
|
|
163
|
+
expect(opted).not_to be(plain)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "treats a wrong-typed stored value as a miss" do
|
|
167
|
+
ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
168
|
+
store.each_key { |key| store.set(key, "corrupted") }
|
|
169
|
+
|
|
170
|
+
result = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
171
|
+
|
|
172
|
+
expect(result).to be_a(ConversionCachingSpec::CachedModel)
|
|
173
|
+
expect(result.name).to eq("a")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "does not cache classes that did not opt in" do
|
|
177
|
+
ConversionCachingSpec::UncachedModel.from_xml(
|
|
178
|
+
"<uncached-model><name>a</name></uncached-model>",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expect(store.size).to eq(0)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "lets store errors propagate instead of mislabeling them" do
|
|
185
|
+
allow(store).to receive(:get).and_raise(TypeError, "store broke")
|
|
186
|
+
|
|
187
|
+
expect do
|
|
188
|
+
ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
189
|
+
end.to raise_error(TypeError, "store broke")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "still raises InvalidFormatError for malformed input" do
|
|
193
|
+
expect do
|
|
194
|
+
ConversionCachingSpec::CachedModel.from_json("{not json")
|
|
195
|
+
end.to raise_error(Lutaml::Model::InvalidFormatError)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
describe "serialization caching" do
|
|
200
|
+
let(:instance) do
|
|
201
|
+
ConversionCachingSpec::CachedModel.new(name: "a", value: "1")
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
before do
|
|
205
|
+
Lutaml::Model::Config.conversion_cache = store
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it "serves repeated serialization from the store" do
|
|
209
|
+
instance.to_xml
|
|
210
|
+
store.each_key { |key| store.set(key, "<sentinel/>") }
|
|
211
|
+
|
|
212
|
+
expect(instance.to_xml).to eq("<sentinel/>")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "re-serializes after the instance is mutated" do
|
|
216
|
+
instance.to_xml
|
|
217
|
+
instance.name = "b"
|
|
218
|
+
|
|
219
|
+
expect(instance.to_xml).to include("b")
|
|
220
|
+
expect(store.size).to eq(2)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "caches each option set under its own key" do
|
|
224
|
+
instance.to_xml
|
|
225
|
+
instance.to_xml(pretty: true)
|
|
226
|
+
|
|
227
|
+
expect(store.size).to eq(2)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it "bypasses for instances that cannot be marshaled" do
|
|
231
|
+
instance.instance_variable_set(:@unmarshalable, proc {})
|
|
232
|
+
|
|
233
|
+
expect(instance.to_xml).to include("<name>a</name>")
|
|
234
|
+
expect(store.size).to eq(0)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "attempts marshal only once for an unmarshalable instance" do
|
|
238
|
+
instance.instance_variable_set(:@unmarshalable, proc {})
|
|
239
|
+
|
|
240
|
+
allow(Marshal).to receive(:dump).and_call_original
|
|
241
|
+
|
|
242
|
+
2.times { instance.to_xml }
|
|
243
|
+
|
|
244
|
+
expect(Marshal).to have_received(:dump).once
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it "does not cache classes that did not opt in" do
|
|
248
|
+
ConversionCachingSpec::UncachedModel.new(name: "a").to_xml
|
|
249
|
+
|
|
250
|
+
expect(store.size).to eq(0)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
describe "with Lutaml::Store::CacheStore" do
|
|
255
|
+
let(:store) { Lutaml::Store::CacheStore.new(adapter: { type: :memory }) }
|
|
256
|
+
let(:xml) { "<cached-model><name>a</name><value>1</value></cached-model>" }
|
|
257
|
+
|
|
258
|
+
before do
|
|
259
|
+
Lutaml::Model::Config.conversion_cache = store
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
it "serves to_* output strings through the JSON-marshaling store" do
|
|
263
|
+
instance = ConversionCachingSpec::CachedModel.new(name: "a", value: "1")
|
|
264
|
+
instance.to_xml
|
|
265
|
+
# CacheStore has no #each_key; snapshot keys first.
|
|
266
|
+
keys = store.keys
|
|
267
|
+
keys.each { |key| store.set(key, "<sentinel/>") }
|
|
268
|
+
|
|
269
|
+
expect(instance.to_xml).to eq("<sentinel/>")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "recomputes from_* results that cannot round-trip through JSON" do
|
|
273
|
+
first = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
274
|
+
second = ConversionCachingSpec::CachedModel.from_xml(xml)
|
|
275
|
+
|
|
276
|
+
expect(second).not_to be(first)
|
|
277
|
+
expect(second.name).to eq("a")
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -78,14 +78,39 @@ RSpec.describe "Lazy collection initialization", type: :model do
|
|
|
78
78
|
expect(ivar_c).to be(sentinel)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
it "
|
|
81
|
+
it "hands out a real empty array for uninitialized collections" do
|
|
82
82
|
xml = "<multi><name>test</name></multi>"
|
|
83
83
|
instance = LazyCollectionTests::MultiCollection.from_xml(xml)
|
|
84
84
|
|
|
85
85
|
items_a = instance.items_a
|
|
86
86
|
expect(items_a).to eq([])
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
# The sentinel is frozen and shared by every instance of every class, so
|
|
88
|
+
# a reader cannot hand it out: `instance.items_a << x` has to work, and
|
|
89
|
+
# has to stay on this instance.
|
|
90
|
+
expect(items_a).not_to be_frozen
|
|
91
|
+
expect(items_a).not_to be(sentinel)
|
|
92
|
+
expect(instance.instance_variable_get(:@items_a)).to be(items_a)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "keeps a pushed item and keeps it on this instance" do
|
|
96
|
+
xml = "<multi><name>test</name></multi>"
|
|
97
|
+
one = LazyCollectionTests::MultiCollection.from_xml(xml)
|
|
98
|
+
two = LazyCollectionTests::MultiCollection.from_xml(xml)
|
|
99
|
+
|
|
100
|
+
one.items_a << "hello"
|
|
101
|
+
|
|
102
|
+
expect(one.items_a).to eq(["hello"])
|
|
103
|
+
expect(two.items_a).to eq([])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "leaves the collections nobody read on the sentinel" do
|
|
107
|
+
xml = "<multi><name>test</name></multi>"
|
|
108
|
+
instance = LazyCollectionTests::MultiCollection.from_xml(xml)
|
|
109
|
+
|
|
110
|
+
instance.items_a
|
|
111
|
+
|
|
112
|
+
expect(instance.instance_variable_get(:@items_b)).to be(sentinel)
|
|
113
|
+
expect(instance.instance_variable_get(:@items_c)).to be(sentinel)
|
|
89
114
|
end
|
|
90
115
|
|
|
91
116
|
it "supports builder-style append on sentinel collections" do
|