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.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +1 -0
  3. data/.rubocop_todo.yml +52 -22
  4. data/README.adoc +43 -0
  5. data/docs/_guides/jsonld-serialization.adoc +3 -1
  6. data/docs/_guides/rdf-serialization.adoc +94 -8
  7. data/docs/_guides/turtle-serialization.adoc +17 -4
  8. data/lib/lutaml/jsonld/transform.rb +70 -24
  9. data/lib/lutaml/key_value/transform.rb +5 -5
  10. data/lib/lutaml/key_value/transformation/collection_serializer.rb +25 -11
  11. data/lib/lutaml/key_value/transformation/value_serializer.rb +7 -7
  12. data/lib/lutaml/key_value/transformation.rb +27 -17
  13. data/lib/lutaml/model/adapter_resolver.rb +4 -6
  14. data/lib/lutaml/model/attribute.rb +26 -23
  15. data/lib/lutaml/model/cached_type_resolver.rb +10 -9
  16. data/lib/lutaml/model/cli.rb +1 -1
  17. data/lib/lutaml/model/collection.rb +4 -4
  18. data/lib/lutaml/model/comparable_model.rb +11 -11
  19. data/lib/lutaml/model/config.rb +1 -1
  20. data/lib/lutaml/model/consolidation/dispatcher.rb +1 -1
  21. data/lib/lutaml/model/consolidation/pattern_chunker.rb +3 -3
  22. data/lib/lutaml/model/format_registry.rb +6 -4
  23. data/lib/lutaml/model/global_context.rb +2 -2
  24. data/lib/lutaml/model/global_register.rb +1 -1
  25. data/lib/lutaml/model/instrumentation.rb +1 -1
  26. data/lib/lutaml/model/mapping/mapping_rule.rb +3 -3
  27. data/lib/lutaml/model/mapping/model_mapping.rb +1 -1
  28. data/lib/lutaml/model/mapping/model_mapping_rule.rb +1 -1
  29. data/lib/lutaml/model/register.rb +3 -3
  30. data/lib/lutaml/model/render_policy.rb +11 -17
  31. data/lib/lutaml/model/runtime_compatibility.rb +0 -1
  32. data/lib/lutaml/model/schema/xml_compiler/group.rb +1 -1
  33. data/lib/lutaml/model/schema/xml_compiler/registry_generator.rb +1 -1
  34. data/lib/lutaml/model/schema/xml_compiler/sequence.rb +0 -2
  35. data/lib/lutaml/model/schema/xml_compiler.rb +14 -14
  36. data/lib/lutaml/model/serialize/attribute_definition.rb +1 -1
  37. data/lib/lutaml/model/serialize/deserialization_context.rb +50 -0
  38. data/lib/lutaml/model/serialize/format_conversion.rb +2 -2
  39. data/lib/lutaml/model/serialize/initialization.rb +44 -7
  40. data/lib/lutaml/model/serialize/model_import.rb +1 -1
  41. data/lib/lutaml/model/serialize.rb +8 -1
  42. data/lib/lutaml/model/services/rule_value_extractor.rb +2 -1
  43. data/lib/lutaml/model/store.rb +77 -24
  44. data/lib/lutaml/model/transformation_registry.rb +1 -1
  45. data/lib/lutaml/model/type_context.rb +7 -1
  46. data/lib/lutaml/model/type_resolver.rb +1 -6
  47. data/lib/lutaml/model/utils.rb +19 -6
  48. data/lib/lutaml/model/validation_framework.rb +1 -1
  49. data/lib/lutaml/model/value_transformer.rb +2 -2
  50. data/lib/lutaml/model/version.rb +1 -1
  51. data/lib/lutaml/rdf/mapping.rb +19 -13
  52. data/lib/lutaml/rdf/mapping_rule.rb +19 -2
  53. data/lib/lutaml/rdf/member_rule.rb +19 -2
  54. data/lib/lutaml/rdf/transform.rb +20 -11
  55. data/lib/lutaml/turtle/transform.rb +125 -53
  56. data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -1
  57. data/lib/lutaml/xml/adapter/base_adapter.rb +10 -14
  58. data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +3 -3
  59. data/lib/lutaml/xml/adapter/plan_based_builder.rb +14 -14
  60. data/lib/lutaml/xml/adapter/xml_serializer.rb +3 -3
  61. data/lib/lutaml/xml/configurable.rb +2 -1
  62. data/lib/lutaml/xml/data_model.rb +2 -2
  63. data/lib/lutaml/xml/decisions/decision_context.rb +3 -3
  64. data/lib/lutaml/xml/decisions/rules/element_form_default_unqualified_rule.rb +1 -1
  65. data/lib/lutaml/xml/decisions/rules/element_form_option_rule.rb +1 -1
  66. data/lib/lutaml/xml/decisions/rules/used_prefix_rule.rb +1 -1
  67. data/lib/lutaml/xml/declaration_plan.rb +2 -2
  68. data/lib/lutaml/xml/declaration_planner.rb +12 -13
  69. data/lib/lutaml/xml/document.rb +13 -13
  70. data/lib/lutaml/xml/format_chooser.rb +3 -3
  71. data/lib/lutaml/xml/hoisting_algorithm.rb +1 -1
  72. data/lib/lutaml/xml/mapping.rb +2 -2
  73. data/lib/lutaml/xml/mapping_rule.rb +16 -3
  74. data/lib/lutaml/xml/model_transform.rb +17 -19
  75. data/lib/lutaml/xml/namespace_collector.rb +10 -10
  76. data/lib/lutaml/xml/namespace_declaration.rb +2 -2
  77. data/lib/lutaml/xml/namespace_declaration_data.rb +5 -8
  78. data/lib/lutaml/xml/namespace_scope_config.rb +3 -2
  79. data/lib/lutaml/xml/namespace_type_resolver.rb +4 -4
  80. data/lib/lutaml/xml/nokogiri/element.rb +2 -2
  81. data/lib/lutaml/xml/polymorphic_value_handler.rb +1 -1
  82. data/lib/lutaml/xml/schema/xsd/base.rb +7 -7
  83. data/lib/lutaml/xml/schema/xsd/choice.rb +2 -2
  84. data/lib/lutaml/xml/schema/xsd/complex_type.rb +5 -5
  85. data/lib/lutaml/xml/schema/xsd/errors/message_builder.rb +3 -3
  86. data/lib/lutaml/xml/schema/xsd/group.rb +2 -2
  87. data/lib/lutaml/xml/schema/xsd/sequence.rb +2 -2
  88. data/lib/lutaml/xml/schema/xsd_schema.rb +5 -5
  89. data/lib/lutaml/xml/serialization/format_conversion.rb +4 -3
  90. data/lib/lutaml/xml/transformation/element_builder.rb +4 -2
  91. data/lib/lutaml/xml/transformation/rule_applier.rb +2 -2
  92. data/lib/lutaml/xml/transformation/value_serializer.rb +4 -6
  93. data/lib/lutaml/xml/transformation.rb +4 -4
  94. data/lib/lutaml/xml/type/configurable.rb +0 -4
  95. data/lib/lutaml/xml/xml_element.rb +21 -13
  96. data/lutaml-model.gemspec +1 -1
  97. data/spec/lutaml/jsonld/transform_spec.rb +239 -0
  98. data/spec/lutaml/model/cached_type_resolver_spec.rb +3 -3
  99. data/spec/lutaml/model/optimization_spec.rb +228 -0
  100. data/spec/lutaml/model/store_spec.rb +195 -0
  101. data/spec/lutaml/rdf/mapping_rule_spec.rb +97 -0
  102. data/spec/lutaml/rdf/mapping_spec.rb +74 -4
  103. data/spec/lutaml/rdf/member_rule_spec.rb +41 -0
  104. data/spec/lutaml/rdf/rdf_transform_spec.rb +95 -29
  105. data/spec/lutaml/turtle/mapping_spec.rb +2 -2
  106. data/spec/lutaml/turtle/transform_spec.rb +315 -0
  107. data/spec/lutaml/xml/data_model_spec.rb +10 -28
  108. metadata +7 -4
