lutaml-model 0.8.10 → 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.
@@ -10,7 +10,7 @@ module Lutaml
10
10
  super
11
11
  @namespace_set = Lutaml::Rdf::NamespaceSet.new
12
12
  @rdf_subject = nil
13
- @rdf_type = nil
13
+ @rdf_type = []
14
14
  @rdf_predicates = []
15
15
  @rdf_members = []
16
16
  end
@@ -24,20 +24,26 @@ module Lutaml
24
24
  end
25
25
 
26
26
  def type(value)
27
- @rdf_type = value
27
+ @rdf_type = Array(value)
28
28
  end
29
29
 
30
- def predicate(name, namespace:, to:, lang_tagged: false)
30
+ def predicate(name, namespace:, to:, lang_tagged: false,
31
+ uri_reference: false)
31
32
  @rdf_predicates << Lutaml::Rdf::MappingRule.new(
32
33
  name,
33
34
  namespace: namespace,
34
35
  to: to,
35
36
  lang_tagged: lang_tagged,
37
+ uri_reference: uri_reference,
36
38
  )
37
39
  end
38
40
 
39
- def members(attr_name)
40
- @rdf_members << Lutaml::Rdf::MemberRule.new(attr_name)
41
+ def members(attr_name, predicate_name: nil, namespace: nil)
42
+ @rdf_members << Lutaml::Rdf::MemberRule.new(
43
+ attr_name,
44
+ predicate_name: predicate_name,
45
+ namespace: namespace,
46
+ )
41
47
  end
42
48
 
43
49
  def mappings(_register_id = nil)
@@ -57,14 +63,14 @@ module Lutaml
57
63
  end
58
64
 
59
65
  def deep_dup
60
- self.class.new.tap do |new_mapping|
61
- new_mapping.instance_variable_set(:@namespace_set, @namespace_set)
62
- new_mapping.instance_variable_set(:@rdf_subject, @rdf_subject)
63
- new_mapping.instance_variable_set(:@rdf_type, @rdf_type)
64
- new_mapping.instance_variable_set(:@rdf_predicates,
65
- @rdf_predicates.dup)
66
- new_mapping.instance_variable_set(:@rdf_members, @rdf_members.dup)
67
- end
66
+ dup
67
+ end
68
+
69
+ def initialize_copy(source)
70
+ super
71
+ @rdf_type = source.rdf_type.dup
72
+ @rdf_predicates = source.rdf_predicates.dup
73
+ @rdf_members = source.rdf_members.dup
68
74
  end
69
75
  end
70
76
  end
@@ -3,14 +3,31 @@
3
3
  module Lutaml
4
4
  module Rdf
5
5
  class MappingRule
6
- attr_reader :predicate_name, :namespace, :to, :lang_tagged
6
+ attr_reader :predicate_name, :namespace, :to, :lang_tagged, :uri_reference
7
7
 
8
- def initialize(predicate_name, namespace:, to:, lang_tagged: false)
8
+ def initialize(predicate_name, namespace:, to:, lang_tagged: false,
9
+ uri_reference: false)
9
10
  validate!(predicate_name, namespace, to)
11
+ if lang_tagged && uri_reference
12
+ raise ArgumentError,
13
+ "lang_tagged and uri_reference are mutually exclusive"
14
+ end
15
+
10
16
  @predicate_name = predicate_name.to_s.freeze
11
17
  @namespace = namespace
12
18
  @to = to
13
19
  @lang_tagged = lang_tagged
20
+ @uri_reference = uri_reference
21
+ end
22
+
23
+ def kind
24
+ if uri_reference
25
+ :uri_reference
26
+ elsif lang_tagged
27
+ :lang_tagged
28
+ else
29
+ :plain
30
+ end
14
31
  end
15
32
 
16
33
  def uri
@@ -3,10 +3,27 @@
3
3
  module Lutaml
4
4
  module Rdf
5
5
  class MemberRule
