redlander 0.2.1
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.
- data/README.rdoc +59 -0
- data/Rakefile +5 -0
- data/ext/README +1 -0
- data/ext/extconf.rb +17 -0
- data/ext/redland-pre.i +6 -0
- data/ext/redland-types.i +9 -0
- data/ext/redland_wrap.c +8052 -0
- data/lib/redlander.rb +61 -0
- data/lib/redlander/error_container.rb +41 -0
- data/lib/redlander/model.rb +33 -0
- data/lib/redlander/model_proxy.rb +85 -0
- data/lib/redlander/node.rb +90 -0
- data/lib/redlander/parser.rb +92 -0
- data/lib/redlander/parser_proxy.rb +18 -0
- data/lib/redlander/serializer.rb +80 -0
- data/lib/redlander/statement.rb +153 -0
- data/lib/redlander/statement_iterator.rb +37 -0
- data/lib/redlander/storage.rb +79 -0
- data/lib/redlander/version.rb +3 -0
- data/spec/redlander/model_spec.rb +239 -0
- data/spec/redlander/node_spec.rb +85 -0
- data/spec/redlander/parser_spec.rb +96 -0
- data/spec/redlander/serializer_spec.rb +52 -0
- data/spec/redlander/statement_spec.rb +86 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +127 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
class Statement
|
4
|
+
|
5
|
+
include ErrorContainer
|
6
|
+
|
7
|
+
attr_reader :rdf_statement
|
8
|
+
|
9
|
+
# Create an RDF statement.
|
10
|
+
# Options are:
|
11
|
+
# :subject
|
12
|
+
# :predicate
|
13
|
+
# :object
|
14
|
+
def initialize(options = {})
|
15
|
+
@rdf_statement = if options.is_a?(SWIG::TYPE_p_librdf_statement_s)
|
16
|
+
# A special case, where you can pass an instance of SWIG::TYPE_p_librdf_statement_s
|
17
|
+
# in order to create a Statement from an internal RDF statement representation.
|
18
|
+
options
|
19
|
+
else
|
20
|
+
s = options[:subject] && Node.new(options[:subject]).rdf_node
|
21
|
+
p = options[:predicate] && Node.new(options[:predicate]).rdf_node
|
22
|
+
o = options[:object] && Node.new(options[:object]).rdf_node
|
23
|
+
Redland.librdf_new_statement_from_nodes(Redlander.rdf_world, s, p, o)
|
24
|
+
end
|
25
|
+
|
26
|
+
raise RedlandError.new("Failed to create a new statement") unless @rdf_statement
|
27
|
+
ObjectSpace.define_finalizer(@rdf_statement, proc { Redland.librdf_free_statement(@rdf_statement) })
|
28
|
+
end
|
29
|
+
|
30
|
+
def subject
|
31
|
+
rdf_node = Redland.librdf_statement_get_subject(@rdf_statement)
|
32
|
+
rdf_node && Node.new(rdf_node)
|
33
|
+
end
|
34
|
+
|
35
|
+
def predicate
|
36
|
+
rdf_node = Redland.librdf_statement_get_predicate(@rdf_statement)
|
37
|
+
rdf_node && Node.new(rdf_node)
|
38
|
+
end
|
39
|
+
|
40
|
+
def object
|
41
|
+
rdf_node = Redland.librdf_statement_get_object(@rdf_statement)
|
42
|
+
rdf_node && Node.new(rdf_node)
|
43
|
+
end
|
44
|
+
|
45
|
+
# set the subject of the statement
|
46
|
+
def subject=(node)
|
47
|
+
Redland.librdf_statement_set_subject(@rdf_statement, rdf_node_from(node))
|
48
|
+
end
|
49
|
+
|
50
|
+
# set the predicate of the statement
|
51
|
+
def predicate=(node)
|
52
|
+
Redland.librdf_statement_set_predicate(@rdf_statement, rdf_node_from(node))
|
53
|
+
end
|
54
|
+
|
55
|
+
# set the object of the statement
|
56
|
+
def object=(node)
|
57
|
+
Redland.librdf_statement_set_object(@rdf_statement, rdf_node_from(node))
|
58
|
+
end
|
59
|
+
|
60
|
+
def model
|
61
|
+
@model
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add the statement to the given model.
|
65
|
+
#
|
66
|
+
# Returns the model on success, or nil.
|
67
|
+
# NOTE: Duplicate statements are not added to the model.
|
68
|
+
# However, this doesn't result in an error here.
|
69
|
+
def model=(model)
|
70
|
+
if model.nil?
|
71
|
+
@model = nil
|
72
|
+
else
|
73
|
+
if self.valid?
|
74
|
+
if Redland.librdf_model_add_statement(model.rdf_model, @rdf_statement).zero?
|
75
|
+
@model = model
|
76
|
+
else
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Destroy the statement (remove it from the model, if possible).
|
84
|
+
#
|
85
|
+
# Returns true if successfully removed from the model, or false.
|
86
|
+
# If the statement is not bound to a model, false is returned.
|
87
|
+
def destroy
|
88
|
+
if @model
|
89
|
+
if Redland.librdf_model_remove_statement(@model.rdf_model, @rdf_statement).zero?
|
90
|
+
self.model = nil
|
91
|
+
true
|
92
|
+
else
|
93
|
+
false
|
94
|
+
end
|
95
|
+
else
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def eql?(other_statement)
|
101
|
+
model == other_statement.model &&
|
102
|
+
subject == other_statement.subject &&
|
103
|
+
predicate == other_statement.predicate &&
|
104
|
+
object == other_statement.object
|
105
|
+
end
|
106
|
+
alias_method :==, :eql?
|
107
|
+
|
108
|
+
def hash
|
109
|
+
self.class.hash + to_s.hash
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_s
|
113
|
+
Redland.librdf_statement_to_string(@rdf_statement)
|
114
|
+
end
|
115
|
+
|
116
|
+
# A valid statement satisfies the following:
|
117
|
+
# URI or blank subject, URI predicate and URI or blank or literal object (i.e. anything).
|
118
|
+
def valid?
|
119
|
+
if is_valid = attributes_satisfy?
|
120
|
+
errors.clear
|
121
|
+
else
|
122
|
+
errors.add("is invalid")
|
123
|
+
end
|
124
|
+
is_valid
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def rdf_node_from(node)
|
131
|
+
if node.nil?
|
132
|
+
nil
|
133
|
+
else
|
134
|
+
# According to Redland docs,
|
135
|
+
# the node here becomes a part of the statement
|
136
|
+
# and must not be used by the caller!
|
137
|
+
if node.frozen?
|
138
|
+
raise RedlandError.new("The node is already bound to a statement and cannot be added.")
|
139
|
+
else
|
140
|
+
node.freeze.rdf_node
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def attributes_satisfy?
|
146
|
+
!subject.nil? && (subject.resource? || subject.blank?) &&
|
147
|
+
!predicate.nil? && predicate.resource? &&
|
148
|
+
!object.nil?
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
# Wrapper for a librdf_stream object.
|
4
|
+
module StatementIterator
|
5
|
+
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
# Iterate over statements in the stream.
|
9
|
+
def each
|
10
|
+
# TODO: The options specify matching criteria: subj, pred, obj;
|
11
|
+
# if an option is not specified, it matches any value,
|
12
|
+
# so with no options given, all statements will be returned.
|
13
|
+
if block_given?
|
14
|
+
while Redland.librdf_stream_end(@rdf_stream).zero?
|
15
|
+
yield current
|
16
|
+
Redland.librdf_stream_next(@rdf_stream).zero?
|
17
|
+
end
|
18
|
+
else
|
19
|
+
raise ::LocalJumpError.new("no block given")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Get the current Statement in the stream.
|
27
|
+
def current
|
28
|
+
rdf_statement = Redland.librdf_stream_get_object(@rdf_stream)
|
29
|
+
statement = Statement.new(rdf_statement)
|
30
|
+
# not using Statement#model= in order to avoid re-adding the statement to the model
|
31
|
+
statement.instance_variable_set(:@model, @model)
|
32
|
+
statement
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Redlander
|
2
|
+
|
3
|
+
module Storage
|
4
|
+
|
5
|
+
VALID_STORAGE_TYPES = [:memory, :hashes, :file, :uri, :tstore, :mysql, :sqlite, :postgresql]
|
6
|
+
|
7
|
+
# Creates a store of the given type
|
8
|
+
#
|
9
|
+
# Store types (:storage option) are:
|
10
|
+
# :memory
|
11
|
+
# :hashes
|
12
|
+
# :file - memory model initialized from RDF/XML file
|
13
|
+
# :uri - read-only memory model with URI provided in 'name' arg
|
14
|
+
# :mysql
|
15
|
+
# :sqlite
|
16
|
+
# :postgresql
|
17
|
+
# :tstore
|
18
|
+
# Options are:
|
19
|
+
# :name - ?
|
20
|
+
# :host - database host name (for store types: :postgres, :mysql, :tstore)
|
21
|
+
# :port - database host port (for store types: :postgres, :mysql, :tstore)
|
22
|
+
# :database - database name (for store types: :postgres, :mysql, :tstore)
|
23
|
+
# :user - database user name (for store types: :postgres, :mysql, :tstore)
|
24
|
+
# :password - database user password (for store types: :postgres, :mysql, :tstore)
|
25
|
+
# :hash_type - hash type (for store types: :bdb)
|
26
|
+
# can be either 'memory' or 'bdb'
|
27
|
+
# :new - force creation of a new store
|
28
|
+
# :dir - directory path (for store types: :hashes)
|
29
|
+
# :contexts - support contexts (for store types: :hashes, :memory)
|
30
|
+
# :write - allow writing data to the store (for store types: :hashes)
|
31
|
+
#
|
32
|
+
# NOTE: When dealing with databases,
|
33
|
+
# Redland (1.0.7) just crashes when the required tables aren't available!
|
34
|
+
def self.initialize_storage(options = {})
|
35
|
+
storage_type, storage_options = split_options(options)
|
36
|
+
storage_type ||= :memory
|
37
|
+
|
38
|
+
unless VALID_STORAGE_TYPES.include?(storage_type)
|
39
|
+
raise RedlandError.new("Unknown storage type: #{storage_type}")
|
40
|
+
end
|
41
|
+
|
42
|
+
rdf_storage = Redland.librdf_new_storage(Redlander.rdf_world,
|
43
|
+
storage_type.to_s,
|
44
|
+
storage_options.delete(:name).to_s,
|
45
|
+
Redlander.to_rdf_options(storage_options))
|
46
|
+
raise RedlandError.new("Failed to initialize storage") unless rdf_storage
|
47
|
+
ObjectSpace.define_finalizer(rdf_storage, proc { Redland.librdf_free_storage(rdf_storage) })
|
48
|
+
|
49
|
+
rdf_storage
|
50
|
+
end
|
51
|
+
|
52
|
+
# Wrap changes to the given model in a transaction.
|
53
|
+
# If an exception is raised in the block, the transaction is rolled back.
|
54
|
+
# (Does not work for all storages, in which case the changes are instanteous).
|
55
|
+
def self.transaction(model, &block)
|
56
|
+
Redland.librdf_model_transaction_start(model.rdf_model).zero? || RedlandError.new("Failed to initialize a transaction")
|
57
|
+
block.call
|
58
|
+
Redland.librdf_model_transaction_commit(model.rdf_model).zero? || RedlandError.new("Failed to commit the transaction")
|
59
|
+
rescue
|
60
|
+
rollback(model)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Rollback a latest transaction for the given model.
|
64
|
+
def self.rollback(model)
|
65
|
+
Redland.librdf_model_transaction_rollback(model.rdf_model).zero? || RedlandError.new("Failed to rollback the latest transaction")
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def self.split_options(options = {})
|
72
|
+
storage_options = options.dup
|
73
|
+
storage_type = storage_options.delete(:storage)
|
74
|
+
[storage_type, storage_options]
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Model do
|
4
|
+
|
5
|
+
it "should be created with default options" do
|
6
|
+
lambda { Model.new }.should_not raise_exception
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "statements" do
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
@model = Model.new
|
13
|
+
end
|
14
|
+
|
15
|
+
it do
|
16
|
+
@model.statements.should be_an_instance_of(ModelProxy)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be created in the model" do
|
20
|
+
lambda {
|
21
|
+
@model.statements.create(statement_attributes)
|
22
|
+
}.should change(@model.statements, :size).by(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should be bound to the model" do
|
26
|
+
statement = @model.statements.create(statement_attributes)
|
27
|
+
statement.model.should be(@model)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should be iterated over" do
|
31
|
+
statement = @model.statements.create(statement_attributes)
|
32
|
+
statements = []
|
33
|
+
lambda {
|
34
|
+
@model.statements.each do |s|
|
35
|
+
statements << s
|
36
|
+
end
|
37
|
+
}.should change(statements, :size).by(1)
|
38
|
+
statements.first.should eql(statement)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should be added to the model" do
|
42
|
+
statement = Statement.new(statement_attributes)
|
43
|
+
lambda {
|
44
|
+
lambda {
|
45
|
+
@model.statements.add(statement)
|
46
|
+
}.should change(@model.statements, :size).by(1)
|
47
|
+
}.should change(statement, :model).from(nil).to(@model)
|
48
|
+
@model.statements.should include(statement)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should be found without a block" do
|
52
|
+
statement = @model.statements.create(statement_attributes)
|
53
|
+
@model.statements.find(:first).should eql(statement)
|
54
|
+
@model.statements.first.should eql(statement)
|
55
|
+
@model.statements.find(:all).should eql([statement])
|
56
|
+
@model.statements.all.should eql([statement])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should be found with a block" do
|
60
|
+
statements = []
|
61
|
+
statement = @model.statements.create(statement_attributes)
|
62
|
+
lambda {
|
63
|
+
@model.statements.find(:all) do |st|
|
64
|
+
statements << st
|
65
|
+
end
|
66
|
+
}.should change(statements, :size).by(1)
|
67
|
+
statements.first.should eql(statement)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should be found by an attribute" do
|
71
|
+
statements = []
|
72
|
+
statement = @model.statements.create(statement_attributes)
|
73
|
+
lambda {
|
74
|
+
@model.statements.find(:all, :object => statement.object) do |st|
|
75
|
+
statements << st
|
76
|
+
end
|
77
|
+
}.should change(statements, :size).by(1)
|
78
|
+
statements.first.should eql(statement)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should not be found with a block" do
|
82
|
+
statements = []
|
83
|
+
statement = @model.statements.create(statement_attributes)
|
84
|
+
lambda {
|
85
|
+
@model.statements.find(:all, :object => "another object") do |st|
|
86
|
+
statements << st
|
87
|
+
end
|
88
|
+
}.should_not change(statements, :size)
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should be removed from the model" do
|
92
|
+
statement = @model.statements.create(statement_attributes)
|
93
|
+
lambda {
|
94
|
+
statement.destroy
|
95
|
+
}.should change(@model.statements, :size).by(-1)
|
96
|
+
@model.statements.should_not include(statement)
|
97
|
+
statement.model.should be_nil
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def statement_attributes
|
104
|
+
s = URI.parse('http://example.com/concepts#subject')
|
105
|
+
p = URI.parse('http://example.com/concepts#label')
|
106
|
+
o = "subject!"
|
107
|
+
{
|
108
|
+
:subject => s,
|
109
|
+
:predicate => p,
|
110
|
+
:object => o
|
111
|
+
}
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "serialization" do
|
117
|
+
|
118
|
+
before :each do
|
119
|
+
@model = Model.new
|
120
|
+
s = URI.parse("http://example.com/concepts#two-dimensional_seismic_imaging")
|
121
|
+
p = URI.parse("http://www.w3.org/2000/01/rdf-schema#label")
|
122
|
+
o = "2-D seismic imaging@en"
|
123
|
+
@model.statements.create(:subject => s, :predicate => p, :object => o)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should produce RDF/XML content" do
|
127
|
+
content = @model.to_rdfxml
|
128
|
+
content.should be_an_instance_of(String)
|
129
|
+
content.should include('2-D seismic imaging')
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should produce N-Triples content" do
|
133
|
+
content = @model.to_ntriples
|
134
|
+
content.should be_an_instance_of(String)
|
135
|
+
content.should include('2-D seismic imaging@en')
|
136
|
+
end
|
137
|
+
|
138
|
+
it "should produce Turtle content" do
|
139
|
+
content = @model.to_turtle
|
140
|
+
content.should be_an_instance_of(String)
|
141
|
+
content.should include('2-D seismic imaging@en')
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should produce JSON content" do
|
145
|
+
content = @model.to_json
|
146
|
+
content.should be_an_instance_of(String)
|
147
|
+
content.should include('2-D seismic imaging@en')
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should produce DOT content" do
|
151
|
+
content = @model.to_dot
|
152
|
+
content.should be_an_instance_of(String)
|
153
|
+
content.should include('2-D seismic imaging@en')
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "file source" do
|
157
|
+
|
158
|
+
before :each do
|
159
|
+
content = '<?xml version="1.0" encoding="utf-8"?><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="http://example.com/concepts#two-dimensional_seismic_imaging"><ns0:label xmlns:ns0="http://www.w3.org/2000/01/rdf-schema#" xml:lang="en">2-D seismic imaging</ns0:label></rdf:Description></rdf:RDF>'
|
160
|
+
reference_model = Model.new
|
161
|
+
reference_model.from_rdfxml(content, :base_uri => 'http://example.com/concepts')
|
162
|
+
reference_model.to_file(filename)
|
163
|
+
end
|
164
|
+
|
165
|
+
after :each do
|
166
|
+
cleanup
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should be loaded from a file" do
|
170
|
+
lambda {
|
171
|
+
@model.from_file(filename, :base_uri => 'http://example.com/concepts')
|
172
|
+
}.should change(@model.statements, :size).by(1)
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "file destination" do
|
178
|
+
|
179
|
+
before :each do
|
180
|
+
cleanup
|
181
|
+
end
|
182
|
+
|
183
|
+
after :each do
|
184
|
+
cleanup
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should produce a file" do
|
188
|
+
@model.to_file(filename)
|
189
|
+
File.should be_exists(filename)
|
190
|
+
File.size(filename).should_not be_zero
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def cleanup
|
198
|
+
File.delete(filename) if File.exists?(filename)
|
199
|
+
end
|
200
|
+
|
201
|
+
def filename
|
202
|
+
"test_model.rdf"
|
203
|
+
end
|
204
|
+
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "deserialization" do
|
208
|
+
|
209
|
+
before :each do
|
210
|
+
@model = Model.new
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should be successful for RDF/XML data" do
|
214
|
+
content = '<?xml version="1.0" encoding="utf-8"?><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><rdf:Description rdf:about="http://example.com/concepts#two-dimensional_seismic_imaging"><ns0:label xmlns:ns0="http://www.w3.org/2000/01/rdf-schema#" xml:lang="en">2-D seismic imaging</ns0:label></rdf:Description></rdf:RDF>'
|
215
|
+
lambda {
|
216
|
+
@model.from_rdfxml(content, :base_uri => 'http://example.com/concepts')
|
217
|
+
}.should change(@model.statements, :size).by(1)
|
218
|
+
end
|
219
|
+
|
220
|
+
it "should be successful for N-Triples data" do
|
221
|
+
content = '<http://example.org/ns/a2> <http://example.org/ns/b2> <http://example.org/ns/c2> .'
|
222
|
+
lambda {
|
223
|
+
@model.from_ntriples(content)
|
224
|
+
}.should change(@model.statements, :size).by(1)
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should be successful for Turtle data" do
|
228
|
+
content = '# this is a complete turtle document
|
229
|
+
@prefix foo: <http://example.org/ns#> .
|
230
|
+
@prefix : <http://other.example.org/ns#> .
|
231
|
+
foo:bar foo: : .'
|
232
|
+
lambda {
|
233
|
+
@model.from_turtle(content, :base_uri => 'http://example.com/concepts')
|
234
|
+
}.should change(@model.statements, :size).by(1)
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|