@@ -4,79 +4,145 @@ require "spec_helper"
4
4
  require "lutaml/rdf"
5
5
 
6
6
  RSpec.describe Lutaml::Rdf::Transform do
7
- let(:transform) { described_class.new(nil) }
8
-
9
7
  describe "#resolve_subject_uri" do
10
8
  it "returns nil when mapping has no subject" do
11
9
  mapping = Lutaml::Rdf::Mapping.new
12
- expect(transform.send(:resolve_subject_uri, mapping, double)).to be_nil
10
+ transform = described_class.new(nil)
11
+ expect(transform.resolve_subject_uri(mapping, double)).to be_nil
13
12
  end
14
13
 
15
14
  it "calls subject proc with instance" do
16
15
  mapping = Lutaml::Rdf::Mapping.new
17
16
  mapping.subject { |i| "http://example.org/#{i}" }
18
17
 
19
- result = transform.send(:resolve_subject_uri, mapping, "test")
18
+ transform = described_class.new(nil)
19
+ result = transform.resolve_subject_uri(mapping, "test")
20
20
  expect(result).to eq("http://example.org/test")
21
21
  end
22
22
  end
23
23
 
24
- describe "#resolve_type_uri" do
25
- it "returns nil when mapping has no type" do
26
- mapping = Lutaml::Rdf::Mapping.new
27
- expect(transform.send(:resolve_type_uri, mapping)).to be_nil
28
- end
29
-
24
+ describe "#resolve_single_type_uri" do
30
25
  it "resolves compact IRI to full URI" do
