lutaml-model 0.8.10 → 0.8.12
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/.github/workflows/opal.yml +31 -0
- data/.rspec-opal +5 -0
- data/.rubocop_todo.yml +68 -7
- data/README.adoc +53 -1
- data/docs/_guides/index.adoc +4 -0
- data/docs/_guides/jsonld-serialization.adoc +3 -1
- data/docs/_guides/opal.adoc +221 -0
- data/docs/_guides/rdf-serialization.adoc +94 -8
- data/docs/_guides/turtle-serialization.adoc +17 -4
- data/docs/_guides/xml_mappings/07_best_practices.adoc +2 -1
- data/docs/_pages/configuration.adoc +9 -4
- data/docs/_pages/index.adoc +1 -0
- data/docs/_pages/serialization_adapters.adoc +3 -2
- data/docs/index.adoc +1 -0
- data/lib/lutaml/hash_format/adapter/mapping.rb +2 -4
- data/lib/lutaml/json/adapter/mapping.rb +2 -4
- data/lib/lutaml/jsonl/adapter/mapping.rb +2 -4
- data/lib/lutaml/jsonld/transform.rb +70 -24
- data/lib/lutaml/key_value/adapter/hash/mapping.rb +2 -4
- data/lib/lutaml/key_value/adapter/json/mapping.rb +2 -4
- data/lib/lutaml/key_value/adapter/jsonl/mapping.rb +2 -4
- data/lib/lutaml/key_value/adapter/toml/mapping.rb +2 -4
- data/lib/lutaml/key_value/adapter/yaml/mapping.rb +2 -4
- data/lib/lutaml/key_value/adapter/yamls/mapping.rb +2 -4
- data/lib/lutaml/key_value/mapping.rb +4 -4
- data/lib/lutaml/model/adapter_resolver.rb +5 -8
- data/lib/lutaml/model/mapping/mapping.rb +12 -0
- data/lib/lutaml/model/store.rb +51 -4
- 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/toml/adapter/mapping.rb +2 -4
- data/lib/lutaml/turtle/transform.rb +125 -53
- data/lib/lutaml/xml/schema/xsd.rb +5 -4
- data/lib/lutaml/xml/schema.rb +8 -5
- data/lib/lutaml/xml/xml_orderable.rb +17 -0
- data/lib/lutaml/xml.rb +8 -11
- data/lib/lutaml/yaml/adapter/mapping.rb +2 -4
- data/lib/lutaml/yamls/adapter/mapping.rb +7 -3
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/jsonld/transform_spec.rb +239 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +117 -0
- data/spec/lutaml/model/store_spec.rb +156 -2
- 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/opal_xml_spec.rb +145 -0
- data/spec/lutaml/xml/xml_spec.rb +64 -13
- data/spec/support/opal.rb +6 -0
- metadata +12 -4
|
@@ -208,4 +208,243 @@ RSpec.describe Lutaml::JsonLd::Transform do
|
|
|
208
208
|
expect(restored.tags).to eq(["en", "fr"])
|
|
209
209
|
end
|
|
210
210
|
end
|
|
211
|
+
|
|
212
|
+
describe "multiple types" do
|
|
213
|
+
before do
|
|
214
|
+
stub_const("DctermsTestNs", Class.new(Lutaml::Rdf::Namespace) do
|
|
215
|
+
uri "http://purl.org/dc/terms/"
|
|
216
|
+
prefix "dcterms"
|
|
217
|
+
end)
|
|
218
|
+
|
|
219
|
+
stub_const("MultiTypeJsonLdModel", Class.new(Lutaml::Model::Serializable) do
|
|
220
|
+
attribute :name, :string
|
|
221
|
+
|
|
222
|
+
rdf do
|
|
223
|
+
namespace TestSkosNs, DctermsTestNs
|
|
224
|
+
|
|
225
|
+
subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
|
|
226
|
+
|
|
227
|
+
type ["skos:Concept", "dcterms:Agent"]
|
|
228
|
+
|
|
229
|
+
predicate :name, namespace: TestExNs, to: :name
|
|
230
|
+
end
|
|
231
|
+
end)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "generates @type as array for multiple types" do
|
|
235
|
+
instance = MultiTypeJsonLdModel.new(name: "multi")
|
|
236
|
+
parsed = JSON.parse(instance.to_jsonld)
|
|
237
|
+
expect(parsed["@type"]).to eq(["skos:Concept", "dcterms:Agent"])
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "generates @type as string for single type" do
|
|
241
|
+
instance = JsonLdTestModel.new(name: "single")
|
|
242
|
+
parsed = JSON.parse(instance.to_jsonld)
|
|
243
|
+
expect(parsed["@type"]).to eq("skos:Concept")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
describe "URI reference predicates" do
|
|
248
|
+
before do
|
|
249
|
+
stub_const("UriRefJsonLdModel", Class.new(Lutaml::Model::Serializable) do
|
|
250
|
+
attribute :name, :string
|
|
251
|
+
attribute :related, :string, collection: true
|
|
252
|
+
|
|
253
|
+
rdf do
|
|
254
|
+
namespace TestSkosNs, TestExNs
|
|
255
|
+
|
|
256
|
+
subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
|
|
257
|
+
|
|
258
|
+
type "skos:Concept"
|
|
259
|
+
|
|
260
|
+
predicate :name, namespace: TestExNs, to: :name
|
|
261
|
+
predicate :related, namespace: TestSkosNs, to: :related,
|
|
262
|
+
uri_reference: true
|
|
263
|
+
end
|
|
264
|
+
end)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
it "generates @type @id in context for uri_reference predicates" do
|
|
268
|
+
instance = UriRefJsonLdModel.new(name: "test", related: ["skos:other"])
|
|
269
|
+
parsed = JSON.parse(instance.to_jsonld)
|
|
270
|
+
expect(parsed["@context"]["related"]).to eq({
|
|
271
|
+
"@id" => "http://www.w3.org/2004/02/skos/core#related",
|
|
272
|
+
"@type" => "@id",
|
|
273
|
+
})
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it "serializes URI reference as @id object" do
|
|
277
|
+
instance = UriRefJsonLdModel.new(name: "test", related: ["skos:other"])
|
|
278
|
+
parsed = JSON.parse(instance.to_jsonld)
|
|
279
|
+
expect(parsed["related"]).to eq([{ "@id" => "skos:other" }])
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it "serializes single URI reference value as @id object" do
|
|
283
|
+
stub_const("SingleUriRefModel", Class.new(Lutaml::Model::Serializable) do
|
|
284
|
+
attribute :name, :string
|
|
285
|
+
attribute :link, :string
|
|
286
|
+
|
|
287
|
+
rdf do
|
|
288
|
+
namespace TestSkosNs, TestExNs
|
|
289
|
+
|
|
290
|
+
subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
|
|
291
|
+
|
|
292
|
+
type "skos:Concept"
|
|
293
|
+
|
|
294
|
+
predicate :name, namespace: TestExNs, to: :name
|
|
295
|
+
predicate :related, namespace: TestSkosNs, to: :link,
|
|
296
|
+
uri_reference: true
|
|
297
|
+
end
|
|
298
|
+
end)
|
|
299
|
+
|
|
300
|
+
instance = SingleUriRefModel.new(name: "test", link: "skos:something")
|
|
301
|
+
parsed = JSON.parse(instance.to_jsonld)
|
|
302
|
+
expect(parsed["related"]).to eq({ "@id" => "skos:something" })
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
describe "member linking predicates" do
|
|
307
|
+
before do
|
|
308
|
+
stub_const("JsonLdChildModel", Class.new(Lutaml::Model::Serializable) do
|
|
309
|
+
attribute :cid, :string
|
|
310
|
+
attribute :label, :string
|
|
311
|
+
|
|
312
|
+
rdf do
|
|
313
|
+
namespace TestSkosNs, TestExNs
|
|
314
|
+
|
|
315
|
+
subject { |m| "http://example.org/item/#{m.cid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
316
|
+
|
|
317
|
+
type "skos:Concept"
|
|
318
|
+
|
|
319
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :label
|
|
320
|
+
end
|
|
321
|
+
end)
|
|
322
|
+
|
|
323
|
+
stub_const("JsonLdParentModel", Class.new(Lutaml::Model::Serializable) do
|
|
324
|
+
attribute :name, :string
|
|
325
|
+
attribute :children, JsonLdChildModel, collection: true
|
|
326
|
+
|
|
327
|
+
rdf do
|
|
328
|
+
namespace TestSkosNs, TestExNs
|
|
329
|
+
|
|
330
|
+
subject { |m| "http://example.org/group/#{m.name}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
331
|
+
|
|
332
|
+
type "skos:Collection"
|
|
333
|
+
|
|
334
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :name
|
|
335
|
+
|
|
336
|
+
members :children,
|
|
337
|
+
predicate_name: :member,
|
|
338
|
+
namespace: TestSkosNs
|
|
339
|
+
end
|
|
340
|
+
end)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "includes linking term in @context with @type @id" do
|
|
344
|
+
parent = JsonLdParentModel.new(
|
|
345
|
+
name: "grp1",
|
|
346
|
+
children: [JsonLdChildModel.new(cid: "a", label: "Alpha")],
|
|
347
|
+
)
|
|
348
|
+
parsed = JSON.parse(parent.to_jsonld)
|
|
349
|
+
expect(parsed["@context"]["member"]).to eq({
|
|
350
|
+
"@id" => "http://www.w3.org/2004/02/skos/core#member",
|
|
351
|
+
"@type" => "@id",
|
|
352
|
+
})
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it "generates @id references for linked members in @graph" do
|
|
356
|
+
parent = JsonLdParentModel.new(
|
|
357
|
+
name: "grp1",
|
|
358
|
+
children: [
|
|
359
|
+
JsonLdChildModel.new(cid: "a", label: "Alpha"),
|
|
360
|
+
JsonLdChildModel.new(cid: "b", label: "Beta"),
|
|
361
|
+
],
|
|
362
|
+
)
|
|
363
|
+
parsed = JSON.parse(parent.to_jsonld)
|
|
364
|
+
parent_resource = parsed["@graph"].find { |r| r["@type"] }
|
|
365
|
+
expect(parent_resource["member"]).to eq([
|
|
366
|
+
{ "@id" => "http://example.org/item/a" },
|
|
367
|
+
{ "@id" => "http://example.org/item/b" },
|
|
368
|
+
])
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it "includes member resources in @graph" do
|
|
372
|
+
parent = JsonLdParentModel.new(
|
|
373
|
+
name: "grp1",
|
|
374
|
+
children: [JsonLdChildModel.new(cid: "a", label: "Alpha")],
|
|
375
|
+
)
|
|
376
|
+
parsed = JSON.parse(parent.to_jsonld)
|
|
377
|
+
member = parsed["@graph"].find { |r| r["prefLabel"] == "Alpha" }
|
|
378
|
+
expect(member).not_to be_nil
|
|
379
|
+
expect(member["@id"]).to eq("http://example.org/item/a")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
it "merges child namespaces into @context" do
|
|
383
|
+
parent = JsonLdParentModel.new(
|
|
384
|
+
name: "grp1",
|
|
385
|
+
children: [JsonLdChildModel.new(cid: "a", label: "Alpha")],
|
|
386
|
+
)
|
|
387
|
+
parsed = JSON.parse(parent.to_jsonld)
|
|
388
|
+
expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
|
|
389
|
+
expect(parsed["@context"]["ex"]).to eq("http://example.org/")
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
it "omits linking key when members have no linking predicate" do
|
|
393
|
+
stub_const("UnlinkedChild", Class.new(Lutaml::Model::Serializable) do
|
|
394
|
+
attribute :label, :string
|
|
395
|
+
|
|
396
|
+
rdf do
|
|
397
|
+
namespace TestSkosNs
|
|
398
|
+
|
|
399
|
+
subject { |m| "http://example.org/#{m.label}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
400
|
+
|
|
401
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :label
|
|
402
|
+
end
|
|
403
|
+
end)
|
|
404
|
+
|
|
405
|
+
stub_const("UnlinkedParent", Class.new(Lutaml::Model::Serializable) do
|
|
406
|
+
attribute :name, :string
|
|
407
|
+
attribute :items, UnlinkedChild, collection: true
|
|
408
|
+
|
|
409
|
+
rdf do
|
|
410
|
+
namespace TestSkosNs
|
|
411
|
+
|
|
412
|
+
subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
|
|
413
|
+
|
|
414
|
+
predicate :prefLabel, namespace: TestSkosNs, to: :name
|
|
415
|
+
|
|
416
|
+
members :items
|
|
417
|
+
end
|
|
418
|
+
end)
|
|
419
|
+
|
|
420
|
+
parent = UnlinkedParent.new(
|
|
421
|
+
name: "grp",
|
|
422
|
+
items: [UnlinkedChild.new(label: "a")],
|
|
423
|
+
)
|
|
424
|
+
parsed = JSON.parse(parent.to_jsonld)
|
|
425
|
+
expect(parsed["@context"]).not_to have_key("member")
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
describe "empty type array" do
|
|
430
|
+
before do
|
|
431
|
+
stub_const("NoTypeJsonLdModel", Class.new(Lutaml::Model::Serializable) do
|
|
432
|
+
attribute :name, :string
|
|
433
|
+
|
|
434
|
+
rdf do
|
|
435
|
+
namespace TestExNs
|
|
436
|
+
|
|
437
|
+
subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
|
|
438
|
+
|
|
439
|
+
predicate :name, namespace: TestExNs, to: :name
|
|
440
|
+
end
|
|
441
|
+
end)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it "omits @type when no types declared" do
|
|
445
|
+
instance = NoTypeJsonLdModel.new(name: "test")
|
|
446
|
+
parsed = JSON.parse(instance.to_jsonld)
|
|
447
|
+
expect(parsed).not_to have_key("@type")
|
|
448
|
+
end
|
|
449
|
+
end
|
|
211
450
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Opal compatibility", if: RUBY_ENGINE == "opal" do
|
|
6
|
+
it "includes Serialize in a class" do
|
|
7
|
+
klass = Class.new { include Lutaml::Model::Serialize }
|
|
8
|
+
expect(klass.include?(Lutaml::Model::Serialize)).to be true
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "defines attributes and serializes to hash" do
|
|
12
|
+
person = Class.new do
|
|
13
|
+
include Lutaml::Model::Serialize
|
|
14
|
+
|
|
15
|
+
attribute :name, :string
|
|
16
|
+
attribute :age, :integer
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
instance = person.new(name: "Alice", age: 30)
|
|
20
|
+
expect(instance.to_hash).to eq({ "name" => "Alice", "age" => 30 })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "round-trips JSON serialization" do
|
|
24
|
+
person = Class.new do
|
|
25
|
+
include Lutaml::Model::Serialize
|
|
26
|
+
|
|
27
|
+
attribute :name, :string
|
|
28
|
+
attribute :age, :integer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
instance = person.from_json('{"name":"Bob","age":25}')
|
|
32
|
+
expect(instance.name).to eq("Bob")
|
|
33
|
+
expect(instance.age).to eq(25)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "handles collections" do
|
|
37
|
+
team = Class.new do
|
|
38
|
+
include Lutaml::Model::Serialize
|
|
39
|
+
|
|
40
|
+
attribute :members, :string, collection: true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
instance = team.new(members: %w[Alice Bob Carol])
|
|
44
|
+
expect(instance.members).to eq(%w[Alice Bob Carol])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "handles defaults" do
|
|
48
|
+
widget = Class.new do
|
|
49
|
+
include Lutaml::Model::Serialize
|
|
50
|
+
|
|
51
|
+
attribute :name, :string
|
|
52
|
+
attribute :active, :boolean, default: -> { true }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
instance = widget.new(name: "test")
|
|
56
|
+
expect(instance.active).to be true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "handles type coercion" do
|
|
60
|
+
record = Class.new do
|
|
61
|
+
include Lutaml::Model::Serialize
|
|
62
|
+
|
|
63
|
+
attribute :count, :integer
|
|
64
|
+
attribute :ratio, :float
|
|
65
|
+
attribute :flag, :boolean
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
instance = record.new(count: "42", ratio: "3.14", flag: "true")
|
|
69
|
+
expect(instance.count).to eq(42)
|
|
70
|
+
expect(instance.ratio).to eq(3.14)
|
|
71
|
+
expect(instance.flag).to be true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "handles nested models" do
|
|
75
|
+
address = Class.new do
|
|
76
|
+
include Lutaml::Model::Serialize
|
|
77
|
+
|
|
78
|
+
attribute :city, :string
|
|
79
|
+
attribute :zip, :string
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
person = Class.new do
|
|
83
|
+
include Lutaml::Model::Serialize
|
|
84
|
+
|
|
85
|
+
attribute :name, :string
|
|
86
|
+
attribute :address, address
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
instance = person.new(name: "Alice",
|
|
90
|
+
address: address.new(
|
|
91
|
+
city: "NYC", zip: "10001",
|
|
92
|
+
))
|
|
93
|
+
expect(instance.address.city).to eq("NYC")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "handles YAML serialization" do
|
|
97
|
+
config = Class.new do
|
|
98
|
+
include Lutaml::Model::Serialize
|
|
99
|
+
|
|
100
|
+
attribute :host, :string
|
|
101
|
+
attribute :port, :integer
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
instance = config.from_yaml("host: localhost\nport: 8080\n")
|
|
105
|
+
expect(instance.host).to eq("localhost")
|
|
106
|
+
expect(instance.port).to eq(8080)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "RuntimeCompatibility detects Opal" do
|
|
110
|
+
expect(Lutaml::Model::RuntimeCompatibility.opal?).to be true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "AdapterResolver auto-detects REXML" do
|
|
114
|
+
adapter = Lutaml::Model::AdapterResolver.detect_xml_adapter
|
|
115
|
+
expect(adapter).to eq(:rexml)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -89,8 +89,8 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
it "registering class B does not iterate class A's indices" do
|
|
92
|
-
# Build index for model_class
|
|
93
|
-
model_class.new(id: "x")
|
|
92
|
+
# Build index for model_class (hold strong ref so GC cannot collect it)
|
|
93
|
+
_obj = model_class.new(id: "x")
|
|
94
94
|
described_class.resolve(model_class, :id, "x")
|
|
95
95
|
|
|
96
96
|
# Registering other_class should not trigger work on model_class indices
|
|
@@ -138,6 +138,160 @@ RSpec.describe Lutaml::Model::Store do
|
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
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
|
+
|
|
141
295
|
describe "#clear" do
|
|
142
296
|
it "removes all registered objects" do
|
|
143
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
|