tripod 0.0.1 → 0.0.2
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/.gitignore +1 -0
- data/Gemfile +4 -0
- data/LICENSE +1 -1
- data/README.md +5 -1
- data/Rakefile +13 -0
- data/lib/tripod.rb +72 -1
- data/lib/tripod/attributes.rb +80 -0
- data/lib/tripod/components.rb +23 -0
- data/lib/tripod/errors.rb +4 -0
- data/lib/tripod/errors/resource_not_found.rb +8 -0
- data/lib/tripod/errors/uri_not_set.rb +9 -0
- data/lib/tripod/errors/validations.rb +18 -0
- data/lib/tripod/fields.rb +7 -0
- data/lib/tripod/finders.rb +44 -0
- data/lib/tripod/persistence.rb +68 -0
- data/lib/tripod/repository.rb +43 -0
- data/lib/tripod/resource.rb +110 -0
- data/lib/tripod/sparql_client.rb +101 -0
- data/lib/tripod/state.rb +47 -0
- data/lib/tripod/version.rb +1 -1
- data/spec/app/models/person.rb +5 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/tripod/attributes_spec.rb +103 -0
- data/spec/tripod/finders_spec.rb +71 -0
- data/spec/tripod/persistence_spec.rb +103 -0
- data/spec/tripod/repository_spec.rb +60 -0
- data/spec/tripod/resource_spec.rb +63 -0
- data/spec/tripod/state_spec.rb +93 -0
- data/tripod.gemspec +11 -0
- metadata +112 -6
@@ -0,0 +1,110 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# module for all domain objects that need to be persisted to the database
|
4
|
+
# as resources
|
5
|
+
module Tripod::Resource
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
include Tripod::Components
|
10
|
+
|
11
|
+
included do
|
12
|
+
# every resource needs a graph and a uri set.
|
13
|
+
validates_presence_of :uri, :graph_uri
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :new_record
|
17
|
+
attr_reader :graph_uri
|
18
|
+
attr_reader :uri
|
19
|
+
|
20
|
+
# Instantiate a +Resource+.
|
21
|
+
# Optionsally pass a uri
|
22
|
+
#
|
23
|
+
# @example Instantiate a new Resource
|
24
|
+
# Person.new('http://swirrl.com/ric.rdf#me')
|
25
|
+
#
|
26
|
+
# @param [ String, RDF::URI ] uri The uri of the resource.
|
27
|
+
#
|
28
|
+
# @return [ Resource ] A new +Resource+
|
29
|
+
def initialize(uri=nil, graph_uri=nil)
|
30
|
+
@new_record = true
|
31
|
+
@uri = RDF::URI(uri.to_s) if uri
|
32
|
+
@graph_uri = RDF::URI(graph_uri.to_s) if graph_uri
|
33
|
+
@repository = RDF::Repository.new
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set the uri for this resource
|
37
|
+
def uri=(new_uri)
|
38
|
+
if new_uri
|
39
|
+
@uri = RDF::URI(new_uri.to_s)
|
40
|
+
else
|
41
|
+
@uri = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Set the uri for this resource
|
46
|
+
def graph_uri=(new_graph_uri)
|
47
|
+
if new_graph_uri
|
48
|
+
@graph_uri = RDF::URI(new_graph_uri.to_s)
|
49
|
+
else
|
50
|
+
@graph_uri = nil
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# default comparison is via the uri
|
55
|
+
def <=>(other)
|
56
|
+
uri.to_s <=> uri.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
# performs equality checking on the uris
|
60
|
+
def ==(other)
|
61
|
+
self.class == other.class &&
|
62
|
+
uri.to_s == uri.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
# performs equality checking on the class
|
66
|
+
def ===(other)
|
67
|
+
other.class == Class ? self.class === other : self == other
|
68
|
+
end
|
69
|
+
|
70
|
+
# delegates to ==
|
71
|
+
def eql?()
|
72
|
+
self == (other)
|
73
|
+
end
|
74
|
+
|
75
|
+
def hash
|
76
|
+
identity.hash
|
77
|
+
end
|
78
|
+
|
79
|
+
# a resource is absolutely identified by it's class and id.
|
80
|
+
def identity
|
81
|
+
[ self.class, self.uri.to_s ]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Return the key value for the resource.
|
85
|
+
#
|
86
|
+
# @example Return the key.
|
87
|
+
# resource.to_key
|
88
|
+
#
|
89
|
+
# @return [ Object ] The uri of the resource or nil if new.
|
90
|
+
def to_key
|
91
|
+
(persisted? || destroyed?) ? [ uri.to_s ] : nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_a
|
95
|
+
[ self ]
|
96
|
+
end
|
97
|
+
|
98
|
+
module ClassMethods
|
99
|
+
|
100
|
+
# Performs class equality checking.
|
101
|
+
def ===(other)
|
102
|
+
other.class == Class ? self <= other : other.is_a?(self)
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
# causes any hooks to be fired, if they've been setup on_load of :tripod.
|
110
|
+
ActiveSupport.run_load_hooks(:triploid, Tripod::Resource)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# this module is responsible for connecting to an http sparql endpoint
|
4
|
+
module Tripod::SparqlClient
|
5
|
+
|
6
|
+
module Query
|
7
|
+
|
8
|
+
|
9
|
+
# Runs a +sparql+ query against the endpoint. Returns a RestClient response object.
|
10
|
+
#
|
11
|
+
# @example Run a query
|
12
|
+
# Tripload::Sparql.query('SELECT * WHERE {?s ?p ?o}')
|
13
|
+
#
|
14
|
+
# @return [ RestClient::Response ]
|
15
|
+
|
16
|
+
def self.query(sparql, format='json', headers = {})
|
17
|
+
|
18
|
+
begin
|
19
|
+
params = { :params => {:query => sparql, :output => format } }
|
20
|
+
hdrs = headers.merge(params)
|
21
|
+
RestClient::Request.execute(
|
22
|
+
:method => :get,
|
23
|
+
:url => Tripod.query_endpoint,
|
24
|
+
:headers => hdrs,
|
25
|
+
:timeout => Tripod.timeout_seconds,
|
26
|
+
)
|
27
|
+
rescue RestClient::BadRequest => e
|
28
|
+
body = e.http_body
|
29
|
+
if body.start_with?('Error 400: Parse error:')
|
30
|
+
# TODO: this is a SPARQL parsing exception. Do something different.
|
31
|
+
puts body.inspect
|
32
|
+
raise e
|
33
|
+
else
|
34
|
+
puts body.inspect
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Runs a SELECT +query+ against the endpoint. Returns a hash of the results.
|
41
|
+
# Specify +raw_format+ if you want the results raw, as returned from the SPARQL endpoint.
|
42
|
+
#
|
43
|
+
# @param [ String ] query The query to run
|
44
|
+
# @param [ String ] raw_format valid formats are: 'json', 'text', 'csv', 'xml'
|
45
|
+
#
|
46
|
+
# @example Run a SELECT query
|
47
|
+
# Triploid::Sparql.select('SELECT * WHERE {?s ?p ?o}')
|
48
|
+
#
|
49
|
+
# @return [ Hash, String ]
|
50
|
+
def self.select(query, raw_format=nil)
|
51
|
+
query_response = self.query(query, (raw_format || 'json'))
|
52
|
+
if raw_format
|
53
|
+
query_response.body
|
54
|
+
else
|
55
|
+
JSON.parse(query_response.body)["results"]["bindings"]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Executes a DESCRIBE +query+ against the SPARQL endpoint.
|
60
|
+
# Executes the +query+ and returns ntriples by default
|
61
|
+
#
|
62
|
+
# @example Run a DESCRIBE query
|
63
|
+
# Triploid::Sparql.select('DESCRIBE <http://foo>')
|
64
|
+
#
|
65
|
+
# @param [ String ] query The query to run
|
66
|
+
# @param [ String ] accept_header The header to pass to the database.
|
67
|
+
#
|
68
|
+
# @return [ String ] the raw response from the endpoint
|
69
|
+
def self.describe(query, accept_header='application/n-triples')
|
70
|
+
response = self.query(query, nil, {:accept=>accept_header})
|
71
|
+
return response.body
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module Update
|
76
|
+
|
77
|
+
def self.update(sparql)
|
78
|
+
|
79
|
+
begin
|
80
|
+
RestClient::Request.execute(
|
81
|
+
:method => :post,
|
82
|
+
:url => Tripod.update_endpoint,
|
83
|
+
:timeout => Tripod.timeout_seconds,
|
84
|
+
:payload => {:update => sparql}
|
85
|
+
)
|
86
|
+
return true
|
87
|
+
rescue RestClient::BadRequest => e
|
88
|
+
body = e.http_body
|
89
|
+
if body.start_with?('Error 400: Parse error:')
|
90
|
+
# TODO: this is a SPARQL parsing exception. Do something different.
|
91
|
+
puts body.inspect
|
92
|
+
raise e
|
93
|
+
else
|
94
|
+
puts body.inspect
|
95
|
+
raise e
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
data/lib/tripod/state.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# This module contains the behaviour for getting the various states through which a
|
4
|
+
# resource can transition.
|
5
|
+
module Tripod::State
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
attr_writer :destroyed, :new_record
|
10
|
+
|
11
|
+
# Returns true if the +Resource+ has not been persisted to the database,
|
12
|
+
# false if it has. This is determined by the variable @new_record
|
13
|
+
# and NOT if the object has an id.
|
14
|
+
#
|
15
|
+
# @example Is the resource new?
|
16
|
+
# person.new_record?
|
17
|
+
#
|
18
|
+
# @return [ true, false ] True if new, false if not.
|
19
|
+
def new_record?
|
20
|
+
@new_record ||= false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Checks if the resource has been saved to the database. Returns false
|
24
|
+
# if the document has been destroyed.
|
25
|
+
#
|
26
|
+
# @example Is the resource persisted?
|
27
|
+
# person.persisted?
|
28
|
+
#
|
29
|
+
# @return [ true, false ] True if persisted, false if not.
|
30
|
+
def persisted?
|
31
|
+
!new_record? && !destroyed?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns true if the +Resource+ has been succesfully destroyed, and false
|
35
|
+
# if it hasn't. This is determined by the variable @destroyed and NOT
|
36
|
+
# by checking the database.
|
37
|
+
#
|
38
|
+
# @example Is the resource destroyed?
|
39
|
+
# person.destroyed?
|
40
|
+
#
|
41
|
+
# @return [ true, false ] True if destroyed, false if not.
|
42
|
+
def destroyed?
|
43
|
+
@destroyed ||= false
|
44
|
+
end
|
45
|
+
alias :deleted? :destroyed?
|
46
|
+
|
47
|
+
end
|
data/lib/tripod/version.rb
CHANGED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
3
|
+
|
4
|
+
MODELS = File.join(File.dirname(__FILE__), "app/models")
|
5
|
+
$LOAD_PATH.unshift(MODELS)
|
6
|
+
|
7
|
+
require 'tripod'
|
8
|
+
require 'rspec'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.mock_with :rspec
|
12
|
+
|
13
|
+
config.before(:each) do
|
14
|
+
# delete from all named graphs.
|
15
|
+
Tripod::SparqlClient::Update.update('
|
16
|
+
# delete from default graph:
|
17
|
+
DELETE {?s ?p ?o} WHERE {?s ?p ?o};
|
18
|
+
# delete from named graphs:
|
19
|
+
DELETE {graph ?g {?s ?p ?o}} WHERE {graph ?g {?s ?p ?o}};
|
20
|
+
')
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
# configure any settings for testing...
|
26
|
+
Tripod.configure do |config|
|
27
|
+
config.update_endpoint = 'http://127.0.0.1:3030/tripod-test/update'
|
28
|
+
config.query_endpoint = 'http://127.0.0.1:3030/tripod-test/sparql'
|
29
|
+
end
|
30
|
+
|
31
|
+
# Autoload every model for the test suite that sits in spec/app/models.
|
32
|
+
Dir[ File.join(MODELS, "*.rb") ].sort.each do |file|
|
33
|
+
name = File.basename(file, ".rb")
|
34
|
+
autoload name.camelize.to_sym, name
|
35
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Tripod::Attributes do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@uri = 'http://ric'
|
7
|
+
@graph = RDF::Graph.new
|
8
|
+
|
9
|
+
stmt = RDF::Statement.new
|
10
|
+
stmt.subject = RDF::URI.new(@uri)
|
11
|
+
stmt.predicate = RDF::URI.new('http://blog')
|
12
|
+
stmt.object = RDF::URI.new('http://blog1')
|
13
|
+
@graph << stmt
|
14
|
+
|
15
|
+
stmt2 = RDF::Statement.new
|
16
|
+
stmt2.subject = RDF::URI.new(@uri)
|
17
|
+
stmt2.predicate = RDF::URI.new('http://blog')
|
18
|
+
stmt2.object = RDF::URI.new('http://blog2')
|
19
|
+
@graph << stmt2
|
20
|
+
|
21
|
+
stmt3 = RDF::Statement.new
|
22
|
+
stmt3.subject = RDF::URI.new(@uri)
|
23
|
+
stmt3.predicate = RDF::URI.new('http://name')
|
24
|
+
stmt3.object = "ric"
|
25
|
+
@graph << stmt3
|
26
|
+
end
|
27
|
+
|
28
|
+
let(:person) do
|
29
|
+
p = Person.new(@uri)
|
30
|
+
p.hydrate!(@graph)
|
31
|
+
p
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#[]" do
|
35
|
+
it 'returns the values where the predicate matches' do
|
36
|
+
values = person['http://blog']
|
37
|
+
values.length.should == 2
|
38
|
+
values.first.should == RDF::URI('http://blog1')
|
39
|
+
values[1].should == RDF::URI('http://blog2')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#read_attribute" do
|
44
|
+
it 'returns the values where the predicate matches' do
|
45
|
+
values = person.read_attribute('http://blog')
|
46
|
+
values.length.should == 2
|
47
|
+
values.first.should == RDF::URI('http://blog1')
|
48
|
+
values[1].should == RDF::URI('http://blog2')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#[]=' do
|
53
|
+
|
54
|
+
context 'single term passed' do
|
55
|
+
it 'replaces the values where the predicate matches' do
|
56
|
+
person['http://name'] = 'richard'
|
57
|
+
person['http://name'].should == [RDF::Literal.new('richard')]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'multiple terms passed' do
|
62
|
+
it 'replaces the values where the predicate matches' do
|
63
|
+
person['http://name'] = ['richard', 'ric', 'ricardo']
|
64
|
+
person['http://name'].should == [RDF::Literal.new('richard'), RDF::Literal.new('ric'), RDF::Literal.new('ricardo')]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#write_attribute' do
|
70
|
+
|
71
|
+
context 'single term passed' do
|
72
|
+
it 'replaces the values where the predicate matches' do
|
73
|
+
person.write_attribute('http://name', 'richard')
|
74
|
+
person['http://name'].should == [RDF::Literal.new('richard')]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'multiple terms passed' do
|
79
|
+
it 'replaces the values where the predicate matches' do
|
80
|
+
person.write_attribute('http://name', ['richard', 'ric', 'ricardo'])
|
81
|
+
person['http://name'].should == [RDF::Literal.new('richard'), RDF::Literal.new('ric'), RDF::Literal.new('ricardo')]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#remove_attribute' do
|
88
|
+
it 'remnoves the values where the predicate matches' do
|
89
|
+
person.remove_attribute('http://blog')
|
90
|
+
person['http://blog'].should be_empty
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "append_to_attribute" do
|
95
|
+
|
96
|
+
it 'appends values to the existing values for the predicate' do
|
97
|
+
person.append_to_attribute('http://name', 'rico')
|
98
|
+
person['http://name'].should == [RDF::Literal.new('ric'), RDF::Literal.new('rico')]
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Tripod::Finders do
|
4
|
+
|
5
|
+
let(:ric) do
|
6
|
+
@ric_uri = 'http://ric'
|
7
|
+
stmts = RDF::Graph.new
|
8
|
+
|
9
|
+
stmt = RDF::Statement.new
|
10
|
+
stmt.subject = RDF::URI.new(@ric_uri)
|
11
|
+
stmt.predicate = RDF::URI.new('http://name')
|
12
|
+
stmt.object = "ric"
|
13
|
+
stmts << stmt
|
14
|
+
|
15
|
+
stmt = RDF::Statement.new
|
16
|
+
stmt.subject = RDF::URI.new(@ric_uri)
|
17
|
+
stmt.predicate = RDF::URI.new('http://knows')
|
18
|
+
stmt.object = RDF::URI.new('http://bill')
|
19
|
+
stmts << stmt
|
20
|
+
|
21
|
+
r = Person.new(@ric_uri, 'http://people')
|
22
|
+
r.hydrate!(stmts)
|
23
|
+
r.save
|
24
|
+
r
|
25
|
+
end
|
26
|
+
|
27
|
+
let(:bill) do
|
28
|
+
@bill_uri = 'http://bill'
|
29
|
+
stmts = RDF::Graph.new
|
30
|
+
stmt = RDF::Statement.new
|
31
|
+
stmt.subject = RDF::URI.new(@bill_uri)
|
32
|
+
stmt.predicate = RDF::URI.new('http://name')
|
33
|
+
stmt.object = "bill"
|
34
|
+
stmts << stmt
|
35
|
+
b = Person.new(@bill_uri, 'http://people')
|
36
|
+
b.hydrate!(stmts)
|
37
|
+
b.save
|
38
|
+
b
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '.find' do
|
42
|
+
|
43
|
+
context 'when record exists' do
|
44
|
+
|
45
|
+
it 'does not error' do
|
46
|
+
r = Person.find(ric.uri)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'hydrates and return an object' do
|
50
|
+
r = Person.find(ric.uri)
|
51
|
+
r['http://name'].should == [RDF::Literal.new("ric")]
|
52
|
+
r['http://knows'].should == [RDF::URI.new('http://bill')]
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'sets the graph on the instantiated object' do
|
56
|
+
r = Person.find(ric.uri)
|
57
|
+
r.graph_uri.should_not be_nil
|
58
|
+
r.graph_uri.should == RDF::URI("http://people")
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'when record does not exist' do
|
64
|
+
it 'raises not found' do
|
65
|
+
lambda { Person.find('http://nonexistant') }.should raise_error(Tripod::Errors::ResourceNotFound)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|