31
- mapping = Lutaml::Rdf::Mapping.new
32
26
  stub_const("TestNs", Class.new(Lutaml::Rdf::Namespace) do
33
27
  uri "http://example.org/"
34
28
  prefix "ex"
35
29
  end)
30
+ mapping = Lutaml::Rdf::Mapping.new
36
31
  mapping.namespace(TestNs)
37
32
  mapping.type "ex:Thing"
38
33
 
39
- result = transform.send(:resolve_type_uri, mapping)
34
+ transform = described_class.new(nil)
35
+ result = transform.resolve_single_type_uri(mapping, "ex:Thing")
40
36
  expect(result).to eq("http://example.org/Thing")
41
37
  end
42
38
  end
43
39
 
44
- describe "#resolve_type_compact" do
45
- it "returns the compact form" do
40
+ describe "#resolve_type_uris" do
41
+ it "returns empty array when mapping has no types" do
42
+ mapping = Lutaml::Rdf::Mapping.new
43
+ transform = described_class.new(nil)
44
+ expect(transform.resolve_type_uris(mapping)).to eq([])
45
+ end
46
+
47
+ it "resolves all type compact IRIs to full URIs" do
48
+ stub_const("MultiNs", Class.new(Lutaml::Rdf::Namespace) do
49
+ uri "http://example.org/"
50
+ prefix "ex"
51
+ end)
46
52
  mapping = Lutaml::Rdf::Mapping.new
47
- mapping.type "skos:Concept"
53
+ mapping.namespace(MultiNs)
54
+ mapping.type ["ex:Thing", "ex:Other"]
48
55
 
49
- expect(transform.send(:resolve_type_compact,
50
- mapping)).to eq("skos:Concept")
56
+ transform = described_class.new(nil)
57
+ result = transform.resolve_type_uris(mapping)
58
+ expect(result).to eq(["http://example.org/Thing", "http://example.org/Other"])
51
59
  end
52
60
  end
53
61
 
54
62
  describe "#extract_language" do
55
63
  it "extracts language from LanguageTagged objects" do
56
64
  literal = Lutaml::Rdf::Literal.new("hello", language: "eng")
57
- expect(transform.send(:extract_language, literal)).to eq("eng")
65
+ transform = described_class.new(nil)
66
+ expect(transform.extract_language(literal)).to eq("eng")
58
67
  end