6
- attr_reader :attr_name
6
+ attr_reader :attr_name, :predicate_name, :namespace
7
+
8
+ def initialize(attr_name, predicate_name: nil, namespace: nil)
9
+ if predicate_name && !namespace
10
+ raise ArgumentError,
11
+ "namespace is required when predicate_name is provided"
12
+ end
7
13
 
8
- def initialize(attr_name)
9
14
  @attr_name = attr_name.to_sym
15
+ @predicate_name = predicate_name
16
+ @namespace = namespace
17
+ end
18
+
19
+ def linked?
20
+ !!@predicate_name
21
+ end
22
+
23
+ def linked_predicate_uri
24
+ return nil unless linked?
25
+
26
+ @namespace[@predicate_name]
10
27
  end
11
28
  end
12
29
  end
@@ -3,22 +3,35 @@
3
3
  module Lutaml
4
4
  module Rdf
5
5
  class Transform < Lutaml::Model::Transform
6
- protected
7
-
8
6
  def resolve_subject_uri(mapping, instance)
9
7
  mapping.rdf_subject&.call(instance)
10
8
  end
11
9
 
12
- def resolve_type_uri(mapping)
13
- return unless mapping.rdf_type
10
+ def resolve_single_type_uri(mapping, type_value)
11
+ mapping.namespace_set.resolve_compact_iri(type_value)
12
+ end
13
+
14
+ def resolve_type_uris(mapping)
15
+ return [] unless mapping.rdf_type.any?
16
+
17
+ mapping.rdf_type.map { |t| resolve_single_type_uri(mapping, t) }
18
+ end
19
+
20
+ def each_member(instance, member_rule, &)
21
+ collection = Array(instance.public_send(member_rule.attr_name))
22
+ collection.each(&)
23
+ end
14
24
 
15
- mapping.namespace_set.resolve_compact_iri(mapping.rdf_type)
25
+ def member_mapping_for(member, format)
26
+ member.class.mappings[format]
16
27
  end
17
28
 
18
- def resolve_type_compact(mapping)
19
- mapping.rdf_type
29
+ def extract_language(value)
30
+ value.language_tag if value.is_a?(Lutaml::Rdf::LanguageTagged)
20
31
  end
21
32
 
33
+ protected
34
+
22
35
  def build_instance(attrs, options)
