lutaml-model 0.8.4 → 0.8.5

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +3 -1
  3. data/.rubocop.yml +18 -0
  4. data/.rubocop_todo.yml +12 -18
  5. data/Gemfile +2 -0
  6. data/README.adoc +114 -2
  7. data/docs/_guides/index.adoc +18 -0
  8. data/docs/_guides/jsonld-serialization.adoc +217 -0
  9. data/docs/_guides/rdf-serialization.adoc +344 -0
  10. data/docs/_guides/turtle-serialization.adoc +224 -0
  11. data/docs/_pages/serialization_adapters.adoc +31 -0
  12. data/docs/_references/index.adoc +1 -0
  13. data/docs/_references/rdf-namespaces.adoc +243 -0
  14. data/docs/index.adoc +3 -2
  15. data/lib/lutaml/jsonld/adapter.rb +23 -0
  16. data/lib/lutaml/jsonld/context.rb +69 -0
  17. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  18. data/lib/lutaml/jsonld/transform.rb +174 -0
  19. data/lib/lutaml/jsonld.rb +23 -0
  20. data/lib/lutaml/model/format_registry.rb +10 -1
  21. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  22. data/lib/lutaml/model/version.rb +1 -1
  23. data/lib/lutaml/model.rb +6 -0
  24. data/lib/lutaml/rdf/error.rb +7 -0
  25. data/lib/lutaml/rdf/iri.rb +44 -0
  26. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  27. data/lib/lutaml/rdf/literal.rb +62 -0
  28. data/lib/lutaml/rdf/mapping.rb +71 -0
  29. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  30. data/lib/lutaml/rdf/member_rule.rb +13 -0
  31. data/lib/lutaml/rdf/namespace.rb +58 -0
  32. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  33. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  34. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  35. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  36. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  37. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  38. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  39. data/lib/lutaml/rdf/namespaces.rb +14 -0
  40. data/lib/lutaml/rdf/transform.rb +36 -0
  41. data/lib/lutaml/rdf.rb +19 -0
  42. data/lib/lutaml/turtle/adapter.rb +35 -0
  43. data/lib/lutaml/turtle/mapping.rb +7 -0
  44. data/lib/lutaml/turtle/transform.rb +158 -0
  45. data/lib/lutaml/turtle.rb +22 -0
  46. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  47. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  48. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  49. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  50. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  51. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  52. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  53. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  54. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  55. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  56. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  57. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  58. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  59. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  60. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  61. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  62. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  63. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  64. metadata +50 -1
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+
6
+ RSpec.describe Lutaml::JsonLd::Context do
7
+ subject(:ctx) { described_class.new }
8
+
9
+ describe "empty context" do
10
+ it "serializes to empty hash" do
11
+ expect(ctx.to_hash).to eq({})
12
+ end
13
+ end
14
+
15
+ describe "#prefix" do
16
+ it "adds namespace prefix from Rdf::Namespace class" do
17
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
18
+ expect(ctx.to_hash).to include("skos" => "http://www.w3.org/2004/02/skos/core#")
19
+ end
20
+
21
+ it "adds multiple prefixes" do
22
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
23
+ ctx.prefix(Lutaml::Rdf::Namespaces::DctermsNamespace)
24
+ hash = ctx.to_hash
25
+ expect(hash).to include("skos" => "http://www.w3.org/2004/02/skos/core#")
26
+ expect(hash).to include("dcterms" => "http://purl.org/dc/terms/")
27
+ end
28
+ end
29
+
30
+ describe "#vocab" do
31
+ it "sets @vocab" do
32
+ ctx.vocab("http://example.org/ns/")
33
+ expect(ctx.to_hash).to include("@vocab" => "http://example.org/ns/")
34
+ end
35
+ end
36
+
37
+ describe "#language" do
38
+ it "sets @language" do
39
+ ctx.language("en")
40
+ expect(ctx.to_hash).to include("@language" => "en")
41
+ end
42
+ end
43
+
44
+ describe "#base" do
45
+ it "sets @base" do
46
+ ctx.base("http://example.org/")
47
+ expect(ctx.to_hash).to include("@base" => "http://example.org/")
48
+ end
49
+ end
50
+
51
+ describe "#term" do
52
+ it "adds simple term as name => id" do
53
+ ctx.term("name", id: "http://example.org/name")
54
+ expect(ctx.to_hash).to include("name" => "http://example.org/name")
55
+ end
56
+
57
+ it "adds term with type" do
58
+ ctx.term("date", id: "http://example.org/date", type: "xsd:date")
59
+ expect(ctx.to_hash).to include("date" => {
60
+ "@id" => "http://example.org/date", "@type" => "xsd:date"
61
+ })
62
+ end
63
+
64
+ it "adds term with container" do
65
+ ctx.term("labels", id: "http://example.org/labels", container: :language)
66
+ expect(ctx.to_hash).to include("labels" => {
67
+ "@id" => "http://example.org/labels", "@container" => "@language"
68
+ })
69
+ end
70
+
71
+ it "adds term with reverse" do
72
+ ctx.term("parent", id: "http://example.org/parent", reverse: true)
73
+ expect(ctx.to_hash).to include("parent" => {
74
+ "@id" => "http://example.org/parent", "@reverse" => true
75
+ })
76
+ end
77
+ end
78
+
79
+ describe "#to_hash" do
80
+ it "serializes complete context" do
81
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
82
+ ctx.vocab("http://example.org/ns/")
83
+ ctx.term("name", id: "http://example.org/name")
84
+ hash = ctx.to_hash
85
+ expect(hash["@vocab"]).to eq("http://example.org/ns/")
86
+ expect(hash["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
87
+ expect(hash["name"]).to eq("http://example.org/name")
88
+ end
89
+ end
90
+
91
+ describe "#resolve" do
92
+ before do
93
+ ctx.prefix(Lutaml::Rdf::Namespaces::SkosNamespace)
94
+ ctx.vocab("http://example.org/ns/")
95
+ ctx.term("status", id: "http://example.org/status")
96
+ end
97
+
98
+ it "resolves compact IRI via prefixes" do
99
+ expect(ctx.resolve("skos:prefLabel")).to eq("http://www.w3.org/2004/02/skos/core#prefLabel")
100
+ end
101
+
102
+ it "resolves via term definitions" do
103
+ expect(ctx.resolve("status")).to eq("http://example.org/status")
104
+ end
105
+
106
+ it "resolves unprefixed name via @vocab" do
107
+ expect(ctx.resolve("unknown")).to eq("http://example.org/ns/unknown")
108
+ end
109
+
110
+ it "returns nil for unknown prefix" do
111
+ expect(ctx.resolve("unknown:something")).to be_nil
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+
6
+ RSpec.describe Lutaml::JsonLd::TermDefinition do
7
+ it "simple term maps to name => id" do
8
+ td = described_class.new(name: "name", id: "http://example.org/name")
9
+ expect(td.to_context_hash).to eq("name" => "http://example.org/name")
10
+ end
11
+
12
+ it "term with type includes @type" do
13
+ td = described_class.new(name: "date", id: "http://example.org/date",
14
+ type: "xsd:date")
15
+ expect(td.to_context_hash).to eq("date" => {
16
+ "@id" => "http://example.org/date", "@type" => "xsd:date"
17
+ })
18
+ end
19
+
20
+ it "term with container includes @container" do
21
+ td = described_class.new(name: "labels", id: "http://example.org/labels",
22
+ container: :language)
23
+ expect(td.to_context_hash).to eq("labels" => {
24
+ "@id" => "http://example.org/labels", "@container" => "@language"
25
+ })
26
+ end
27
+
28
+ it "term with language includes @language" do
29
+ td = described_class.new(name: "title", id: "http://example.org/title",
30
+ language: "en")
31
+ expect(td.to_context_hash).to eq("title" => {
32
+ "@id" => "http://example.org/title", "@language" => "en"
33
+ })
34
+ end
35
+
36
+ it "term with reverse includes @reverse" do
37
+ td = described_class.new(name: "parent", id: "http://example.org/parent",
38
+ reverse: true)
39
+ expect(td.to_context_hash).to eq("parent" => {
40
+ "@id" => "http://example.org/parent", "@reverse" => true
41
+ })
42
+ end
43
+
44
+ it "complex term includes all fields" do
45
+ td = described_class.new(
46
+ name: "date",
47
+ id: "http://example.org/date",
48
+ type: "xsd:date",
49
+ container: :set,
50
+ )
51
+ hash = td.to_context_hash
52
+ expect(hash["date"]).to eq({ "@id" => "http://example.org/date",
53
+ "@type" => "xsd:date", "@container" => "@set" })
54
+ end
55
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/jsonld"
5
+
6
+ RSpec.describe Lutaml::JsonLd::Transform do
7
+ before do
8
+ stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
9
+ uri "http://www.w3.org/2004/02/skos/core#"
10
+ prefix "skos"
11
+ end)
12
+
13
+ stub_const("TestExNs", Class.new(Lutaml::Rdf::Namespace) do
14
+ uri "http://example.org/"
15
+ prefix "ex"
16
+ end)
17
+ stub_const("JsonLdTestModel", Class.new(Lutaml::Model::Serializable) do
18
+ attribute :name, :string
19
+ attribute :description, :string
20
+
21
+ rdf do
22
+ namespace TestSkosNs, TestExNs
23
+
24
+ subject { |m| "http://example.org/#{m.name}" } # rubocop:disable RSpec/NamedSubject
25
+
26
+ type "skos:Concept"
27
+
28
+ predicate :name, namespace: TestExNs, to: :name
29
+ predicate :description, namespace: TestExNs, to: :description
30
+ end
31
+ end)
32
+ end
33
+
34
+ let(:instance) do
35
+ JsonLdTestModel.new(name: "test", description: "A test concept")
36
+ end
37
+
38
+ describe "model_to_data" do
39
+ let(:result) { instance.to_jsonld }
40
+
41
+ it "generates @context with namespace prefixes" do
42
+ parsed = JSON.parse(result)
43
+ expect(parsed).to have_key("@context")
44
+ expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
45
+ expect(parsed["@context"]["ex"]).to eq("http://example.org/")
46
+ end
47
+
48
+ it "generates @type as compact IRI" do
49
+ parsed = JSON.parse(result)
50
+ expect(parsed["@type"]).to eq("skos:Concept")
51
+ end
52
+
53
+ it "generates @id from subject block" do
54
+ parsed = JSON.parse(result)
55
+ expect(parsed["@id"]).to eq("http://example.org/test")
56
+ end
57
+
58
+ it "includes predicate data" do
59
+ parsed = JSON.parse(result)
60
+ expect(parsed["name"]).to eq("test")
61
+ expect(parsed["description"]).to eq("A test concept")
62
+ end
63
+ end
64
+
65
+ describe "model_to_data without type and subject" do
66
+ before do
67
+ stub_const("MinimalJsonLdModel", Class.new(Lutaml::Model::Serializable) do
68
+ attribute :value, :string
69
+
70
+ rdf do
71
+ namespace TestSkosNs
72
+
73
+ predicate :notation, namespace: TestSkosNs, to: :value
74
+ end
75
+ end)
76
+ end
77
+
78
+ it "omits @type and @id when not defined" do
79
+ instance = MinimalJsonLdModel.new(value: "x")
80
+ parsed = JSON.parse(instance.to_jsonld)
81
+ expect(parsed).not_to have_key("@type")
82
+ expect(parsed).not_to have_key("@id")
83
+ expect(parsed["notation"]).to eq("x")
84
+ end
85
+ end
86
+
87
+ describe "data_to_model" do
88
+ let(:jsonld_input) do
89
+ {
90
+ "@context" => { "ex" => "http://example.org/" },
91
+ "@type" => "skos:Concept",
92
+ "@id" => "http://example.org/test",
93
+ "name" => "from_jsonld",
94
+ "description" => "Loaded from JSON-LD",
95
+ }
96
+ end
97
+
98
+ it "parses JSON-LD back to model" do
99
+ model = JsonLdTestModel.from_jsonld(JSON.generate(jsonld_input))
100
+ expect(model.name).to eq("from_jsonld")
101
+ expect(model.description).to eq("Loaded from JSON-LD")
102
+ end
103
+
104
+ it "strips JSON-LD keywords before attribute mapping" do
105
+ model = JsonLdTestModel.from_jsonld(JSON.generate(jsonld_input))
106
+ expect(model).to be_a(JsonLdTestModel)
107
+ end
108
+ end
109
+
110
+ describe "deserialization ignores JSON-LD keywords" do
111
+ let(:jsonld_input) do
112
+ {
113
+ "@context" => { "ex" => "http://example.org/" },
114
+ "@type" => "skos:Concept",
115
+ "@id" => "http://example.org/test",
116
+ "@graph" => [],
117
+ "name" => "value_only",
118
+ }
119
+ end
120
+
121
+ it "strips all @-prefixed keys" do
122
+ model = JsonLdTestModel.from_jsonld(JSON.generate(jsonld_input))
123
+ expect(model.name).to eq("value_only")
124
+ end
125
+ end
126
+
127
+ describe "round-trip" do
128
+ it "preserves data through model → JSON-LD → model" do
129
+ json = instance.to_jsonld
130
+ restored = JsonLdTestModel.from_jsonld(json)
131
+ expect(restored.name).to eq("test")
132
+ expect(restored.description).to eq("A test concept")
133
+ end
134
+
135
+ it "produces consistent @context through round-trip" do
136
+ json = instance.to_jsonld
137
+ parsed = JSON.parse(json)
138
+ expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
139
+ expect(parsed["@context"]["ex"]).to eq("http://example.org/")
140
+ end
141
+ end
142
+
143
+ describe "nil attribute values" do
144
+ let(:instance) { JsonLdTestModel.new(name: "test", description: nil) }
145
+
146
+ it "omits nil values from output" do
147
+ parsed = JSON.parse(instance.to_jsonld)
148
+ expect(parsed["name"]).to eq("test")
149
+ expect(parsed).not_to have_key("description")
150
+ end
151
+ end
152
+
153
+ describe "model with integer and boolean attributes" do
154
+ before do
155
+ stub_const("TypedJsonLdModel", Class.new(Lutaml::Model::Serializable) do
156
+ attribute :label, :string
157
+ attribute :count, :integer
158
+ attribute :active, :boolean
159
+
160
+ rdf do
161
+ namespace TestSkosNs
162
+
163
+ subject { |_| "http://example.org/1" } # rubocop:disable RSpec/NamedSubject
164
+
165
+ type "skos:Concept"
166
+
167
+ predicate :prefLabel, namespace: TestSkosNs, to: :label
168
+ predicate :notation, namespace: TestSkosNs, to: :count
169
+ predicate :note, namespace: TestSkosNs, to: :active
170
+ end
171
+ end)
172
+ end
173
+
174
+ it "serializes and deserializes typed values" do
175
+ instance = TypedJsonLdModel.new(label: "test", count: 42, active: true)
176
+ json = instance.to_jsonld
177
+ parsed = JSON.parse(json)
178
+ expect(parsed["notation"]).to eq(42)
179
+ expect(parsed["note"]).to be(true)
180
+
181
+ restored = TypedJsonLdModel.from_jsonld(json)
182
+ expect(restored.count).to eq(42)
183
+ expect(restored.active).to be(true)
184
+ end
185
+ end
186
+
187
+ describe "collection attributes" do
188
+ before do
189
+ stub_const("CollectionJsonLdModel", Class.new(Lutaml::Model::Serializable) do
190
+ attribute :tags, :string, collection: true
191
+
192
+ rdf do
193
+ namespace TestSkosNs
194
+
195
+ subject { |_| "http://example.org/1" } # rubocop:disable RSpec/NamedSubject
196
+
197
+ type "skos:Concept"
198
+
199
+ predicate :notation, namespace: TestSkosNs, to: :tags
200
+ end
201
+ end)
202
+ end
203
+
204
+ it "round-trips collection values" do
205
+ instance = CollectionJsonLdModel.new(tags: ["en", "fr"])
206
+ json = instance.to_jsonld
207
+ restored = CollectionJsonLdModel.from_jsonld(json)
208
+ expect(restored.tags).to eq(["en", "fr"])
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/turtle"
5
+ require "lutaml/jsonld"
6
+
7
+ RSpec.describe "RDF graph-aware serialization" do
8
+ before do
9
+ stub_const("GraphTestNs", Class.new(Lutaml::Rdf::Namespace) do
10
+ uri "http://example.org/"
11
+ prefix "ex"
12
+ end)
13
+
14
+ stub_const("GraphMemberModel", Class.new(Lutaml::Model::Serializable) do
15
+ attribute :code, :string
16
+ attribute :name, :string
17
+
18
+ rdf do
19
+ namespace GraphTestNs
20
+
21
+ type "ex:Item"
22
+ predicate :notation, namespace: GraphTestNs, to: :code
23
+ predicate :prefLabel, namespace: GraphTestNs, to: :name
24
+ end
25
+ end)
26
+
27
+ stub_const("GraphContainerModel", Class.new(Lutaml::Model::Serializable) do
28
+ attribute :id, :string
29
+ attribute :items, GraphMemberModel, collection: true
30
+
31
+ rdf do
32
+ namespace GraphTestNs
33
+ subject { |m| "http://example.org/container/#{m.id}" } # rubocop:disable RSpec/NamedSubject
34
+
35
+ type "ex:Container"
36
+ predicate :prefLabel, namespace: GraphTestNs, to: :id
37
+ members :items
38
+ end
39
+ end)
40
+ end
41
+
42
+ let(:first_item) { GraphMemberModel.new(code: "1", name: "First") }
43
+ let(:second_item) { GraphMemberModel.new(code: "2", name: "Second") }
44
+ let(:container) do
45
+ GraphContainerModel.new(id: "test", items: [first_item, second_item])
46
+ end
47
+
48
+ describe "Turtle serialization with members" do
49
+ subject(:turtle) { container.to_turtle }
50
+
51
+ it "includes container triples" do
52
+ expect(turtle).to include("a ex:Container")
53
+ expect(turtle).to include("ex:container")
54
+ end
55
+
56
+ it "includes container predicates" do
57
+ expect(turtle).to include('ex:prefLabel "test"')
58
+ end
59
+
60
+ it "includes all member blank nodes" do
61
+ expect(turtle.scan("a ex:Item").length).to be >= 2
62
+ end
63
+
64
+ it "includes member types" do
65
+ expect(turtle.scan("a ex:Item").length).to eq(2)
66
+ end
67
+
68
+ it "includes member predicates" do
69
+ expect(turtle).to include('ex:notation "1"')
70
+ expect(turtle).to include('ex:notation "2"')
71
+ expect(turtle).to include('ex:prefLabel "First"')
72
+ expect(turtle).to include('ex:prefLabel "Second"')
73
+ end
74
+
75
+ it "shares prefix declarations" do
76
+ prefix_count = turtle.scan(/@prefix ex:/).length
77
+ expect(prefix_count).to eq(1)
78
+ end
79
+ end
80
+
81
+ describe "JSON-LD serialization with members" do
82
+ subject(:jsonld) { JSON.parse(container.to_jsonld) }
83
+
84
+ it "includes @context" do
85
+ expect(jsonld["@context"]).to include("ex")
86
+ end
87
+
88
+ it "includes @graph array" do
89
+ expect(jsonld["@graph"]).to be_an(Array)
90
+ expect(jsonld["@graph"].length).to eq(3) # container + 2 items
91
+ end
92
+
93
+ it "includes container in @graph" do
94
+ container_data = jsonld["@graph"].find do |r|
95
+ r["@type"] == "ex:Container"
96
+ end
97
+ expect(container_data).not_to be_nil
98
+ expect(container_data["@id"]).to eq("http://example.org/container/test")
99
+ expect(container_data["prefLabel"]).to eq("test")
100
+ end
101
+
102
+ it "includes all members in @graph" do
103
+ items = jsonld["@graph"].select { |r| r["@type"] == "ex:Item" }
104
+ expect(items.length).to eq(2)
105
+ codes = items.map { |i| i["notation"] }
106
+ expect(codes).to contain_exactly("1", "2")
107
+ end
108
+ end
109
+
110
+ describe "container without subject (member-only)" do
111
+ before do
112
+ stub_const("MemberOnlyModel", Class.new(Lutaml::Model::Serializable) do
113
+ attribute :items, GraphMemberModel, collection: true
114
+
115
+ rdf do
116
+ namespace GraphTestNs
117
+ members :items
118
+ end
119
+ end)
120
+ end
121
+
122
+ let(:model) { MemberOnlyModel.new(items: [first_item, second_item]) }
123
+
124
+ it "serializes only member triples to turtle" do
125
+ turtle = model.to_turtle
126
+ expect(turtle).to include("a ex:Item")
127
+ expect(turtle).not_to include("ex:Container")
128
+ end
129
+
130
+ it "serializes only members to jsonld @graph" do
131
+ jsonld = JSON.parse(model.to_jsonld)
132
+ expect(jsonld["@graph"].length).to eq(2)
133
+ items = jsonld["@graph"].select { |r| r["@type"] == "ex:Item" }
134
+ expect(items.length).to eq(2)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/rdf"
5
+
6
+ RSpec.describe Lutaml::Rdf::Iri do
7
+ subject(:iri) { described_class.new("http://example.org/ns/Concept") }
8
+
9
+ it "stores frozen value" do
10
+ expect(iri.value).to eq("http://example.org/ns/Concept")
11
+ expect(iri.value).to be_frozen
12
+ end
13
+
14
+ it "coerces to string" do
15
+ expect(iri.to_s).to eq("http://example.org/ns/Concept")
16
+ end
17
+
18
+ describe "equality" do
19
+ it "equals Iri with same value" do
20
+ other = described_class.new("http://example.org/ns/Concept")
21
+ expect(iri).to eq(other)
22
+ end
23
+
24
+ it "does not equal Iri with different value" do
25
+ other = described_class.new("http://other.org/Thing")
26
+ expect(iri).not_to eq(other)
27
+ end
28
+
29
+ it "has consistent hash" do
30
+ other = described_class.new("http://example.org/ns/Concept")
31
+ expect(iri.hash).to eq(other.hash)
32
+ end
33
+ end
34
+
35
+ describe "comparable" do
36
+ it "compares by value" do
37
+ a = described_class.new("http://a.org/")
38
+ b = described_class.new("http://b.org/")
39
+ expect(a < b).to be true
40
+ end
41
+ end
42
+
43
+ describe "#expand" do
44
+ let(:ns_set) do
45
+ Lutaml::Rdf::NamespaceSet.new(
46
+ Lutaml::Rdf::Namespaces::SkosNamespace,
47
+ )
48
+ end
49
+
50
+ it "expands compact IRI via namespace set" do
51
+ iri = described_class.new("skos:Concept")
52
+ expect(iri.expand(ns_set)).to eq("http://www.w3.org/2004/02/skos/core#Concept")
53
+ end
54
+ end
55
+
56
+ describe "#compact" do
57
+ let(:ns_set) do
58
+ Lutaml::Rdf::NamespaceSet.new(
59
+ Lutaml::Rdf::Namespaces::SkosNamespace,
60
+ )
61
+ end
62
+
63
+ it "compacts full URI to prefixed form" do
64
+ iri = described_class.new("http://www.w3.org/2004/02/skos/core#Concept")
65
+ expect(iri.compact(ns_set)).to eq("skos:Concept")
66
+ end
67
+
68
+ it "returns nil for unknown URI" do
69
+ iri = described_class.new("http://unknown.org/Thing")
70
+ expect(iri.compact(ns_set)).to be_nil
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "lutaml/rdf"
5
+
6
+ RSpec.describe Lutaml::Rdf::Literal do
7
+ describe "plain literal" do
8
+ subject(:lit) { described_class.new("hello") }
9
+
10
+ it "stores value" do
11
+ expect(lit.value).to eq("hello")
12
+ end
13
+
14
+ it "has no datatype" do
15
+ expect(lit.datatype).to be_nil
16
+ end
17
+
18
+ it "has no language" do
19
+ expect(lit.language).to be_nil
20
+ end
21
+
22
+ it "serializes to Turtle" do
23
+ expect(lit.to_turtle).to eq('"hello"')
24
+ end
25
+
26
+ it "serializes to JSON-LD as plain value" do
27
+ expect(lit.to_jsonld_term).to eq("hello")
28
+ end
29
+ end
30
+
31
+ describe "language-tagged literal" do
32
+ subject(:lit) { described_class.new("hello", language: "en") }
33
+
34
+ it "serializes to Turtle with language tag" do
35
+ expect(lit.to_turtle).to eq('"hello"@en')
36
+ end
37
+
38
+ it "serializes to JSON-LD with @language" do
39
+ expect(lit.to_jsonld_term).to eq({ "@value" => "hello",
40
+ "@language" => "en" })
41
+ end
42
+ end
43
+
44
+ describe "typed literal" do
45
+ subject(:lit) { described_class.new("2024-01-01", datatype: "http://www.w3.org/2001/XMLSchema#date") }
46
+
47
+ it "serializes to Turtle with datatype" do
48
+ expect(lit.to_turtle).to eq('"2024-01-01"^^<http://www.w3.org/2001/XMLSchema#date>')
49
+ end
50
+
51
+ it "serializes to JSON-LD with @type" do
52
+ expect(lit.to_jsonld_term).to eq({ "@value" => "2024-01-01",
53
+ "@type" => "http://www.w3.org/2001/XMLSchema#date" })
54
+ end
55
+ end
56
+
57
+ describe "special character escaping" do
58
+ it "escapes quotes" do
59
+ lit = described_class.new('has "quotes"')
60
+ expect(lit.to_turtle).to eq('"has \\"quotes\\""')
61
+ end
62
+
63
+ it "escapes newlines" do
64
+ lit = described_class.new("line1\nline2")
65
+ expect(lit.to_turtle).to eq('"line1\\nline2"')
66
+ end
67
+
68
+ it "escapes backslashes" do
69
+ lit = described_class.new("back\\slash")
70
+ expect(lit.to_turtle).to eq('"back\\\\slash"')
71
+ end
72
+
73
+ it "escapes tabs" do
74
+ lit = described_class.new("tab\there")
75
+ expect(lit.to_turtle).to eq('"tab\\there"')
76
+ end
77
+ end
78
+
79
+ describe "equality" do
80
+ it "equals literal with same value, datatype, and language" do
81
+ a = described_class.new("hello", language: "en")
82
+ b = described_class.new("hello", language: "en")
83
+ expect(a).to eq(b)
84
+ end
85
+
86
+ it "does not equal with different language" do
87
+ a = described_class.new("hello", language: "en")
88
+ b = described_class.new("hello", language: "fr")
89
+ expect(a).not_to eq(b)
90
+ end
91
+
92
+ it "does not equal with different datatype" do
93
+ a = described_class.new("1", datatype: "xsd:integer")
94
+ b = described_class.new("1", datatype: "xsd:string")
95
+ expect(a).not_to eq(b)
96
+ end
97
+ end
98
+ end