59
68
 
60
69
  it "returns nil for plain strings" do
61
- expect(transform.send(:extract_language, "hello")).to be_nil
70
+ transform = described_class.new(nil)
71
+ expect(transform.extract_language("hello")).to be_nil
62
72
  end
63
73
 
64
74
  it "returns nil for non-LanguageTagged objects" do
65
- expect(transform.send(:extract_language, 42)).to be_nil
75
+ transform = described_class.new(nil)
76
+ expect(transform.extract_language(42)).to be_nil
66
77
  end
67
78
  end
68
79
 
69
- describe "#build_instance" do
70
- it "constructs a model instance with resolved register" do
71
- stub_const("BuildTestModel", Class.new(Lutaml::Model::Serializable) do
72
- attribute :name, :string
80
+ describe "#each_member" do
81
+ it "iterates over collection attribute values" do
82
+ stub_const("MemberItem", Class.new do
83
+ attr_reader :label
84
+
85
+ def initialize(label)
86
+ @label = label
87
+ end
88
+ end)
89
+ stub_const("MemberParent", Class.new(Lutaml::Model::Serializable) do
90
+ attribute :items, :string, collection: true
91
+ end)
92
+
93
+ instance = MemberParent.new(items: ["a", "b"])
94
+ member_rule = Lutaml::Rdf::MemberRule.new(:items)
95
+ transform = described_class.new(nil)
96
+
97
+ collected = []
98
+ transform.each_member(instance, member_rule) { |m| collected << m }
99
+ expect(collected).to eq(["a", "b"])
100
+ end
101
+
102
+ it "handles nil collection as empty" do
103
+ stub_const("NilParent", Class.new(Lutaml::Model::Serializable) do
104
+ attribute :items, :string, collection: true
105
+ end)
106
+
107
+ instance = NilParent.new
108
+ member_rule = Lutaml::Rdf::MemberRule.new(:items)
109
+ transform = described_class.new(nil)
110
+
111
+ collected = []
112
+ transform.each_member(instance, member_rule) { |m| collected << m }
113
+ expect(collected).to eq([])
114
+ end
115
+ end
116
+
117
+ describe "#member_mapping_for" do
118
+ it "returns the mapping for the given format" do
119
+ stub_const("MappedChild", Class.new(Lutaml::Model::Serializable) do
120
+ attribute :label, :string
121
+
122
+ turtle do
123
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
124
+ subject { |m| "http://example.org/#{m.label}" } # rubocop:disable RSpec/NamedSubject
125
+
126
+ predicate :prefLabel,
127
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
128
+ to: :label
129
+ end
130
+ end)
131
+
132
+ member = MappedChild.new(label: "test")
133
+ transform = described_class.new(nil)
134
+ mapping = transform.member_mapping_for(member, :turtle)
135
+ expect(mapping).to be_a(Lutaml::Rdf::Mapping)
136
+ end
137
+
138
+ it "returns nil when no mapping for format" do
139
+ stub_const("UnmappedChild", Class.new(Lutaml::Model::Serializable) do
140
+ attribute :label, :string
73
141
  end)
74
142
 
75
- context = BuildTestModel
76
- t = described_class.new(context)
77
- instance = t.send(:build_instance, { name: "test" }, {})
78
- expect(instance).to be_a(BuildTestModel)
79
- expect(instance.name).to eq("test")
143
+ member = UnmappedChild.new(label: "test")
144
+ transform = described_class.new(nil)
145
+ expect(transform.member_mapping_for(member, :turtle)).to be_nil
80
146
  end
81
147
  end
82
148
  end
@@ -27,7 +27,7 @@ RSpec.describe Lutaml::Turtle::Mapping do
27
27
  describe "#type" do
28
28
  it "sets RDF type" do
29
29
  mapping.type("skos:Concept")
30
- expect(mapping.rdf_type).to eq("skos:Concept")
30
+ expect(mapping.rdf_type).to eq(["skos:Concept"])
31
31
  end
32
32
  end
33
33
 
@@ -108,7 +108,7 @@ RSpec.describe Lutaml::Turtle::Mapping do
108
108
  duped = mapping.deep_dup
109
109
  expect(duped.namespace_set.size).to eq(2)
110
110
  expect(duped.rdf_subject).to be_a(Proc)
111
- expect(duped.rdf_type).to eq("skos:Concept")
111
+ expect(duped.rdf_type).to eq(["skos:Concept"])
112
112
  expect(duped.rdf_predicates.length).to eq(1)
113
113
  end
114
114
 
@@ -260,6 +260,13 @@ RSpec.describe Lutaml::Turtle::Transform do
260
260
  model = TurtleTestModel.from_turtle(turtle_input)
261
261
  expect(model.name).to eq("test concept")
262
262
  end
263
+
264
+ it "round-trips deserialized data back to Turtle" do
265
+ model = TurtleTestModel.from_turtle(turtle_input)
266
+ turtle_out = model.to_turtle
267
+ expect(turtle_out).to include("skos:Concept")
268
+ expect(turtle_out).to include("skos:notation \"2119\"")
269
+ end
263
270
  end
264
271
 
265
272
  describe "round-trip" do
@@ -270,4 +277,312 @@ RSpec.describe Lutaml::Turtle::Transform do
270
277
  expect(restored.description).to eq("A test description")
271
278
  end
272
279
  end
280
+
281
+ describe "multiple types" do
282
+ before do
283
+ stub_const("DualTypeModel", Class.new(Lutaml::Model::Serializable) do
284
+ attribute :name, :string
285
+
286
+ turtle do
287
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace,
288
+ Lutaml::Rdf::Namespaces::DctermsNamespace
289
+
290
+ subject { |m| "http://example.org/#{m.name}" }
291
+
292
+ type ["skos:Concept", "dcterms:Agent"]
293
+
294
+ predicate :prefLabel,
295
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
296
+ to: :name
297
+ end
298
+ end)
299
+ end
300
+
301
+ it "generates multiple rdf:type triples" do
302
+ instance = DualTypeModel.new(name: "test")
303
+ result = instance.to_turtle
304
+ expect(result).to include("a skos:Concept")
305
+ expect(result).to include("dcterms:Agent")
306
+ end
307
+
308
+ it "round-trips multiple types" do
309
+ instance = DualTypeModel.new(name: "test")
310
+ turtle = instance.to_turtle
311
+ restored = DualTypeModel.from_turtle(turtle)
312
+ expect(restored.name).to eq("test")
313
+ end
314
+ end
315
+
316
+ describe "empty type array" do
317
+ before do
318
+ stub_const("NoTypeModel", Class.new(Lutaml::Model::Serializable) do
319
+ attribute :name, :string
320
+
321
+ turtle do
322
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
323
+
324
+ subject { |m| "http://example.org/#{m.name}" }
325
+
326
+ predicate :prefLabel,
327
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
328
+ to: :name
329
+ end
330
+ end)
331
+ end
332
+
333
+ it "omits rdf:type when no types declared" do
334
+ instance = NoTypeModel.new(name: "test")
335
+ result = instance.to_turtle
336
+ expect(result).not_to include(" a ")
337
+ expect(result).to include("skos:prefLabel")
338
+ end
339
+ end
340
+
341
+ describe "URI reference predicates" do
342
+ before do
343
+ stub_const("UriRefModel", Class.new(Lutaml::Model::Serializable) do
344
+ attribute :name, :string
345
+ attribute :related, :string, collection: true
346
+
347
+ turtle do
348
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace,
349
+ Lutaml::Rdf::Namespaces::DctermsNamespace
350
+
351
+ subject { |m| "http://example.org/#{m.name}" }
352
+
353
+ type "skos:Concept"
354
+
355
+ predicate :prefLabel,
356
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
357
+ to: :name
358
+
359
+ predicate :related,
360
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
361
+ to: :related,
362
+ uri_reference: true
363
+ end
364
+ end)
365
+ end
366
+
367
+ it "emits URI objects instead of literals" do
368
+ instance = UriRefModel.new(name: "test", related: ["skos:other"])
369
+ result = instance.to_turtle
370
+ expect(result).to include("skos:related skos:other")
371
+ expect(result).not_to include('"skos:other"')
372
+ end
373
+
374
+ it "round-trips URI references preserving compact form" do
375
+ instance = UriRefModel.new(name: "test", related: ["skos:other"])
376
+ turtle = instance.to_turtle
377
+ restored = UriRefModel.from_turtle(turtle)
378
+ expect(restored.related).to eq("skos:other")
379
+ end
380
+
381
+ it "handles full URI values without prefix" do
382
+ instance = UriRefModel.new(name: "test",
383
+ related: ["http://example.org/foo"])
384
+ result = instance.to_turtle
385
+ expect(result).to include("skos:related <http://example.org/foo>")
386
+ end
387
+
388
+ it "round-trips full URI values as-is" do
389
+ instance = UriRefModel.new(name: "test",
390
+ related: ["http://example.org/foo"])
391
+ turtle = instance.to_turtle
392
+ restored = UriRefModel.from_turtle(turtle)
393
+ expect(restored.related).to eq("http://example.org/foo")
394
+ end
395
+ end
396
+
397
+ describe "member linking predicates" do
398
+ before do
399
+ stub_const("ChildModel", Class.new(Lutaml::Model::Serializable) do
400
+ attribute :label, :string
401
+ attribute :cid, :string
402
+
403
+ turtle do
404
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
405
+
406
+ subject { |m| "http://example.org/child/#{m.cid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
407
+
408
+ type "skos:Concept"
409
+
410
+ predicate :prefLabel,
411
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
412
+ to: :label
413
+ end
414
+ end)
415
+
416
+ stub_const("ParentModel", Class.new(Lutaml::Model::Serializable) do
417
+ attribute :name, :string
418
+ attribute :children, ChildModel, collection: true
419
+
420
+ turtle do
421
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
422
+
423
+ subject { |m| "http://example.org/parent/#{m.name}" }
424
+
425
+ type "skos:Collection"
426
+
427
+ predicate :prefLabel,
428
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
429
+ to: :name
430
+
431
+ members :children,
432
+ predicate_name: :member,
433
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace
434
+ end
435
+ end)
436
+ end
437
+
438
+ it "generates linking triples from parent to members" do
439
+ parent = ParentModel.new(
440
+ name: "parent1",
441
+ children: [
442
+ ChildModel.new(label: "child1", cid: "c1"),
443
+ ChildModel.new(label: "child2", cid: "c2"),
444
+ ],
445
+ )
446
+ result = parent.to_turtle
447
+ expect(result).to include("skos:member <http://example.org/child/c1>")
448
+ expect(result).to include("<http://example.org/child/c2>")
449
+ end
450
+
451
+ it "still generates member graph nodes" do
452
+ parent = ParentModel.new(
453
+ name: "parent1",
454
+ children: [
455
+ ChildModel.new(label: "child1", cid: "c1"),
456
+ ],
457
+ )
458
+ result = parent.to_turtle
459
+ expect(result).to include("skos:prefLabel \"child1\"")
460
+ end
461
+ end
462
+
463
+ describe "linked-only model (no type, no predicates)" do
464
+ before do
465
+ stub_const("LinkedOnlyChild", Class.new(Lutaml::Model::Serializable) do
466
+ attribute :cid, :string
467
+ attribute :label, :string
468
+
469
+ turtle do
470
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
471
+
472
+ subject { |m| "http://example.org/item/#{m.cid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
473
+
474
+ type "skos:Concept"
475
+
476
+ predicate :prefLabel,
477
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
478
+ to: :label
479
+ end
480
+ end)
481
+
482
+ stub_const("LinkedOnlyParent", Class.new(Lutaml::Model::Serializable) do
483
+ attribute :pid, :string
484
+ attribute :items, LinkedOnlyChild, collection: true
485
+
486
+ turtle do
487
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
488
+
489
+ subject { |m| "http://example.org/group/#{m.pid}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
490
+
491
+ members :items,
492
+ predicate_name: :member,
493
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace
494
+ end
495
+ end)
496
+ end
497
+
498
+ it "generates linking triples even without type or predicates" do
499
+ parent = LinkedOnlyParent.new(
500
+ pid: "g1",
501
+ items: [
502
+ LinkedOnlyChild.new(cid: "a", label: "Alpha"),
503
+ LinkedOnlyChild.new(cid: "b", label: "Beta"),
504
+ ],
505
+ )
506
+ result = parent.to_turtle
507
+ expect(result).to include("skos:member")
508
+ expect(result).to include("<http://example.org/item/a>")
509
+ expect(result).to include("<http://example.org/item/b>")
510
+ end
511
+
512
+ it "includes member subgraph data" do
513
+ parent = LinkedOnlyParent.new(
514
+ pid: "g1",
515
+ items: [LinkedOnlyChild.new(cid: "a", label: "Alpha")],
516
+ )
517
+ result = parent.to_turtle
518
+ expect(result).to include("skos:prefLabel \"Alpha\"")
519
+ end
520
+
521
+ it "does not generate rdf:type for parent" do
522
+ parent = LinkedOnlyParent.new(
523
+ pid: "g1",
524
+ items: [LinkedOnlyChild.new(cid: "a", label: "Alpha")],
525
+ )
526
+ result = parent.to_turtle
527
+ parent_line = result.lines.find { |l| l.include?("<http://example.org/group/g1>") }
528
+ expect(parent_line).not_to include(" a ")
529
+ end
530
+ end
531
+
532
+ describe "heterogeneous member collection" do
533
+ before do
534
+ stub_const("HeteroChildA", Class.new(Lutaml::Model::Serializable) do
535
+ attribute :label, :string
536
+
537
+ turtle do
538
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
539
+
540
+ subject { |m| "http://example.org/a/#{m.label}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
541
+
542
+ type "skos:Concept"
543
+
544
+ predicate :prefLabel,
545
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
546
+ to: :label
547
+ end
548
+ end)
549
+
550
+ stub_const("HeteroChildB", Class.new(Lutaml::Model::Serializable) do
551
+ attribute :title, :string
552
+
553
+ turtle do
554
+ namespace Lutaml::Rdf::Namespaces::DctermsNamespace
555
+
556
+ subject { |m| "http://example.org/b/#{m.title}" } # rubocop:disable RSpec/NamedSubject, RSpec/MultipleSubjects
557
+
558
+ type "dcterms:Agent"
559
+
560
+ predicate :title,
561
+ namespace: Lutaml::Rdf::Namespaces::DctermsNamespace,
562
+ to: :title
563
+ end
564
+ end)
565
+
566
+ stub_const("HeteroParent", Class.new(Lutaml::Model::Serializable) do
567
+ attribute :name, :string
568
+ attribute :items, :string, collection: true
569
+
570
+ turtle do
571
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
572
+
573
+ subject { |m| "http://example.org/parent/#{m.name}" }
574
+
575
+ type "skos:Collection"
576
+
577
+ predicate :prefLabel,
578
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
579
+ to: :name
580
+ end
581
+ end)
582
+ end
583
+
584
+ it "includes prefixes from all member types" do
585
+ skip "Heterogeneous collection requires union-typed attribute (not yet supported)"
586
+ end
587
+ end
273
588
  end
@@ -6,15 +6,10 @@ RSpec.describe Lutaml::Xml::DataModel do
6
6
  describe Lutaml::Xml::DataModel::XmlElement do
7
7
  let(:element_name) { "test-element" }
8
8
  let(:namespace_class) do
9
- Class.new do
10
- def self.prefix_default
11
- "test"
12
- end
13
-
14
- def self.uri
15
- "http://example.com/test"
16
- end
17
- end
9
+ ns = Class.new(Lutaml::Xml::Namespace)
10
+ ns.prefix_default "test"
11
+ ns.uri "http://example.com/test"
12
+ ns
18
13
  end
19
14
 
20
15
  describe "#initialize" do
@@ -146,11 +141,7 @@ RSpec.describe Lutaml::Xml::DataModel do
146
141
  end
147
142
 
148
143
  it "returns unprefixed when namespace has no prefix" do
149
- ns_no_prefix = Class.new do
150
- def self.prefix_default
151
- nil
152
- end
153
- end
144
+ ns_no_prefix = Class.new(Lutaml::Xml::Namespace)
154
145
  element = described_class.new("element", ns_no_prefix)
155
146
 
156
147
  expect(element.qualified_name).to eq("element")
@@ -208,15 +199,10 @@ RSpec.describe Lutaml::Xml::DataModel do
208
199
  let(:attr_name) { "test-attr" }
209
200
  let(:attr_value) { "test-value" }
210
201
  let(:namespace_class) do
211
- Class.new do
212
- def self.prefix_default
213
- "test"
214
- end
215
-
216
- def self.uri
217
- "http://example.com/test"
218
- end
219
- end
202
+ ns = Class.new(Lutaml::Xml::Namespace)
203
+ ns.prefix_default "test"
204
+ ns.uri "http://example.com/test"
205
+ ns
220
206
  end
221
207
 
222
208
  describe "#initialize" do
@@ -257,11 +243,7 @@ RSpec.describe Lutaml::Xml::DataModel do
257
243
  end
258
244
 
259
245
  it "returns unprefixed when namespace has no prefix" do
260
- ns_no_prefix = Class.new do
261
- def self.prefix_default
262
- nil
263
- end
264
- end
246
+ ns_no_prefix = Class.new(Lutaml::Xml::Namespace)
265
247
  attribute = described_class.new(attr_name, attr_value, ns_no_prefix)
266
248
 
267
249
  expect(attribute.qualified_name).to eq(attr_name)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.9
4
+ version: 0.8.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-14 00:00:00.000000000 Z
11
+ date: 2026-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -92,14 +92,14 @@ dependencies:
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: 0.1.16
95
+ version: 0.1.20
96
96
  type: :runtime
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: 0.1.16
102
+ version: 0.1.20
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: ostruct
105
105
  requirement: !ruby/object:Gem::Requirement
@@ -532,6 +532,7 @@ files:
532
532
  - lib/lutaml/model/serialize.rb
533
533
  - lib/lutaml/model/serialize/attribute_definition.rb
534
534
  - lib/lutaml/model/serialize/builder.rb
535
+ - lib/lutaml/model/serialize/deserialization_context.rb
535
536
  - lib/lutaml/model/serialize/enum_handling.rb
536
537
  - lib/lutaml/model/serialize/format_conversion.rb
537
538
  - lib/lutaml/model/serialize/initialization.rb
@@ -1614,6 +1615,7 @@ files:
1614
1615
  - spec/lutaml/model/multiple_mapping_spec.rb
1615
1616
  - spec/lutaml/model/namespace_versioning_spec.rb
1616
1617
  - spec/lutaml/model/one_entry_cache_spec.rb
1618
+ - spec/lutaml/model/optimization_spec.rb
1617
1619
  - spec/lutaml/model/ordered_content_spec.rb
1618
1620
  - spec/lutaml/model/polymorphic_spec.rb
1619
1621
  - spec/lutaml/model/processing_instruction_spec.rb
@@ -1705,6 +1707,7 @@ files:
1705
1707
  - spec/lutaml/rdf/graph_serialization_spec.rb
1706
1708
  - spec/lutaml/rdf/iri_spec.rb
1707
1709
  - spec/lutaml/rdf/literal_spec.rb
1710
+ - spec/lutaml/rdf/mapping_rule_spec.rb
1708
1711
  - spec/lutaml/rdf/mapping_spec.rb
1709
1712
  - spec/lutaml/rdf/member_rule_spec.rb
1710
1713
  - spec/lutaml/rdf/namespace_set_spec.rb