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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +1 -0
  3. data/.github/workflows/opal.yml +31 -0
  4. data/.rspec-opal +5 -0
  5. data/.rubocop_todo.yml +68 -7
  6. data/README.adoc +53 -1
  7. data/docs/_guides/index.adoc +4 -0
  8. data/docs/_guides/jsonld-serialization.adoc +3 -1
  9. data/docs/_guides/opal.adoc +221 -0
  10. data/docs/_guides/rdf-serialization.adoc +94 -8
  11. data/docs/_guides/turtle-serialization.adoc +17 -4
  12. data/docs/_guides/xml_mappings/07_best_practices.adoc +2 -1
  13. data/docs/_pages/configuration.adoc +9 -4
  14. data/docs/_pages/index.adoc +1 -0
  15. data/docs/_pages/serialization_adapters.adoc +3 -2
  16. data/docs/index.adoc +1 -0
  17. data/lib/lutaml/hash_format/adapter/mapping.rb +2 -4
  18. data/lib/lutaml/json/adapter/mapping.rb +2 -4
  19. data/lib/lutaml/jsonl/adapter/mapping.rb +2 -4
  20. data/lib/lutaml/jsonld/transform.rb +70 -24
  21. data/lib/lutaml/key_value/adapter/hash/mapping.rb +2 -4
  22. data/lib/lutaml/key_value/adapter/json/mapping.rb +2 -4
  23. data/lib/lutaml/key_value/adapter/jsonl/mapping.rb +2 -4
  24. data/lib/lutaml/key_value/adapter/toml/mapping.rb +2 -4
  25. data/lib/lutaml/key_value/adapter/yaml/mapping.rb +2 -4
  26. data/lib/lutaml/key_value/adapter/yamls/mapping.rb +2 -4
  27. data/lib/lutaml/key_value/mapping.rb +4 -4
  28. data/lib/lutaml/model/adapter_resolver.rb +5 -8
  29. data/lib/lutaml/model/mapping/mapping.rb +12 -0
  30. data/lib/lutaml/model/store.rb +51 -4
  31. data/lib/lutaml/model/version.rb +1 -1
  32. data/lib/lutaml/rdf/mapping.rb +19 -13
  33. data/lib/lutaml/rdf/mapping_rule.rb +19 -2
  34. data/lib/lutaml/rdf/member_rule.rb +19 -2
  35. data/lib/lutaml/rdf/transform.rb +20 -11
  36. data/lib/lutaml/toml/adapter/mapping.rb +2 -4
  37. data/lib/lutaml/turtle/transform.rb +125 -53
  38. data/lib/lutaml/xml/schema/xsd.rb +5 -4
  39. data/lib/lutaml/xml/schema.rb +8 -5
  40. data/lib/lutaml/xml/xml_orderable.rb +17 -0
  41. data/lib/lutaml/xml.rb +8 -11
  42. data/lib/lutaml/yaml/adapter/mapping.rb +2 -4
  43. data/lib/lutaml/yamls/adapter/mapping.rb +7 -3
  44. data/lutaml-model.gemspec +1 -1
  45. data/spec/lutaml/jsonld/transform_spec.rb +239 -0
  46. data/spec/lutaml/model/opal_smoke_spec.rb +117 -0
  47. data/spec/lutaml/model/store_spec.rb +156 -2
  48. data/spec/lutaml/rdf/mapping_rule_spec.rb +97 -0
  49. data/spec/lutaml/rdf/mapping_spec.rb +74 -4
  50. data/spec/lutaml/rdf/member_rule_spec.rb +41 -0
  51. data/spec/lutaml/rdf/rdf_transform_spec.rb +95 -29
  52. data/spec/lutaml/turtle/mapping_spec.rb +2 -2
  53. data/spec/lutaml/turtle/transform_spec.rb +315 -0
  54. data/spec/lutaml/xml/opal_xml_spec.rb +145 -0
  55. data/spec/lutaml/xml/xml_spec.rb +64 -13
  56. data/spec/support/opal.rb +6 -0
  57. 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