lutaml-model 0.8.9 → 0.8.11
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/.github/workflows/dependent-repos.json +1 -0
- data/.rubocop_todo.yml +52 -22
- data/README.adoc +43 -0
- data/docs/_guides/jsonld-serialization.adoc +3 -1
- data/docs/_guides/rdf-serialization.adoc +94 -8
- data/docs/_guides/turtle-serialization.adoc +17 -4
- data/lib/lutaml/jsonld/transform.rb +70 -24
- data/lib/lutaml/key_value/transform.rb +5 -5
- data/lib/lutaml/key_value/transformation/collection_serializer.rb +25 -11
- data/lib/lutaml/key_value/transformation/value_serializer.rb +7 -7
- data/lib/lutaml/key_value/transformation.rb +27 -17
- data/lib/lutaml/model/adapter_resolver.rb +4 -6
- data/lib/lutaml/model/attribute.rb +26 -23
- data/lib/lutaml/model/cached_type_resolver.rb +10 -9
- data/lib/lutaml/model/cli.rb +1 -1
- data/lib/lutaml/model/collection.rb +4 -4
- data/lib/lutaml/model/comparable_model.rb +11 -11
- data/lib/lutaml/model/config.rb +1 -1
- data/lib/lutaml/model/consolidation/dispatcher.rb +1 -1
- data/lib/lutaml/model/consolidation/pattern_chunker.rb +3 -3
- data/lib/lutaml/model/format_registry.rb +6 -4
- data/lib/lutaml/model/global_context.rb +2 -2
- data/lib/lutaml/model/global_register.rb +1 -1
- data/lib/lutaml/model/instrumentation.rb +1 -1
- data/lib/lutaml/model/mapping/mapping_rule.rb +3 -3
- data/lib/lutaml/model/mapping/model_mapping.rb +1 -1
- data/lib/lutaml/model/mapping/model_mapping_rule.rb +1 -1
- data/lib/lutaml/model/register.rb +3 -3
- data/lib/lutaml/model/render_policy.rb +11 -17
- data/lib/lutaml/model/runtime_compatibility.rb +0 -1
- data/lib/lutaml/model/schema/xml_compiler/group.rb +1 -1
- data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +1 -1
- data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -2
- data/lib/lutaml/model/schema/xml_compiler.rb +14 -14
- data/lib/lutaml/model/serialize/attribute_definition.rb +1 -1
- data/lib/lutaml/model/serialize/deserialization_context.rb +50 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +2 -2
- data/lib/lutaml/model/serialize/initialization.rb +44 -7
- data/lib/lutaml/model/serialize/model_import.rb +1 -1
- data/lib/lutaml/model/serialize.rb +8 -1
- data/lib/lutaml/model/services/rule_value_extractor.rb +2 -1
- data/lib/lutaml/model/store.rb +77 -24
- data/lib/lutaml/model/transformation_registry.rb +1 -1
- data/lib/lutaml/model/type_context.rb +7 -1
- data/lib/lutaml/model/type_resolver.rb +1 -6
- data/lib/lutaml/model/utils.rb +19 -6
- data/lib/lutaml/model/validation_framework.rb +1 -1
- data/lib/lutaml/model/value_transformer.rb +2 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/rdf/mapping.rb +19 -13
- data/lib/lutaml/rdf/mapping_rule.rb +19 -2
- data/lib/lutaml/rdf/member_rule.rb +19 -2
- data/lib/lutaml/rdf/transform.rb +20 -11
- data/lib/lutaml/turtle/transform.rb +125 -53
- data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -1
- data/lib/lutaml/xml/adapter/base_adapter.rb +10 -14
- data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +3 -3
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +14 -14
- data/lib/lutaml/xml/adapter/xml_serializer.rb +3 -3
- data/lib/lutaml/xml/configurable.rb +2 -1
- data/lib/lutaml/xml/data_model.rb +2 -2
- data/lib/lutaml/xml/decisions/decision_context.rb +3 -3
- data/lib/lutaml/xml/decisions/rules/element_form_default_unqualified_rule.rb +1 -1
- data/lib/lutaml/xml/decisions/rules/element_form_option_rule.rb +1 -1
- data/lib/lutaml/xml/decisions/rules/used_prefix_rule.rb +1 -1
- data/lib/lutaml/xml/declaration_plan.rb +2 -2
- data/lib/lutaml/xml/declaration_planner.rb +12 -13
- data/lib/lutaml/xml/document.rb +13 -13
- data/lib/lutaml/xml/format_chooser.rb +3 -3
- data/lib/lutaml/xml/hoisting_algorithm.rb +1 -1
- data/lib/lutaml/xml/mapping.rb +2 -2
- data/lib/lutaml/xml/mapping_rule.rb +16 -3
- data/lib/lutaml/xml/model_transform.rb +17 -19
- data/lib/lutaml/xml/namespace_collector.rb +10 -10
- data/lib/lutaml/xml/namespace_declaration.rb +2 -2
- data/lib/lutaml/xml/namespace_declaration_data.rb +5 -8
- data/lib/lutaml/xml/namespace_scope_config.rb +3 -2
- data/lib/lutaml/xml/namespace_type_resolver.rb +4 -4
- data/lib/lutaml/xml/nokogiri/element.rb +2 -2
- data/lib/lutaml/xml/polymorphic_value_handler.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/base.rb +7 -7
- data/lib/lutaml/xml/schema/xsd/choice.rb +2 -2
- data/lib/lutaml/xml/schema/xsd/complex_type.rb +5 -5
- data/lib/lutaml/xml/schema/xsd/errors/message_builder.rb +3 -3
- data/lib/lutaml/xml/schema/xsd/group.rb +2 -2
- data/lib/lutaml/xml/schema/xsd/sequence.rb +2 -2
- data/lib/lutaml/xml/schema/xsd_schema.rb +5 -5
- data/lib/lutaml/xml/serialization/format_conversion.rb +4 -3
- data/lib/lutaml/xml/transformation/element_builder.rb +4 -2
- data/lib/lutaml/xml/transformation/rule_applier.rb +2 -2
- data/lib/lutaml/xml/transformation/value_serializer.rb +4 -6
- data/lib/lutaml/xml/transformation.rb +4 -4
- data/lib/lutaml/xml/type/configurable.rb +0 -4
- data/lib/lutaml/xml/xml_element.rb +21 -13
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/jsonld/transform_spec.rb +239 -0
- data/spec/lutaml/model/cached_type_resolver_spec.rb +3 -3
- data/spec/lutaml/model/optimization_spec.rb +228 -0
- data/spec/lutaml/model/store_spec.rb +195 -0
- data/spec/lutaml/rdf/mapping_rule_spec.rb +97 -0
- data/spec/lutaml/rdf/mapping_spec.rb +74 -4
- data/spec/lutaml/rdf/member_rule_spec.rb +41 -0
- data/spec/lutaml/rdf/rdf_transform_spec.rb +95 -29
- data/spec/lutaml/turtle/mapping_spec.rb +2 -2
- data/spec/lutaml/turtle/transform_spec.rb +315 -0
- data/spec/lutaml/xml/data_model_spec.rb +10 -28
- metadata +7 -4
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Optimization behaviors" do
|
|
6
|
+
before do
|
|
7
|
+
Lutaml::Model::GlobalContext.clear_caches
|
|
8
|
+
Lutaml::Model::TransformationRegistry.instance.clear
|
|
9
|
+
Lutaml::Model::GlobalRegister.instance.reset
|
|
10
|
+
Lutaml::Model::Store.clear
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after do
|
|
14
|
+
Lutaml::Model::GlobalContext.clear_caches
|
|
15
|
+
Lutaml::Model::TransformationRegistry.instance.clear
|
|
16
|
+
Lutaml::Model::GlobalRegister.instance.reset
|
|
17
|
+
Lutaml::Model::Store.clear
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "DeserializationContext propagation" do
|
|
21
|
+
let(:propagation_keys) do
|
|
22
|
+
Lutaml::Model::Serialize::DeserializationContext::PROPAGATION_KEYS
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "propagates only whitelisted keys" do
|
|
26
|
+
options = {
|
|
27
|
+
lutaml_parent: "parent",
|
|
28
|
+
lutaml_root: "root",
|
|
29
|
+
default_namespace: "http://example.com",
|
|
30
|
+
namespace_uri: "http://internal.com",
|
|
31
|
+
resolved_type: String,
|
|
32
|
+
converted: true,
|
|
33
|
+
polymorphic: :by_type,
|
|
34
|
+
collection: true,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
propagated = Lutaml::Model::Serialize::DeserializationContext.propagate(options)
|
|
38
|
+
|
|
39
|
+
expect(propagated).to eq({
|
|
40
|
+
lutaml_parent: "parent",
|
|
41
|
+
lutaml_root: "root",
|
|
42
|
+
default_namespace: "http://example.com",
|
|
43
|
+
polymorphic: :by_type,
|
|
44
|
+
collection: true,
|
|
45
|
+
})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "excludes parent-internal keys" do
|
|
49
|
+
options = {
|
|
50
|
+
namespace_uri: "http://internal.com",
|
|
51
|
+
resolved_type: String,
|
|
52
|
+
converted: true,
|
|
53
|
+
mappings: double("mappings"),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
propagated = Lutaml::Model::Serialize::DeserializationContext.propagate(options)
|
|
57
|
+
|
|
58
|
+
expect(propagated).to be_empty
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "CHILD_PROPAGATION_KEYS matches PROPAGATION_KEYS" do
|
|
62
|
+
expect(Lutaml::Model::Attribute::CHILD_PROPAGATION_KEYS).to eq(propagation_keys)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "conditional reference store registration" do
|
|
67
|
+
it "registers instances by default" do
|
|
68
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
69
|
+
attribute :id, :string
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
obj = klass.new(id: "test")
|
|
73
|
+
result = Lutaml::Model::Store.resolve(klass, :id, "test")
|
|
74
|
+
expect(result).to eq(obj)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "skips registration when skip_reference_registration is declared" do
|
|
78
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
79
|
+
attribute :id, :string
|
|
80
|
+
skip_reference_registration
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
klass.new(id: "test")
|
|
84
|
+
result = Lutaml::Model::Store.resolve(klass, :id, "test")
|
|
85
|
+
expect(result).to be_nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "reference_resolvable? returns true by default" do
|
|
89
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
90
|
+
attribute :id, :string
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
expect(klass.reference_resolvable?).to be true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "reference_resolvable? returns false after skip_reference_registration" do
|
|
97
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
98
|
+
attribute :id, :string
|
|
99
|
+
skip_reference_registration
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
expect(klass.reference_resolvable?).to be false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "skips registration during allocate_for_deserialization" do
|
|
106
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
107
|
+
attribute :id, :string
|
|
108
|
+
skip_reference_registration
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
instance = klass.allocate_for_deserialization
|
|
112
|
+
instance.public_send(:id=, "deserialized")
|
|
113
|
+
result = Lutaml::Model::Store.resolve(klass, :id, "deserialized")
|
|
114
|
+
expect(result).to be_nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "class_attributes accessor" do
|
|
119
|
+
it "returns raw attributes without register merging" do
|
|
120
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
121
|
+
attribute :name, :string
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
expect(klass.class_attributes).to be_a(Hash)
|
|
125
|
+
expect(klass.class_attributes.keys).to include(:name)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe "MappingRule#static_namespace_option" do
|
|
130
|
+
it "returns frozen hash for rules with explicit namespace" do
|
|
131
|
+
rule = Lutaml::Xml::MappingRule.new(
|
|
132
|
+
"test",
|
|
133
|
+
to: :test,
|
|
134
|
+
namespace: ExampleComNamespace,
|
|
135
|
+
namespace_set: true,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
result = rule.static_namespace_option
|
|
139
|
+
expect(result).to eq({ default_namespace: "http://example.com" })
|
|
140
|
+
expect(result).to be_frozen
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "returns the same object on repeated calls (cached)" do
|
|
144
|
+
rule = Lutaml::Xml::MappingRule.new(
|
|
145
|
+
"test",
|
|
146
|
+
to: :test,
|
|
147
|
+
namespace: ExampleComNamespace,
|
|
148
|
+
namespace_set: true,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
first = rule.static_namespace_option
|
|
152
|
+
second = rule.static_namespace_option
|
|
153
|
+
expect(first).to equal(second) # same object identity
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "returns nil when no namespace is set" do
|
|
157
|
+
rule = Lutaml::Xml::MappingRule.new("test", to: :test)
|
|
158
|
+
|
|
159
|
+
expect(rule.static_namespace_option).to be_nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "returns nil when namespace is :inherit" do
|
|
163
|
+
rule = Lutaml::Xml::MappingRule.new(
|
|
164
|
+
"test",
|
|
165
|
+
to: :test,
|
|
166
|
+
namespace: :inherit,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
expect(rule.static_namespace_option).to be_nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
describe "String deduplication in namespace URIs" do
|
|
174
|
+
it "XmlElement#namespace_uri deduplicates via unary minus" do
|
|
175
|
+
ns_instance = ExampleComNamespace.new
|
|
176
|
+
parent = Lutaml::Xml::XmlElement.new(nil, {}, [], nil,
|
|
177
|
+
name: "root",
|
|
178
|
+
parent_document: double(namespaces: { nil => ns_instance }))
|
|
179
|
+
uri = parent.namespace_uri
|
|
180
|
+
|
|
181
|
+
# The returned string should be the deduplicated version (-uri)
|
|
182
|
+
expect(uri).to eq("http://example.com")
|
|
183
|
+
expect(uri).to be_frozen
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "XmlElement#namespaced_name deduplicates the result string" do
|
|
187
|
+
ExampleComNamespace.new
|
|
188
|
+
parent = Lutaml::Xml::XmlElement.new(nil, {}, [], nil,
|
|
189
|
+
name: "root",
|
|
190
|
+
default_namespace: "http://example.com",
|
|
191
|
+
parent_document: double(namespaces: {}))
|
|
192
|
+
result = parent.namespaced_name
|
|
193
|
+
|
|
194
|
+
expect(result).to eq("http://example.com:root")
|
|
195
|
+
expect(result).to be_frozen
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
describe "value_set_for" do
|
|
200
|
+
it "transitions from nil (all defaults) to per-attribute tracking" do
|
|
201
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
202
|
+
attribute :name, :string
|
|
203
|
+
attribute :age, :integer
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
obj = klass.allocate_for_deserialization
|
|
207
|
+
expect(obj.using_default?(:name)).to be true
|
|
208
|
+
expect(obj.using_default?(:age)).to be true
|
|
209
|
+
|
|
210
|
+
obj.value_set_for(:name)
|
|
211
|
+
expect(obj.using_default?(:name)).to be false
|
|
212
|
+
expect(obj.using_default?(:age)).to be true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "values_set_for sets multiple attributes at once" do
|
|
216
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
217
|
+
attribute :name, :string
|
|
218
|
+
attribute :age, :integer
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
obj = klass.allocate_for_deserialization
|
|
222
|
+
obj.values_set_for(%i[name age])
|
|
223
|
+
|
|
224
|
+
expect(obj.using_default?(:name)).to be false
|
|
225
|
+
expect(obj.using_default?(:age)).to be false
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -69,6 +69,47 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
describe "multi-class index isolation" do
|
|
73
|
+
let(:other_class) do
|
|
74
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
75
|
+
attribute :id, :string
|
|
76
|
+
attribute :code, :string
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "does not mix indices across different classes" do
|
|
81
|
+
model_class.new(id: "a")
|
|
82
|
+
other_class.new(id: "a")
|
|
83
|
+
|
|
84
|
+
result = described_class.resolve(model_class, :id, "a")
|
|
85
|
+
expect(result).to be_a(model_class)
|
|
86
|
+
|
|
87
|
+
result2 = described_class.resolve(other_class, :id, "a")
|
|
88
|
+
expect(result2).to be_a(other_class)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "registering class B does not iterate class A's indices" do
|
|
92
|
+
# Build index for model_class (hold strong ref so GC cannot collect it)
|
|
93
|
+
_obj = model_class.new(id: "x")
|
|
94
|
+
described_class.resolve(model_class, :id, "x")
|
|
95
|
+
|
|
96
|
+
# Registering other_class should not trigger work on model_class indices
|
|
97
|
+
100.times { |i| other_class.new(id: "other-#{i}", code: "c#{i}") }
|
|
98
|
+
result = described_class.resolve(model_class, :id, "x")
|
|
99
|
+
expect(result.id).to eq("x")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "resolves by different reference keys independently per class" do
|
|
103
|
+
other_class.new(id: "alpha", code: "Z1")
|
|
104
|
+
|
|
105
|
+
result = described_class.resolve(other_class, :id, "alpha")
|
|
106
|
+
expect(result.id).to eq("alpha")
|
|
107
|
+
|
|
108
|
+
result2 = described_class.resolve(other_class, :code, "Z1")
|
|
109
|
+
expect(result2.code).to eq("Z1")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
72
113
|
describe "WeakRef behavior" do
|
|
73
114
|
it "uses WeakRef for storage (objects can be collected when unreferenced)" do
|
|
74
115
|
obj = model_class.new(id: "alive")
|
|
@@ -97,6 +138,160 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
97
138
|
end
|
|
98
139
|
end
|
|
99
140
|
|
|
141
|
+
describe "compaction amortisation" do
|
|
142
|
+
it "bounds the number of full-array compactions across many live registers" do
|
|
143
|
+
threshold = Lutaml::Model::Store::COMPACTION_THRESHOLD
|
|
144
|
+
interval = Lutaml::Model::Store::COMPACTION_INTERVAL
|
|
145
|
+
n = threshold + (3 * interval)
|
|
146
|
+
|
|
147
|
+
instance = described_class.instance
|
|
148
|
+
|
|
149
|
+
# Holding strong refs so WeakRefs stay alive across the registers.
|
|
150
|
+
objects = Array.new(n) { |i| model_class.new(id: "amortise-#{i}") }
|
|
151
|
+
expect(objects.size).to eq(n)
|
|
152
|
+
|
|
153
|
+
# Without amortisation this would be ~3000 compactions (one per register
|
|
154
|
+
# past threshold). With amortisation it fires once per INTERVAL inserts,
|
|
155
|
+
# so ~3 compactions plus a small slack.
|
|
156
|
+
expect(instance.compaction_count).to be <= 5
|
|
157
|
+
|
|
158
|
+
# Correctness: the most recently registered object still resolves.
|
|
159
|
+
expect(described_class.resolve(model_class, :id, "amortise-#{n - 1}").id)
|
|
160
|
+
.to eq("amortise-#{n - 1}")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "resets the per-class insertion counter on clear" do
|
|
164
|
+
threshold = Lutaml::Model::Store::COMPACTION_THRESHOLD
|
|
165
|
+
objects = Array.new(threshold + 10) do |i|
|
|
166
|
+
model_class.new(id: "pre-#{i}")
|
|
167
|
+
end
|
|
168
|
+
expect(objects.size).to eq(threshold + 10)
|
|
169
|
+
described_class.clear
|
|
170
|
+
|
|
171
|
+
expect(described_class.instance.inserts_since_compaction).to be_empty
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "resets the compaction counter on clear" do
|
|
175
|
+
threshold = Lutaml::Model::Store::COMPACTION_THRESHOLD
|
|
176
|
+
interval = Lutaml::Model::Store::COMPACTION_INTERVAL
|
|
177
|
+
_objects = Array.new(threshold + interval + 10) do |i|
|
|
178
|
+
model_class.new(id: "pre-#{i}")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
described_class.clear
|
|
182
|
+
|
|
183
|
+
expect(described_class.instance.compaction_count).to eq(0)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "removes dead refs during compaction" do
|
|
187
|
+
threshold = Lutaml::Model::Store::COMPACTION_THRESHOLD
|
|
188
|
+
interval = Lutaml::Model::Store::COMPACTION_INTERVAL
|
|
189
|
+
instance = described_class.instance
|
|
190
|
+
|
|
191
|
+
# Register enough objects to trigger compaction, then release them.
|
|
192
|
+
# Use .times so each object is immediately unreferenced after the
|
|
193
|
+
# iteration — no Array holds strong refs, so GC can collect them.
|
|
194
|
+
(threshold + 1).times { |i| model_class.new(id: "die-#{i}") }
|
|
195
|
+
GC.start
|
|
196
|
+
|
|
197
|
+
# Register enough more to cross the interval gate and trigger compaction.
|
|
198
|
+
Array.new(interval) { |i| model_class.new(id: "live-#{i}") }
|
|
199
|
+
|
|
200
|
+
# After compaction, the refs array should be smaller than before
|
|
201
|
+
# (some dead refs removed). Exact count depends on GC timing,
|
|
202
|
+
# but the live refs must still be present.
|
|
203
|
+
live = instance.refs_for(model_class.to_s)
|
|
204
|
+
alive_count = live.count do |ref|
|
|
205
|
+
ref.weakref_alive?
|
|
206
|
+
rescue WeakRef::RefError
|
|
207
|
+
false
|
|
208
|
+
end
|
|
209
|
+
expect(alive_count).to be < (threshold + 1 + interval)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
it "maintains per-class counter independence" do
|
|
213
|
+
other_class = Class.new(Lutaml::Model::Serializable) do
|
|
214
|
+
attribute :id, :string
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Register 5 model_class objects and 3 other_class objects.
|
|
218
|
+
_objects_a = Array.new(5) { |i| model_class.new(id: "a-#{i}") }
|
|
219
|
+
_objects_b = Array.new(3) { |i| other_class.new(id: "b-#{i}") }
|
|
220
|
+
|
|
221
|
+
counters = described_class.instance.inserts_since_compaction
|
|
222
|
+
expect(counters[model_class.to_s]).to eq(5)
|
|
223
|
+
expect(counters[other_class.to_s]).to eq(3)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "does not compact when exactly at threshold" do
|
|
227
|
+
threshold = Lutaml::Model::Store::COMPACTION_THRESHOLD
|
|
228
|
+
instance = described_class.instance
|
|
229
|
+
|
|
230
|
+
_objects = Array.new(threshold) { |i| model_class.new(id: "edge-#{i}") }
|
|
231
|
+
|
|
232
|
+
# refs.size == threshold, which does not satisfy size > threshold
|
|
233
|
+
expect(instance.compaction_count).to eq(0)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
describe "index pruning" do
|
|
238
|
+
it "removes stale entry on resolve" do
|
|
239
|
+
instance = described_class.instance
|
|
240
|
+
|
|
241
|
+
_obj = model_class.new(id: "stale")
|
|
242
|
+
described_class.resolve(model_class, :id, "stale")
|
|
243
|
+
expect(instance.index_entry_count(model_class.to_s)).to eq(1)
|
|
244
|
+
|
|
245
|
+
_obj = nil
|
|
246
|
+
GC.start
|
|
247
|
+
|
|
248
|
+
expect(described_class.resolve(model_class, :id, "stale")).to be_nil
|
|
249
|
+
expect(instance.index_entry_count(model_class.to_s)).to eq(0)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "prunes dead index entries during compaction" do
|
|
253
|
+
threshold = described_class::COMPACTION_THRESHOLD
|
|
254
|
+
interval = described_class::COMPACTION_INTERVAL
|
|
255
|
+
instance = described_class.instance
|
|
256
|
+
|
|
257
|
+
# Register and index a batch of objects
|
|
258
|
+
_batch = Array.new(threshold + 1) { |i| model_class.new(id: "die-#{i}") }
|
|
259
|
+
described_class.resolve(model_class, :id, "die-0")
|
|
260
|
+
expect(instance.index_entry_count(model_class.to_s)).to eq(threshold + 1)
|
|
261
|
+
|
|
262
|
+
# Release and trigger compaction
|
|
263
|
+
_batch = nil
|
|
264
|
+
GC.start
|
|
265
|
+
Array.new(interval) { |i| model_class.new(id: "live-#{i}") }
|
|
266
|
+
|
|
267
|
+
# Dead entries should be pruned; only live entries remain
|
|
268
|
+
expect(instance.index_entry_count(model_class.to_s)).to be <= interval + 50
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "rebuilds index after pruning removes all entries for a reference key" do
|
|
272
|
+
threshold = described_class::COMPACTION_THRESHOLD
|
|
273
|
+
interval = described_class::COMPACTION_INTERVAL
|
|
274
|
+
|
|
275
|
+
# Register and index by :name only
|
|
276
|
+
_batch = Array.new(threshold + 1) do |i|
|
|
277
|
+
model_class.new(id: "die-#{i}", name: "n-#{i}")
|
|
278
|
+
end
|
|
279
|
+
described_class.resolve(model_class, :name, "n-0")
|
|
280
|
+
|
|
281
|
+
# Release all and trigger compaction
|
|
282
|
+
_batch = nil
|
|
283
|
+
GC.start
|
|
284
|
+
Array.new(interval) do |i|
|
|
285
|
+
model_class.new(id: "live-#{i}", name: "live-n-#{i}")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Index for :name should be rebuilt on next resolve
|
|
289
|
+
new_obj = model_class.new(id: "fresh", name: "fresh-name")
|
|
290
|
+
expect(described_class.resolve(model_class, :name,
|
|
291
|
+
"fresh-name")).to eq(new_obj)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
100
295
|
describe "#clear" do
|
|
101
296
|
it "removes all registered objects" do
|
|
102
297
|
model_class.new(id: "a")
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/rdf"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::Rdf::MappingRule do
|
|
7
|
+
subject(:rule) do
|
|
8
|
+
described_class.new(
|
|
9
|
+
:prefLabel,
|
|
10
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
11
|
+
to: :name,
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe ".new" do
|
|
16
|
+
it "stores predicate_name as frozen string" do
|
|
17
|
+
expect(rule.predicate_name).to eq("prefLabel")
|
|
18
|
+
expect(rule.predicate_name).to be_frozen
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "stores to as symbol" do
|
|
22
|
+
expect(rule.to).to eq(:name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "defaults lang_tagged to false" do
|
|
26
|
+
expect(rule.lang_tagged).to be(false)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "defaults uri_reference to false" do
|
|
30
|
+
expect(rule.uri_reference).to be(false)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "raises when predicate_name is nil" do
|
|
34
|
+
expect do
|
|
35
|
+
described_class.new(nil,
|
|
36
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
37
|
+
to: :name)
|
|
38
|
+
end.to raise_error(ArgumentError, /predicate_name is required/)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "raises when namespace is not a Rdf::Namespace subclass" do
|
|
42
|
+
expect do
|
|
43
|
+
described_class.new(:foo, namespace: String, to: :bar)
|
|
44
|
+
end.to raise_error(ArgumentError, /Rdf::Namespace/)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "raises when to is nil" do
|
|
48
|
+
expect do
|
|
49
|
+
described_class.new(:foo,
|
|
50
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
51
|
+
to: nil)
|
|
52
|
+
end.to raise_error(ArgumentError, /required/)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "raises when both lang_tagged and uri_reference are true" do
|
|
56
|
+
expect do
|
|
57
|
+
described_class.new(:foo,
|
|
58
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
59
|
+
to: :bar,
|
|
60
|
+
lang_tagged: true,
|
|
61
|
+
uri_reference: true)
|
|
62
|
+
end.to raise_error(ArgumentError, /mutually exclusive/)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "#kind" do
|
|
67
|
+
it "returns :plain by default" do
|
|
68
|
+
expect(rule.kind).to eq(:plain)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "returns :lang_tagged when lang_tagged" do
|
|
72
|
+
r = described_class.new(:prefLabel,
|
|
73
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
74
|
+
to: :name, lang_tagged: true)
|
|
75
|
+
expect(r.kind).to eq(:lang_tagged)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "returns :uri_reference when uri_reference" do
|
|
79
|
+
r = described_class.new(:related,
|
|
80
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
81
|
+
to: :related, uri_reference: true)
|
|
82
|
+
expect(r.kind).to eq(:uri_reference)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe "#uri" do
|
|
87
|
+
it "resolves predicate name to full URI via namespace" do
|
|
88
|
+
expect(rule.uri).to eq("http://www.w3.org/2004/02/skos/core#prefLabel")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe "#prefixed_name" do
|
|
93
|
+
it "returns prefix:local form" do
|
|
94
|
+
expect(rule.prefixed_name).to eq("skos:prefLabel")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -29,9 +29,20 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
describe "#type" do
|
|
32
|
-
it "stores RDF type" do
|
|
32
|
+
it "stores single RDF type as array" do
|
|
33
33
|
mapping.type("skos:Concept")
|
|
34
|
-
expect(mapping.rdf_type).to eq("skos:Concept")
|
|
34
|
+
expect(mapping.rdf_type).to eq(["skos:Concept"])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "stores multiple RDF types" do
|
|
38
|
+
mapping.type(["skos:Concept", "dcterms:Agent"])
|
|
39
|
+
expect(mapping.rdf_type).to eq(["skos:Concept", "dcterms:Agent"])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "overwrites previous type on subsequent call" do
|
|
43
|
+
mapping.type("skos:Concept")
|
|
44
|
+
mapping.type("dcterms:Agent")
|
|
45
|
+
expect(mapping.rdf_type).to eq(["dcterms:Agent"])
|
|
35
46
|
end
|
|
36
47
|
end
|
|
37
48
|
|
|
@@ -60,6 +71,28 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
60
71
|
expect(mapping.rdf_predicates.first.lang_tagged).to be(true)
|
|
61
72
|
end
|
|
62
73
|
|
|
74
|
+
it "creates MappingRule with uri_reference option" do
|
|
75
|
+
mapping.predicate(
|
|
76
|
+
:related,
|
|
77
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
78
|
+
to: :related,
|
|
79
|
+
uri_reference: true,
|
|
80
|
+
)
|
|
81
|
+
expect(mapping.rdf_predicates.first.uri_reference).to be(true)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "rejects lang_tagged combined with uri_reference" do
|
|
85
|
+
expect do
|
|
86
|
+
mapping.predicate(
|
|
87
|
+
:related,
|
|
88
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
89
|
+
to: :related,
|
|
90
|
+
lang_tagged: true,
|
|
91
|
+
uri_reference: true,
|
|
92
|
+
)
|
|
93
|
+
end.to raise_error(ArgumentError, /mutually exclusive/)
|
|
94
|
+
end
|
|
95
|
+
|
|
63
96
|
it "registers multiple predicates" do
|
|
64
97
|
mapping.predicate(:prefLabel,
|
|
65
98
|
namespace: Lutaml::Rdf::Namespaces::SkosNamespace, to: :name)
|
|
@@ -100,6 +133,28 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
100
133
|
expect(mapping.rdf_members.length).to eq(1)
|
|
101
134
|
expect(mapping.rdf_members.first.attr_name).to eq(:items)
|
|
102
135
|
end
|
|
136
|
+
|
|
137
|
+
it "creates MemberRule with linking predicate" do
|
|
138
|
+
mapping.members(:items,
|
|
139
|
+
predicate_name: :member,
|
|
140
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
141
|
+
rule = mapping.rdf_members.first
|
|
142
|
+
expect(rule.linked?).to be(true)
|
|
143
|
+
expect(rule.linked_predicate_uri).to eq("http://www.w3.org/2004/02/skos/core#member")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it "creates MemberRule without linking predicate" do
|
|
147
|
+
mapping.members(:items)
|
|
148
|
+
rule = mapping.rdf_members.first
|
|
149
|
+
expect(rule.linked?).to be(false)
|
|
150
|
+
expect(rule.linked_predicate_uri).to be_nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "raises when predicate_name given without namespace" do
|
|
154
|
+
expect do
|
|
155
|
+
mapping.members(:items, predicate_name: :member)
|
|
156
|
+
end.to raise_error(ArgumentError, /namespace is required/)
|
|
157
|
+
end
|
|
103
158
|
end
|
|
104
159
|
|
|
105
160
|
describe "#mappings" do
|
|
@@ -140,7 +195,7 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
140
195
|
Lutaml::Rdf::Namespaces::DctermsNamespace,
|
|
141
196
|
)
|
|
142
197
|
mapping.subject { |m| "http://example.org/#{m.name}" }
|
|
143
|
-
mapping.type("skos:Concept")
|
|
198
|
+
mapping.type(["skos:Concept", "dcterms:Agent"])
|
|
144
199
|
mapping.predicate(:prefLabel,
|
|
145
200
|
namespace: Lutaml::Rdf::Namespaces::SkosNamespace, to: :name)
|
|
146
201
|
end
|
|
@@ -149,7 +204,7 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
149
204
|
duped = mapping.deep_dup
|
|
150
205
|
expect(duped.namespace_set.size).to eq(2)
|
|
151
206
|
expect(duped.rdf_subject).to be_a(Proc)
|
|
152
|
-
expect(duped.rdf_type).to eq("skos:Concept")
|
|
207
|
+
expect(duped.rdf_type).to eq(["skos:Concept", "dcterms:Agent"])
|
|
153
208
|
expect(duped.rdf_predicates.length).to eq(1)
|
|
154
209
|
end
|
|
155
210
|
|
|
@@ -160,5 +215,20 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
160
215
|
expect(mapping.rdf_predicates.length).to eq(1)
|
|
161
216
|
expect(duped.rdf_predicates.length).to eq(2)
|
|
162
217
|
end
|
|
218
|
+
|
|
219
|
+
it "does not share type array with original" do
|
|
220
|
+
duped = mapping.deep_dup
|
|
221
|
+
duped.type("skos:Collection")
|
|
222
|
+
expect(mapping.rdf_type).to eq(["skos:Concept", "dcterms:Agent"])
|
|
223
|
+
expect(duped.rdf_type).to eq(["skos:Collection"])
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "does not share member state with original" do
|
|
227
|
+
mapping.members(:items)
|
|
228
|
+
duped = mapping.deep_dup
|
|
229
|
+
duped.members(:more_items)
|
|
230
|
+
expect(mapping.rdf_members.length).to eq(1)
|
|
231
|
+
expect(duped.rdf_members.length).to eq(2)
|
|
232
|
+
end
|
|
163
233
|
end
|
|
164
234
|
end
|
|
@@ -13,5 +13,46 @@ RSpec.describe Lutaml::Rdf::MemberRule do
|
|
|
13
13
|
rule = described_class.new("concepts")
|
|
14
14
|
expect(rule.attr_name).to eq(:concepts)
|
|
15
15
|
end
|
|
16
|
+
|
|
17
|
+
it "raises ArgumentError when predicate_name given without namespace" do
|
|
18
|
+
expect do
|
|
19
|
+
described_class.new(:items, predicate_name: :member)
|
|
20
|
+
end.to raise_error(ArgumentError, /namespace is required/)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "allows both predicate_name and namespace together" do
|
|
24
|
+
rule = described_class.new(:items,
|
|
25
|
+
predicate_name: :member,
|
|
26
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
27
|
+
expect(rule.predicate_name).to eq(:member)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "#linked?" do
|
|
32
|
+
it "returns false when no predicate_name" do
|
|
33
|
+
rule = described_class.new(:items)
|
|
34
|
+
expect(rule.linked?).to be(false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "returns true when predicate_name is set" do
|
|
38
|
+
rule = described_class.new(:items,
|
|
39
|
+
predicate_name: :member,
|
|
40
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
41
|
+
expect(rule.linked?).to be(true)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe "#linked_predicate_uri" do
|
|
46
|
+
it "returns nil when no linking predicate" do
|
|
47
|
+
rule = described_class.new(:items)
|
|
48
|
+
expect(rule.linked_predicate_uri).to be_nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "resolves the linking predicate URI" do
|
|
52
|
+
rule = described_class.new(:items,
|
|
53
|
+
predicate_name: :member,
|
|
54
|
+
namespace: Lutaml::Rdf::Namespaces::SkosNamespace)
|
|
55
|
+
expect(rule.linked_predicate_uri).to eq("http://www.w3.org/2004/02/skos/core#member")
|
|
56
|
+
end
|
|
16
57
|
end
|
|
17
58
|
end
|