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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +3 -1
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +12 -18
- data/Gemfile +2 -0
- data/README.adoc +114 -2
- data/docs/_guides/index.adoc +18 -0
- data/docs/_guides/jsonld-serialization.adoc +217 -0
- data/docs/_guides/rdf-serialization.adoc +344 -0
- data/docs/_guides/turtle-serialization.adoc +224 -0
- data/docs/_pages/serialization_adapters.adoc +31 -0
- data/docs/_references/index.adoc +1 -0
- data/docs/_references/rdf-namespaces.adoc +243 -0
- data/docs/index.adoc +3 -2
- data/lib/lutaml/jsonld/adapter.rb +23 -0
- data/lib/lutaml/jsonld/context.rb +69 -0
- data/lib/lutaml/jsonld/term_definition.rb +39 -0
- data/lib/lutaml/jsonld/transform.rb +174 -0
- data/lib/lutaml/jsonld.rb +23 -0
- data/lib/lutaml/model/format_registry.rb +10 -1
- data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +6 -0
- data/lib/lutaml/rdf/error.rb +7 -0
- data/lib/lutaml/rdf/iri.rb +44 -0
- data/lib/lutaml/rdf/language_tagged.rb +11 -0
- data/lib/lutaml/rdf/literal.rb +62 -0
- data/lib/lutaml/rdf/mapping.rb +71 -0
- data/lib/lutaml/rdf/mapping_rule.rb +35 -0
- data/lib/lutaml/rdf/member_rule.rb +13 -0
- data/lib/lutaml/rdf/namespace.rb +58 -0
- data/lib/lutaml/rdf/namespace_set.rb +69 -0
- data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
- data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces.rb +14 -0
- data/lib/lutaml/rdf/transform.rb +36 -0
- data/lib/lutaml/rdf.rb +19 -0
- data/lib/lutaml/turtle/adapter.rb +35 -0
- data/lib/lutaml/turtle/mapping.rb +7 -0
- data/lib/lutaml/turtle/transform.rb +158 -0
- data/lib/lutaml/turtle.rb +22 -0
- data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
- data/spec/lutaml/integration/multi_format_spec.rb +106 -0
- data/spec/lutaml/integration/round_trip_spec.rb +170 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
- data/spec/lutaml/jsonld/context_spec.rb +114 -0
- data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
- data/spec/lutaml/jsonld/transform_spec.rb +211 -0
- data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
- data/spec/lutaml/rdf/iri_spec.rb +73 -0
- data/spec/lutaml/rdf/literal_spec.rb +98 -0
- data/spec/lutaml/rdf/mapping_spec.rb +164 -0
- data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
- data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
- data/spec/lutaml/rdf/namespace_spec.rb +241 -0
- data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
- data/spec/lutaml/turtle/adapter_spec.rb +47 -0
- data/spec/lutaml/turtle/mapping_spec.rb +123 -0
- data/spec/lutaml/turtle/transform_spec.rb +273 -0
- 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
|