lutaml-model 0.8.6 → 0.8.8
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_todo.yml +15 -68
- data/lib/lutaml/model/attribute.rb +5 -7
- data/lib/lutaml/model/attribute_validator.rb +3 -1
- data/lib/lutaml/model/choice.rb +1 -1
- data/lib/lutaml/model/deep_dupable.rb +16 -0
- data/lib/lutaml/model/mapping/mapping.rb +2 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +5 -3
- data/lib/lutaml/model/sequence.rb +4 -2
- data/lib/lutaml/model/serialize/initialization.rb +91 -4
- data/lib/lutaml/model/serialize.rb +24 -34
- data/lib/lutaml/model/store.rb +75 -6
- data/lib/lutaml/model/transform.rb +14 -3
- data/lib/lutaml/model/type/hash.rb +9 -5
- data/lib/lutaml/model/type/symbol.rb +1 -1
- data/lib/lutaml/model/utils.rb +15 -4
- data/lib/lutaml/model/validation.rb +8 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +1 -0
- data/lib/lutaml/xml/schema/xsd/schema.rb +8 -5
- data/lib/lutaml/xml/serialization/instance_methods.rb +2 -0
- data/lib/lutaml/xml/xml_element.rb +1 -1
- data/spec/lutaml/model/attribute_spec.rb +8 -19
- data/spec/lutaml/model/register_methods_spec.rb +149 -0
- data/spec/lutaml/model/store_spec.rb +120 -0
- data/spec/lutaml/model/transform_cache_spec.rb +42 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +10 -3
- data/spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb +35 -0
- data/spec/lutaml/xml/schema/xsd/spec_helper.rb +1 -0
- metadata +5 -2
|
@@ -6,7 +6,8 @@ module Lutaml
|
|
|
6
6
|
@transform_cache = {}
|
|
7
7
|
|
|
8
8
|
# Maximum number of cached Transform instances before eviction.
|
|
9
|
-
|
|
9
|
+
# Covers most OOXML/ISO schemas with ~1000+ classes and multiple registers.
|
|
10
|
+
MAX_CACHE_SIZE = 2048
|
|
10
11
|
|
|
11
12
|
def self.data_to_model(context, data, format, options = {})
|
|
12
13
|
register = options[:register] || Lutaml::Model::Config.default_register
|
|
@@ -15,7 +16,7 @@ module Lutaml
|
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def self.model_to_data(context, model, format, options = {})
|
|
18
|
-
register = model.lutaml_register if model.
|
|
19
|
+
register = model.lutaml_register if model.is_a?(Lutaml::Model::Serialize)
|
|
19
20
|
register ||= Lutaml::Model::Config.default_register
|
|
20
21
|
transform = cached_transform(context, register)
|
|
21
22
|
transform.model_to_data(model, format, options)
|
|
@@ -23,7 +24,7 @@ module Lutaml
|
|
|
23
24
|
|
|
24
25
|
def self.cached_transform(context, register)
|
|
25
26
|
@transform_cache ||= {}
|
|
26
|
-
cache_key = [context
|
|
27
|
+
cache_key = [context, register]
|
|
27
28
|
entry = @transform_cache[cache_key]
|
|
28
29
|
return entry if entry
|
|
29
30
|
|
|
@@ -39,6 +40,16 @@ module Lutaml
|
|
|
39
40
|
@transform_cache&.size || 0
|
|
40
41
|
end
|
|
41
42
|
|
|
43
|
+
def self.invalidate_for(context, register = nil)
|
|
44
|
+
return unless @transform_cache
|
|
45
|
+
|
|
46
|
+
if register
|
|
47
|
+
@transform_cache.delete([context, register])
|
|
48
|
+
else
|
|
49
|
+
@transform_cache.reject! { |(ctx, _reg)| ctx == context }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
42
53
|
def self.evict_if_needed
|
|
43
54
|
# Evict oldest half of entries when cache is full
|
|
44
55
|
keys_to_remove = @transform_cache.keys.first(@transform_cache.size / 2)
|
|
@@ -8,10 +8,10 @@ module Lutaml
|
|
|
8
8
|
return super if Utils.uninitialized?(value)
|
|
9
9
|
return nil if value.nil?
|
|
10
10
|
|
|
11
|
-
hash =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
hash = case value
|
|
12
|
+
when ::Hash then value
|
|
13
|
+
when ::Array then value.to_h
|
|
14
|
+
else value.to_h
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
normalize_hash(hash)
|
|
@@ -40,7 +40,11 @@ module Lutaml
|
|
|
40
40
|
return nil if value.nil?
|
|
41
41
|
return value if value.is_a?(::Hash)
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
case value
|
|
44
|
+
when ::Hash then value
|
|
45
|
+
when ::Array then value.to_h
|
|
46
|
+
else value.to_h
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# XSD type for Hash
|
|
@@ -7,7 +7,7 @@ module Lutaml
|
|
|
7
7
|
def self.cast(value, options = {})
|
|
8
8
|
return nil if value.nil?
|
|
9
9
|
return value if Utils.uninitialized?(value)
|
|
10
|
-
return nil if value.
|
|
10
|
+
return nil if value.is_a?(::String) && value.empty?
|
|
11
11
|
|
|
12
12
|
# Convert to string for validation and unwrapping
|
|
13
13
|
str_value = if value.is_a?(::Symbol)
|
data/lib/lutaml/model/utils.rb
CHANGED
|
@@ -115,18 +115,25 @@ module Lutaml
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def blank?(value)
|
|
118
|
-
|
|
118
|
+
case value
|
|
119
|
+
when ::String, ::Array, ::Hash then value.empty?
|
|
120
|
+
when ::NilClass then true
|
|
121
|
+
else false
|
|
122
|
+
end
|
|
119
123
|
end
|
|
120
124
|
|
|
121
125
|
def empty_collection?(collection)
|
|
122
126
|
return false if collection.nil?
|
|
123
|
-
return false unless [Array, Hash].include?(collection.class)
|
|
127
|
+
return false unless [::Array, ::Hash].include?(collection.class)
|
|
124
128
|
|
|
125
129
|
collection.empty?
|
|
126
130
|
end
|
|
127
131
|
|
|
128
132
|
def empty?(value)
|
|
129
|
-
|
|
133
|
+
case value
|
|
134
|
+
when ::String, ::Array, ::Hash then value.empty?
|
|
135
|
+
else false
|
|
136
|
+
end
|
|
130
137
|
end
|
|
131
138
|
|
|
132
139
|
def add_if_present(hash, key, value)
|
|
@@ -273,7 +280,11 @@ module Lutaml
|
|
|
273
280
|
end
|
|
274
281
|
|
|
275
282
|
def deep_dup_object(object)
|
|
276
|
-
object.
|
|
283
|
+
if object.is_a?(DeepDupable)
|
|
284
|
+
object.deep_dup
|
|
285
|
+
else
|
|
286
|
+
object.dup
|
|
287
|
+
end
|
|
277
288
|
end
|
|
278
289
|
|
|
279
290
|
def camelize_part(part)
|
|
@@ -10,7 +10,7 @@ module Lutaml
|
|
|
10
10
|
value = public_send(:"#{name}")
|
|
11
11
|
|
|
12
12
|
begin
|
|
13
|
-
if value.
|
|
13
|
+
if value.is_a?(Lutaml::Model::Serialize)
|
|
14
14
|
sub_errors = value.validate
|
|
15
15
|
errors.concat(sub_errors) if sub_errors.is_a?(Array)
|
|
16
16
|
else
|
|
@@ -74,8 +74,14 @@ module Lutaml
|
|
|
74
74
|
nil
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Default: no element order. XML overrides via InstanceMethods prepend
|
|
78
|
+
# with attr_accessor :element_order.
|
|
79
|
+
def element_order
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
77
83
|
def order_names
|
|
78
|
-
return [] unless
|
|
84
|
+
return [] unless element_order
|
|
79
85
|
|
|
80
86
|
element_order.each_with_object([]) do |element, arr|
|
|
81
87
|
next if element.text?
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Lutaml
|
|
|
33
33
|
autoload :AdapterResolver, "#{__dir__}/model/adapter_resolver"
|
|
34
34
|
autoload :AdapterScope, "#{__dir__}/model/adapter_scope"
|
|
35
35
|
autoload :Utils, "#{__dir__}/model/utils"
|
|
36
|
+
autoload :DeepDupable, "#{__dir__}/model/deep_dupable"
|
|
36
37
|
autoload :Serializable, "#{__dir__}/model/serializable"
|
|
37
38
|
autoload :Error, "#{__dir__}/model/error"
|
|
38
39
|
autoload :Constants, "#{__dir__}/model/constants"
|
|
@@ -145,13 +145,10 @@ module Lutaml
|
|
|
145
145
|
attribute_group.sort_by { |item| item.name.to_s }
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
# discovered during XML deserialization.
|
|
150
|
-
def target_namespace_from(model, value, custom_args = {})
|
|
148
|
+
def target_namespace_from(model, value)
|
|
151
149
|
model.target_namespace = value
|
|
152
|
-
namespaces = custom_args[:namespaces] || {}
|
|
153
150
|
model.target_namespace_prefix =
|
|
154
|
-
|
|
151
|
+
namespace_prefix_for(model.pending_plan_root_element, value)
|
|
155
152
|
end
|
|
156
153
|
|
|
157
154
|
# Find a type definition by local name
|
|
@@ -246,6 +243,12 @@ module Lutaml
|
|
|
246
243
|
|
|
247
244
|
private
|
|
248
245
|
|
|
246
|
+
def namespace_prefix_for(element, namespace_uri)
|
|
247
|
+
element&.own_namespaces&.find do |_, namespace|
|
|
248
|
+
namespace.uri == namespace_uri
|
|
249
|
+
end&.first
|
|
250
|
+
end
|
|
251
|
+
|
|
249
252
|
def all_namespaces
|
|
250
253
|
# Aggregate the schema target namespace with imported namespaces.
|
|
251
254
|
namespaces = [target_namespace].compact
|
|
@@ -288,30 +288,19 @@ RSpec.describe Lutaml::Model::Attribute do
|
|
|
288
288
|
describe "#deep_dup" do
|
|
289
289
|
let(:duplicate_attribute) { Lutaml::Model::Utils.deep_dup(attribute) }
|
|
290
290
|
|
|
291
|
-
context "when
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
after do
|
|
300
|
-
described_class.alias_method :deep_dup, :orig_deep_dup
|
|
301
|
-
attribute.options.delete(:foo)
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
it "confirms that options values are linked of original and duplicate instances" do
|
|
305
|
-
duplicate_attribute
|
|
306
|
-
attribute.options[:foo] = "bar"
|
|
307
|
-
expect(duplicate_attribute.options).to include(:foo)
|
|
291
|
+
context "when object does not include DeepDupable" do
|
|
292
|
+
it "falls back to dup for plain objects" do
|
|
293
|
+
plain = Struct.new(:value).new("hello")
|
|
294
|
+
result = Lutaml::Model::Utils.deep_dup(plain)
|
|
295
|
+
expect(result.value).to eq("hello")
|
|
296
|
+
expect(result).not_to equal(plain)
|
|
308
297
|
end
|
|
309
298
|
end
|
|
310
299
|
|
|
311
|
-
context "when
|
|
300
|
+
context "when Attribute is deep_duplicated" do
|
|
312
301
|
let(:attribute) { described_class.new("name", :string) }
|
|
313
302
|
|
|
314
|
-
it "
|
|
303
|
+
it "creates independent copies of options" do
|
|
315
304
|
duplicate_attribute
|
|
316
305
|
attribute.options[:foo] = "bar"
|
|
317
306
|
expect(duplicate_attribute.options).not_to include(:foo)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Register-specific attribute methods" do
|
|
6
|
+
let(:register) do
|
|
7
|
+
Lutaml::Model::Register.new(:register_methods_test,
|
|
8
|
+
fallback: [:default])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
let(:base_model) do
|
|
12
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
13
|
+
attribute :name, :string
|
|
14
|
+
|
|
15
|
+
xml do
|
|
16
|
+
root "Base"
|
|
17
|
+
map_element "Name", to: :name
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
let(:extension_model) do
|
|
23
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
24
|
+
attribute :version, :string
|
|
25
|
+
attribute :priority, :integer
|
|
26
|
+
|
|
27
|
+
xml do
|
|
28
|
+
root "Extension"
|
|
29
|
+
map_element "Version", to: :version
|
|
30
|
+
map_element "Priority", to: :priority
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
before do
|
|
36
|
+
Lutaml::Model::GlobalContext.reset!
|
|
37
|
+
Lutaml::Model::GlobalRegister.register(register)
|
|
38
|
+
register.register_model(extension_model, id: :extension_model)
|
|
39
|
+
base_model.import_model_attributes(extension_model, register.id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "class-level method definition" do
|
|
43
|
+
it "does not create singleton class methods for register-specific attributes" do
|
|
44
|
+
instance = base_model.new({ name: "test",
|
|
45
|
+
version: "1.0" },
|
|
46
|
+
register: register.id)
|
|
47
|
+
|
|
48
|
+
expect(instance.version).to eq("1.0")
|
|
49
|
+
|
|
50
|
+
# Method should be on the class, NOT directly on the singleton class
|
|
51
|
+
expect(instance.singleton_class.instance_methods(false)).not_to include(:version)
|
|
52
|
+
expect(instance.singleton_class.instance_methods(false)).not_to include(:version=)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "defines methods on the class accessible to all instances" do
|
|
56
|
+
instance1 = base_model.new({ name: "a" }, register: register.id)
|
|
57
|
+
instance2 = base_model.new({ name: "b" }, register: register.id)
|
|
58
|
+
|
|
59
|
+
instance1.version = "1.0"
|
|
60
|
+
instance2.version = "2.0"
|
|
61
|
+
|
|
62
|
+
expect(instance1.version).to eq("1.0")
|
|
63
|
+
expect(instance2.version).to eq("2.0")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "guards against re-definition with @_register_methods_defined" do
|
|
67
|
+
base_model.new({ name: "a" }, register: register.id)
|
|
68
|
+
|
|
69
|
+
guard = base_model.instance_variable_get(:@_register_methods_defined)
|
|
70
|
+
expect(guard).to include(register.id => true)
|
|
71
|
+
|
|
72
|
+
# Second instance creation should not re-trigger method definition
|
|
73
|
+
base_model.new({ name: "b" }, register: register.id)
|
|
74
|
+
expect(guard[register.id]).to be(true)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "type casting in setter" do
|
|
79
|
+
it "casts integer values correctly" do
|
|
80
|
+
instance = base_model.new({ name: "test" }, register: register.id)
|
|
81
|
+
|
|
82
|
+
instance.priority = "5"
|
|
83
|
+
expect(instance.priority).to eq(5)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "casts string values correctly" do
|
|
87
|
+
instance = base_model.new({ name: "test" }, register: register.id)
|
|
88
|
+
|
|
89
|
+
instance.version = 42
|
|
90
|
+
expect(instance.version).to eq("42")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
describe "value_set_for tracking" do
|
|
95
|
+
it "marks register-specific attributes as explicitly set" do
|
|
96
|
+
instance = base_model.new({ name: "test" }, register: register.id)
|
|
97
|
+
|
|
98
|
+
# After initialization, version was set via public_send in initialize_attributes
|
|
99
|
+
expect(instance.using_default?(:version)).to be(false)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "uses default when register-specific attribute is not provided" do
|
|
103
|
+
instance = base_model.allocate_for_deserialization(register.id)
|
|
104
|
+
|
|
105
|
+
# Not yet set — using default
|
|
106
|
+
expect(instance.using_default?(:version)).to be(true)
|
|
107
|
+
|
|
108
|
+
instance.version = "1.0"
|
|
109
|
+
expect(instance.using_default?(:version)).to be(false)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "default register instances" do
|
|
114
|
+
it "returns early without defining methods for :default register" do
|
|
115
|
+
base_model.ensure_register_methods_defined(:default)
|
|
116
|
+
|
|
117
|
+
guard = base_model.instance_variable_get(:@_register_methods_defined)
|
|
118
|
+
expect(guard).to be_nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "default-register instances still work for class-level attributes" do
|
|
122
|
+
instance = base_model.new(name: "test")
|
|
123
|
+
|
|
124
|
+
expect(instance.name).to eq("test")
|
|
125
|
+
instance.name = "changed"
|
|
126
|
+
expect(instance.name).to eq("changed")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe "deserialization path" do
|
|
131
|
+
it "defines methods via finalize_deserialization" do
|
|
132
|
+
instance = base_model.allocate_for_deserialization(register.id)
|
|
133
|
+
instance.version = "3.0"
|
|
134
|
+
|
|
135
|
+
expect(instance.version).to eq("3.0")
|
|
136
|
+
expect(instance.singleton_class.instance_methods(false)).not_to include(:version)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe "clear_cache resets the guard" do
|
|
141
|
+
it "clears @_register_methods_defined on clear_cache" do
|
|
142
|
+
base_model.new({ name: "a" }, register: register.id)
|
|
143
|
+
expect(base_model.instance_variable_get(:@_register_methods_defined)).not_to be_nil
|
|
144
|
+
|
|
145
|
+
base_model.clear_cache
|
|
146
|
+
expect(base_model.instance_variable_get(:@_register_methods_defined)).to be_nil
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Lutaml::Model::Store do
|
|
6
|
+
let(:model_class) do
|
|
7
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
8
|
+
attribute :id, :string
|
|
9
|
+
attribute :name, :string
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
before { described_class.clear }
|
|
14
|
+
after { described_class.clear }
|
|
15
|
+
|
|
16
|
+
describe "#register and #resolve" do
|
|
17
|
+
it "resolves a registered object by reference key" do
|
|
18
|
+
obj = model_class.new(id: "abc", name: "test")
|
|
19
|
+
result = described_class.resolve(model_class, :id, "abc")
|
|
20
|
+
expect(result).to eq(obj)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "returns nil for non-existent reference" do
|
|
24
|
+
model_class.new(id: "abc")
|
|
25
|
+
result = described_class.resolve(model_class, :id, "nonexistent")
|
|
26
|
+
expect(result).to be_nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "returns nil for non-existent class" do
|
|
30
|
+
other_class = Class.new(Lutaml::Model::Serializable) do
|
|
31
|
+
attribute :id, :string
|
|
32
|
+
end
|
|
33
|
+
result = described_class.resolve(other_class, :id, "anything")
|
|
34
|
+
expect(result).to be_nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "resolves by name attribute" do
|
|
38
|
+
obj = model_class.new(id: "abc", name: "myobj")
|
|
39
|
+
result = described_class.resolve(model_class, :name, "myobj")
|
|
40
|
+
expect(result).to eq(obj)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe "indexed lookup" do
|
|
45
|
+
it "builds index lazily on first resolve" do
|
|
46
|
+
5.times { |i| model_class.new(id: "obj-#{i}") }
|
|
47
|
+
|
|
48
|
+
result = described_class.resolve(model_class, :id, "obj-3")
|
|
49
|
+
expect(result.id).to eq("obj-3")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "updates index when new objects are registered after first resolve" do
|
|
53
|
+
model_class.new(id: "first")
|
|
54
|
+
described_class.resolve(model_class, :id, "first") # triggers index build
|
|
55
|
+
|
|
56
|
+
model_class.new(id: "second")
|
|
57
|
+
result = described_class.resolve(model_class, :id, "second")
|
|
58
|
+
expect(result.id).to eq("second")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "reuses index across multiple resolves for same key" do
|
|
62
|
+
10.times { |i| model_class.new(id: "obj-#{i}") }
|
|
63
|
+
|
|
64
|
+
# First resolve builds the index
|
|
65
|
+
described_class.resolve(model_class, :id, "obj-5")
|
|
66
|
+
# Second resolve should use the same index
|
|
67
|
+
result = described_class.resolve(model_class, :id, "obj-7")
|
|
68
|
+
expect(result.id).to eq("obj-7")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "WeakRef behavior" do
|
|
73
|
+
it "uses WeakRef for storage (objects can be collected when unreferenced)" do
|
|
74
|
+
obj = model_class.new(id: "alive")
|
|
75
|
+
result = described_class.resolve(model_class, :id, "alive")
|
|
76
|
+
expect(result).to eq(obj)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "resolves objects with strong references held externally" do
|
|
80
|
+
obj = model_class.new(id: "alive")
|
|
81
|
+
GC.start
|
|
82
|
+
result = described_class.resolve(model_class, :id, "alive")
|
|
83
|
+
expect(result).to eq(obj)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "#store" do
|
|
88
|
+
it "returns only live objects" do
|
|
89
|
+
obj1 = model_class.new(id: "keep")
|
|
90
|
+
_obj2 = model_class.new(id: "drop")
|
|
91
|
+
_obj2 = nil
|
|
92
|
+
GC.start
|
|
93
|
+
|
|
94
|
+
entries = described_class.store[model_class.to_s]
|
|
95
|
+
# obj1 should be alive; obj2 may or may not be GC'd depending on timing
|
|
96
|
+
expect(entries).to include(obj1)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "#clear" do
|
|
101
|
+
it "removes all registered objects" do
|
|
102
|
+
model_class.new(id: "a")
|
|
103
|
+
model_class.new(id: "b")
|
|
104
|
+
described_class.clear
|
|
105
|
+
|
|
106
|
+
expect(described_class.resolve(model_class, :id, "a")).to be_nil
|
|
107
|
+
expect(described_class.resolve(model_class, :id, "b")).to be_nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "clears the index" do
|
|
111
|
+
model_class.new(id: "indexed")
|
|
112
|
+
described_class.resolve(model_class, :id, "indexed") # build index
|
|
113
|
+
described_class.clear
|
|
114
|
+
|
|
115
|
+
model_class.new(id: "new-one")
|
|
116
|
+
result = described_class.resolve(model_class, :id, "new-one")
|
|
117
|
+
expect(result.id).to eq("new-one")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -59,4 +59,46 @@ RSpec.describe "Transform caching" do
|
|
|
59
59
|
expect(Lutaml::Model::Transform.cache_size).to be <= 4
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
describe ".invalidate_for" do
|
|
64
|
+
it "invalidates cache for a specific context" do
|
|
65
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
66
|
+
attribute :name, :string
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
t1 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
70
|
+
Lutaml::Model::Transform.invalidate_for(klass)
|
|
71
|
+
t2 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
72
|
+
expect(t1).not_to equal(t2)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "invalidates cache for a specific context and register" do
|
|
76
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
77
|
+
attribute :name, :string
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
t1_default = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
81
|
+
t1_other = Lutaml::Model::Transform.cached_transform(klass, :other)
|
|
82
|
+
|
|
83
|
+
Lutaml::Model::Transform.invalidate_for(klass, :default)
|
|
84
|
+
|
|
85
|
+
t2_default = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
86
|
+
t2_other = Lutaml::Model::Transform.cached_transform(klass, :other)
|
|
87
|
+
|
|
88
|
+
expect(t1_default).not_to equal(t2_default)
|
|
89
|
+
expect(t1_other).to equal(t2_other)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "uses class identity as key, not object_id" do
|
|
93
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
94
|
+
attribute :name, :string
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
t1 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
98
|
+
# Force GC — object_id reuse should not cause stale hits
|
|
99
|
+
GC.start
|
|
100
|
+
t2 = Lutaml::Model::Transform.cached_transform(klass, :default)
|
|
101
|
+
expect(t1).to equal(t2)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
62
104
|
end
|
|
@@ -75,11 +75,18 @@ RSpec.describe "#clear_xml_parse_state!" do
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
describe "user-facing attributes" do
|
|
78
|
-
it "
|
|
78
|
+
it "clears element_order to release parse buffers" do
|
|
79
79
|
model = model_class.from_xml(xml_with_ns)
|
|
80
|
-
|
|
80
|
+
expect(model.element_order).not_to be_nil
|
|
81
81
|
model.clear_xml_parse_state!
|
|
82
|
-
expect(model.element_order).to
|
|
82
|
+
expect(model.element_order).to be_nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "clears attribute_order to release parse buffers" do
|
|
86
|
+
model = model_class.from_xml(xml_with_ns)
|
|
87
|
+
expect(model.attribute_order).not_to be_nil
|
|
88
|
+
model.clear_xml_parse_state!
|
|
89
|
+
expect(model.attribute_order).to be_nil
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
it "does not clear encoding" do
|
|
@@ -14,6 +14,41 @@ RSpec.describe "Schema mapping integration" do
|
|
|
14
14
|
Lutaml::Xml::Schema::Xsd::Glob.schema_mappings = nil
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
describe "target namespace prefix parsing" do
|
|
18
|
+
let(:xsd_content) do
|
|
19
|
+
<<~XSD
|
|
20
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
21
|
+
xmlns:t="http://example.com/test"
|
|
22
|
+
targetNamespace="http://example.com/test">
|
|
23
|
+
<xs:element name="Root" type="xs:string"/>
|
|
24
|
+
</xs:schema>
|
|
25
|
+
XSD
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "captures the root prefix declared for the target namespace" do
|
|
29
|
+
parsed = Lutaml::Xml::Schema::Xsd.parse(xsd_content)
|
|
30
|
+
|
|
31
|
+
expect(parsed.target_namespace).to eq("http://example.com/test")
|
|
32
|
+
expect(parsed.target_namespace_prefix).to eq("t")
|
|
33
|
+
expect(parsed.element.first.target_prefix).to eq("t")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "captures nil prefix when target namespace is the default (unprefixed)" do
|
|
37
|
+
xsd_default_ns = <<~XSD
|
|
38
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
39
|
+
xmlns="http://example.com/default"
|
|
40
|
+
targetNamespace="http://example.com/default">
|
|
41
|
+
<xs:element name="Root" type="xs:string"/>
|
|
42
|
+
</xs:schema>
|
|
43
|
+
XSD
|
|
44
|
+
|
|
45
|
+
parsed = Lutaml::Xml::Schema::Xsd.parse(xsd_default_ns)
|
|
46
|
+
|
|
47
|
+
expect(parsed.target_namespace).to eq("http://example.com/default")
|
|
48
|
+
expect(parsed.target_namespace_prefix).to be_nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
17
52
|
describe "parsing with exact string mappings" do
|
|
18
53
|
let(:xsd_content) do
|
|
19
54
|
<<~XSD
|