lutaml-model 0.8.4 → 0.8.6
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 +5 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +91 -22
- 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/_migrations/0-8-0-namespace-restructuring.adoc +90 -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/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
- data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
- data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
- data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
- data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
- data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
- data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
- data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
- data/lib/lutaml/xml/adapter.rb +0 -1
- data/lib/lutaml/xml/adapter_element.rb +7 -1
- data/lib/lutaml/xml/builder/base.rb +0 -1
- data/lib/lutaml/xml/data_model.rb +9 -1
- data/lib/lutaml/xml/document.rb +3 -1
- data/lib/lutaml/xml/element.rb +13 -10
- data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
- data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
- data/lib/lutaml/xml/xml_element.rb +24 -20
- 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
- data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
- data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
- data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
- data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
- metadata +58 -3
- data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/turtle"
|
|
5
|
+
require "lutaml/jsonld"
|
|
6
|
+
|
|
7
|
+
RSpec.describe "Round-trip fidelity" do
|
|
8
|
+
before do
|
|
9
|
+
stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
|
|
10
|
+
uri "http://www.w3.org/2004/02/skos/core#"
|
|
11
|
+
prefix "skos"
|
|
12
|
+
end)
|
|
13
|
+
|
|
14
|
+
stub_const("TestExNs", Class.new(Lutaml::Rdf::Namespace) do
|
|
15
|
+
uri "http://example.org/"
|
|
16
|
+
prefix "ex"
|
|
17
|
+
end)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "Turtle round-trip" do
|
|
21
|
+
before do
|
|
22
|
+
stub_const("RtTurtleModel", Class.new(Lutaml::Model::Serializable) do
|
|
23
|
+
attribute :label, :string
|
|
24
|
+
attribute :note, :string
|
|
25
|
+
attribute :code, :integer
|
|
26
|
+
|
|
27
|
+
turtle do
|
|
28
|
+
namespace TestSkosNs
|
|
29
|
+
|
|
30
|
+
subject { |m| "http://example.org/item/#{m.code}" } # rubocop:disable RSpec/NamedSubject
|
|
31
|
+
|
|
32
|
+
type "skos:Concept"
|
|
33
|
+
predicate :prefLabel,
|
|
34
|
+
namespace: TestSkosNs,
|
|
35
|
+
to: :label
|
|
36
|
+
predicate :note,
|
|
37
|
+
namespace: TestSkosNs,
|
|
38
|
+
to: :note
|
|
39
|
+
predicate :notation,
|
|
40
|
+
namespace: TestSkosNs,
|
|
41
|
+
to: :code
|
|
42
|
+
end
|
|
43
|
+
end)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "preserves string and integer attributes" do
|
|
47
|
+
original = RtTurtleModel.new(label: "hello", note: "world", code: 99)
|
|
48
|
+
turtle = original.to_turtle
|
|
49
|
+
restored = RtTurtleModel.from_turtle(turtle)
|
|
50
|
+
expect(restored.label).to eq("hello")
|
|
51
|
+
expect(restored.note).to eq("world")
|
|
52
|
+
expect(restored.code).to eq(99)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "handles special characters in string values" do
|
|
56
|
+
original = RtTurtleModel.new(label: 'say "hi"', note: "line1\nline2",
|
|
57
|
+
code: 1)
|
|
58
|
+
turtle = original.to_turtle
|
|
59
|
+
restored = RtTurtleModel.from_turtle(turtle)
|
|
60
|
+
expect(restored.label).to eq('say "hi"')
|
|
61
|
+
expect(restored.note).to eq("line1\nline2")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "handles nil optional attributes" do
|
|
65
|
+
original = RtTurtleModel.new(label: "test", note: nil, code: 1)
|
|
66
|
+
turtle = original.to_turtle
|
|
67
|
+
restored = RtTurtleModel.from_turtle(turtle)
|
|
68
|
+
expect(restored.label).to eq("test")
|
|
69
|
+
expect(restored.note).to be_nil
|
|
70
|
+
expect(restored.code).to eq(1)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "JSON-LD round-trip" do
|
|
75
|
+
before do
|
|
76
|
+
stub_const("RtJsonLdModel", Class.new(Lutaml::Model::Serializable) do
|
|
77
|
+
attribute :title, :string
|
|
78
|
+
attribute :body, :string
|
|
79
|
+
attribute :priority, :integer
|
|
80
|
+
|
|
81
|
+
rdf do
|
|
82
|
+
namespace TestExNs
|
|
83
|
+
|
|
84
|
+
subject { |m| "http://example.org/articles/#{m.priority}" } # rubocop:disable RSpec/NamedSubject
|
|
85
|
+
|
|
86
|
+
type "http://example.org/Article"
|
|
87
|
+
|
|
88
|
+
predicate :title, namespace: TestExNs, to: :title
|
|
89
|
+
predicate :body, namespace: TestExNs, to: :body
|
|
90
|
+
predicate :priority, namespace: TestExNs, to: :priority
|
|
91
|
+
end
|
|
92
|
+
end)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "preserves all attributes through serialize → deserialize" do
|
|
96
|
+
original = RtJsonLdModel.new(title: "Test", body: "Content", priority: 5)
|
|
97
|
+
json = original.to_jsonld
|
|
98
|
+
restored = RtJsonLdModel.from_jsonld(json)
|
|
99
|
+
expect(restored.title).to eq("Test")
|
|
100
|
+
expect(restored.body).to eq("Content")
|
|
101
|
+
expect(restored.priority).to eq(5)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "preserves @context structure across round-trip" do
|
|
105
|
+
original = RtJsonLdModel.new(title: "Test", body: "Content", priority: 1)
|
|
106
|
+
json1 = original.to_jsonld
|
|
107
|
+
restored = RtJsonLdModel.from_jsonld(json1)
|
|
108
|
+
json2 = restored.to_jsonld
|
|
109
|
+
|
|
110
|
+
ctx1 = JSON.parse(json1)["@context"]
|
|
111
|
+
ctx2 = JSON.parse(json2)["@context"]
|
|
112
|
+
expect(ctx1).to eq(ctx2)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "handles nil optional attributes" do
|
|
116
|
+
original = RtJsonLdModel.new(title: "Test", body: nil, priority: 1)
|
|
117
|
+
json = original.to_jsonld
|
|
118
|
+
restored = RtJsonLdModel.from_jsonld(json)
|
|
119
|
+
expect(restored.title).to eq("Test")
|
|
120
|
+
expect(restored.body).to be_nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe "Error handling" do
|
|
125
|
+
it "Turtle raises MissingSubjectError without subject" do
|
|
126
|
+
stub_const("NoSubjModel", Class.new(Lutaml::Model::Serializable) do
|
|
127
|
+
attribute :name, :string
|
|
128
|
+
|
|
129
|
+
turtle do
|
|
130
|
+
namespace TestSkosNs
|
|
131
|
+
type "skos:Concept"
|
|
132
|
+
predicate :prefLabel,
|
|
133
|
+
namespace: TestSkosNs,
|
|
134
|
+
to: :name
|
|
135
|
+
end
|
|
136
|
+
end)
|
|
137
|
+
|
|
138
|
+
expect { NoSubjModel.new(name: "test").to_turtle }
|
|
139
|
+
.to raise_error(Lutaml::Turtle::MissingSubjectError, /subject/)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "JSON-LD handles invalid JSON gracefully" do
|
|
143
|
+
stub_const("SimpleJsonLdModel", Class.new(Lutaml::Model::Serializable) do
|
|
144
|
+
attribute :name, :string
|
|
145
|
+
|
|
146
|
+
rdf do
|
|
147
|
+
namespace TestExNs
|
|
148
|
+
predicate :name, namespace: TestExNs, to: :name
|
|
149
|
+
end
|
|
150
|
+
end)
|
|
151
|
+
|
|
152
|
+
expect { SimpleJsonLdModel.from_jsonld("not valid json!!!") }
|
|
153
|
+
.to raise_error(Lutaml::Model::InvalidFormatError)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "Rdf::MappingRule validates namespace type" do
|
|
157
|
+
mapping = Lutaml::Rdf::Mapping.new
|
|
158
|
+
expect do
|
|
159
|
+
mapping.predicate(:foo, namespace: String, to: :bar)
|
|
160
|
+
end.to raise_error(ArgumentError, /Rdf::Namespace/)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "Rdf::MappingRule requires :to parameter" do
|
|
164
|
+
mapping = Lutaml::Rdf::Mapping.new
|
|
165
|
+
expect do
|
|
166
|
+
mapping.predicate(:foo, namespace: TestSkosNs, to: nil)
|
|
167
|
+
end.to raise_error(ArgumentError, /required/)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "lutaml/jsonld"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Lutaml::JsonLd::Adapter do
|
|
7
|
+
let(:jsonld_hash) do
|
|
8
|
+
{
|
|
9
|
+
"@context" => { "name" => "http://example.org/name" },
|
|
10
|
+
"@type" => "Thing",
|
|
11
|
+
"name" => "test",
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe ".parse" do
|
|
16
|
+
it "parses valid JSON-LD string to hash" do
|
|
17
|
+
json = JSON.generate(jsonld_hash)
|
|
18
|
+
result = described_class.parse(json)
|
|
19
|
+
expect(result).to eq(jsonld_hash.transform_keys(&:to_s))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe "#to_jsonld" do
|
|
24
|
+
it "generates JSON-LD string from hash" do
|
|
25
|
+
adapter = described_class.new(jsonld_hash)
|
|
26
|
+
result = adapter.to_jsonld
|
|
27
|
+
parsed = JSON.parse(result)
|
|
28
|
+
expect(parsed["@context"]).to eq({ "name" => "http://example.org/name" })
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "supports pretty generation" do
|
|
32
|
+
adapter = described_class.new(jsonld_hash)
|
|
33
|
+
result = adapter.to_jsonld(pretty: true)
|
|
34
|
+
expect(result).to include("\n")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "round-trips parse → generate" do
|
|
39
|
+
json = JSON.generate(jsonld_hash)
|
|
40
|
+
parsed = described_class.parse(json)
|
|
41
|
+
adapter = described_class.new(parsed)
|
|
42
|
+
result = adapter.to_jsonld
|
|
43
|
+
round_tripped = JSON.parse(result)
|
|
44
|
+
expect(round_tripped).to eq(jsonld_hash.transform_keys(&:to_s))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -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
|