23
36
  child_register = Lutaml::Model::Register.resolve_for_child(
24
37
  model_class, lutaml_register
@@ -27,10 +40,6 @@ module Lutaml
27
40
  root_and_parent_assignment(instance, options)
28
41
  instance
29
42
  end
30
-
31
- def extract_language(value)
32
- value.language_tag if value.is_a?(Lutaml::Rdf::LanguageTagged)
33
- end
34
43
  end
35
44
  end
36
45
  end
@@ -46,56 +46,101 @@ module Lutaml
46
46
  def build_graph(mapping, instance)
47
47
  graph = RDF::Graph.new
48
48
 
49
- has_predicates_or_type = mapping.rdf_type || mapping.rdf_predicates.any?
50
-
51
- if has_predicates_or_type
52
- subject_uri = if mapping.rdf_subject
53
- RDF::URI(resolve_subject_uri(mapping, instance))
54
- else
55
- RDF::Node.new
56
- end
57
-
58
- if mapping.rdf_type
59
- type_uri = RDF::URI(resolve_type_uri(mapping))
60
- graph << RDF::Statement.new(subject_uri, RDF.type, type_uri)
49
+ has_resource_data =
50
+ mapping.rdf_type.any? ||
51
+ mapping.rdf_predicates.any? ||
52
+ mapping.rdf_members.any?(&:linked?)
53
+
54
+ if has_resource_data
55
+ subject_uri = resolve_subject(mapping, instance)
56
+ build_resource_triples(graph, mapping, instance, subject_uri)
57
+ end
58
+
59
+ build_member_subgraphs(graph, mapping, instance)
60
+
61
+ graph
62
+ end
63
+
64
+ def resolve_subject(mapping, instance)
65
+ if mapping.rdf_subject
66
+ RDF::URI(resolve_subject_uri(mapping, instance))
67
+ else
68
+ RDF::Node.new
69
+ end
70
+ end
71
+
72
+ def build_resource_triples(graph, mapping, instance, subject_uri)
73
+ mapping.rdf_type.each do |type_value|
74
+ type_uri = RDF::URI(resolve_single_type_uri(mapping, type_value))
75
+ graph << RDF::Statement.new(subject_uri, RDF.type, type_uri)
76
+ end
77
+
78
+ mapping.rdf_predicates.each do |rule|
79
+ value = instance.public_send(rule.to)
80
+ next if value.nil?
81
+
82
+ Array(value).each do |v|
83
+ object = build_rdf_object(v, rule, mapping.namespace_set)
84
+ graph << RDF::Statement.new(subject_uri, RDF::URI(rule.uri), object)
61
85
  end
86
+ end
87
+
88
+ mapping.rdf_members.each do |member_rule|
89
+ next unless member_rule.linked?
62
90
 
63
- mapping.rdf_predicates.each do |rule|
64
- value = instance.public_send(rule.to)
65
- next if value.nil?
91
+ each_member(instance, member_rule) do |member|
92
+ member_mapping = member_mapping_for(member, :turtle)
93
+ next unless member_mapping
66
94
 
67
- Array(value).each do |v|
68
- object = build_rdf_object(v, rule)
69
- graph << RDF::Statement.new(subject_uri, RDF::URI(rule.uri),
70
- object)
71
- end
95
+ member_subject = RDF::URI(resolve_subject_uri(member_mapping,
96
+ member))
97
+ graph << RDF::Statement.new(
98
+ subject_uri,
99
+ RDF::URI(member_rule.linked_predicate_uri),
100
+ member_subject,
101
+ )
72
102
  end
73
103
  end
104
+ end
74
105
 
106
+ def build_member_subgraphs(graph, mapping, instance)
75
107
  mapping.rdf_members.each do |member_rule|
76
- collection = Array(instance.public_send(member_rule.attr_name))
77
- collection.each do |member|
78
- member_mapping = member.class.mappings[:turtle]
108
+ each_member(instance, member_rule) do |member|
109
+ member_mapping = member_mapping_for(member, :turtle)
79
110
  next unless member_mapping
80
111
 
81
112
  graph << build_graph(member_mapping, member)
82
113
  end
83
114
  end
84
-
85
- graph
86
115
  end
87
116
 
88
- def build_rdf_object(value, rule)
89
- if rule.lang_tagged
117
+ def build_rdf_object(value, rule, namespace_set)
118
+ case rule.kind
119
+ when :uri_reference
120
+ build_uri_reference_object(value, namespace_set)
121
+ when :lang_tagged
90
122
  lang = extract_language(value)
91
123
  RDF::Literal.new(value.to_s, language: lang)
92
124
  else
93
- case value
94
- when Integer then RDF::Literal.new(value, datatype: RDF::XSD.integer)
95
- when Float then RDF::Literal.new(value, datatype: RDF::XSD.double)
96
- when TrueClass, FalseClass then RDF::Literal.new(value, datatype: RDF::XSD.boolean)
97
- else RDF::Literal.new(value.to_s)
98
- end
125
+ build_plain_literal(value)
126
+ end
127
+ end
128
+
129
+ def build_uri_reference_object(value, namespace_set)
130
+ resolved = if value.to_s.include?(":")
131
+ namespace_set.resolve_compact_iri(value.to_s)
132
+ else
133
+ value.to_s
134
+ end
135
+ RDF::URI.new(resolved)
136
+ end
137
+
138
+ def build_plain_literal(value)
139
+ case value
140
+ when Integer then RDF::Literal.new(value, datatype: RDF::XSD.integer)
141
+ when Float then RDF::Literal.new(value, datatype: RDF::XSD.double)
142
+ when TrueClass, FalseClass then RDF::Literal.new(value, datatype: RDF::XSD.boolean)
143
+ else RDF::Literal.new(value.to_s)
99
144
  end
100
145
  end
101
146
 
@@ -103,13 +148,12 @@ module Lutaml
103
148
  ns_set = mapping.namespace_set
104
149
 
105
150
  mapping.rdf_members.each do |member_rule|
106
- collection = Array(instance.public_send(member_rule.attr_name))
107
- next if collection.empty?
108
-
109
- member_mapping = collection.first.class.mappings[:turtle]
110
- next unless member_mapping
151
+ each_member(instance, member_rule) do |member|
152
+ member_mapping = member_mapping_for(member, :turtle)
153
+ next unless member_mapping
111
154
 
112
- ns_set = ns_set.merge(member_mapping.namespace_set)
155
+ ns_set = ns_set.merge(member_mapping.namespace_set)
156
+ end
113
157
  end
114
158
 
115
159
  ns_set.each.with_object({}) do |ns, h|
@@ -119,38 +163,66 @@ module Lutaml
119
163
 
120
164
  def extract_attributes(graph, mapping)
121
165
  attrs = {}
122
- type_uri = resolve_type_uri(mapping)
166
+ type_uris = resolve_type_uris(mapping)
123
167
 
124
- matching_subjects = find_subjects_by_type(graph, type_uri)
168
+ matching_subjects = find_subjects_by_types(graph, type_uris)
125
169
 
126
170
  matching_subjects.each do |subject|
127
- mapping.rdf_predicates.each do |rule|
128
- stmts = graph.query([subject, RDF::URI(rule.uri), nil])
129
- next if stmts.empty?
130
-
131
- values = stmts.map { |s| literal_to_ruby(s.object) }
132
- attrs[rule.to] = values.length == 1 ? values.first : values
133
- end
171
+ attrs["id"] = subject.to_s unless subject.node?
172
+ extract_predicate_attributes(graph, subject, mapping, attrs)
134
173
  end
135
174
 
136
175
  attrs
137
176
  end
138
177
 
139
- def find_subjects_by_type(graph, type_uri)
140
- graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
178
+ def extract_predicate_attributes(graph, subject, mapping, attrs)
179
+ mapping.rdf_predicates.each do |rule|
180
+ stmts = graph.query([subject, RDF::URI(rule.uri), nil])
181
+ next if stmts.empty?
182
+
183
+ values = stmts.map do |s|
184
+ literal_to_ruby(s.object, rule, mapping.namespace_set)
185
+ end
186
+ attrs[rule.to] = values.length == 1 ? values.first : values
187
+ end
188
+ end
189
+
190
+ def find_subjects_by_types(graph, type_uris)
191
+ type_uris.flat_map do |type_uri|
192
+ graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
193
+ end.uniq
141
194
  end
142
195
 
143
- def literal_to_ruby(rdf_object)
196
+ def literal_to_ruby(rdf_object, rule, namespace_set)
144
197
  case rdf_object
198
+ when RDF::URI
199
+ uri_to_ruby(rdf_object, rule, namespace_set)
145
200
  when RDF::Literal
201
+ literal_value_to_ruby(rdf_object, rule)
202
+ else
203
+ rdf_object.to_s
204
+ end
205
+ end
206
+
207
+ def uri_to_ruby(rdf_object, rule, namespace_set)
208
+ uri_str = rdf_object.to_s
209
+ if rule.kind == :uri_reference
210
+ namespace_set.compact(uri_str) || uri_str
211
+ else
212
+ uri_str
213
+ end
214
+ end
215
+
216
+ def literal_value_to_ruby(rdf_object, rule)
217
+ if rule.kind == :lang_tagged && rdf_object.language
218
+ rdf_object.value
219
+ else
146
220
  case rdf_object.datatype
147
221
  when RDF::XSD.integer then rdf_object.value.to_i
148
222
  when RDF::XSD.double, RDF::XSD.decimal, RDF::XSD.float then rdf_object.value.to_f
149
223
  when RDF::XSD.boolean then rdf_object.value == "true"
150
224
  else rdf_object.value
151
225
  end
152
- else
153
- rdf_object.to_s
154
226
  end
155
227
  end
156
228
  end
@@ -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