spira 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -0
- data/README +271 -0
- data/UNLICENSE +24 -0
- data/VERSION +1 -0
- data/lib/spira.rb +40 -0
- data/lib/spira/resource.rb +26 -0
- data/lib/spira/resource/class_methods.rb +87 -0
- data/lib/spira/resource/dsl.rb +164 -0
- data/lib/spira/resource/instance_methods.rb +175 -0
- data/lib/spira/resource/validations.rb +23 -0
- data/lib/spira/type.rb +0 -0
- data/lib/spira/types/boolean.rb +0 -0
- metadata +162 -0
data/AUTHORS
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
* Ben Lavender <blavender@gmail.com>
|
data/README
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
# Spira
|
2
|
+
|
3
|
+
It's time to breathe life into your linked data.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## Synopsis
|
8
|
+
Spira is a framework for using the information in RDF.rb repositories as model
|
9
|
+
objects. It gives you the ability to work in a resource-oriented way without
|
10
|
+
losing access to statement-oriented nature of linked data, if you so choose.
|
11
|
+
It can be used either to access existing RDF data in a resource-oriented way,
|
12
|
+
or to create a new store of RDF data based on simple defaults.
|
13
|
+
|
14
|
+
### Example
|
15
|
+
|
16
|
+
class Person
|
17
|
+
|
18
|
+
include Spira::Resource
|
19
|
+
|
20
|
+
base_uri "http://example.org/example/people"
|
21
|
+
|
22
|
+
property :name, :predicate => RDF::FOAF.name, :type => String
|
23
|
+
property :age, :predicate => RDF::FOAF.age, :type => Integer
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
bob = Person.create 'bob'
|
28
|
+
bob.age = 15
|
29
|
+
bob.name = "Bob Smith"
|
30
|
+
bob.save!
|
31
|
+
|
32
|
+
bob.each_statement {|s| puts s}
|
33
|
+
#=> RDF::Statement:0x80abb80c(<http://example.org/example/people/bob> <http://xmlns.com/foaf/0.1/name> "Bob Smith" .)
|
34
|
+
#=> RDF::Statement:0x80abb8fc(<http://example.org/example/people/bob> <http://xmlns.com/foaf/0.1/age> "15"^^<http://www.w3.org/2001/XMLSchema#integer> .)
|
35
|
+
|
36
|
+
### Features
|
37
|
+
|
38
|
+
* Extensible validations system
|
39
|
+
* Easy to use multiple data sources
|
40
|
+
* Easy to adapt models to existing data
|
41
|
+
* Objects are still RDF.rb-compatible enumerable objects
|
42
|
+
* No need to put everything about an object into Spira
|
43
|
+
* Easy to use a resource as multiple models
|
44
|
+
|
45
|
+
## Getting Started
|
46
|
+
|
47
|
+
By far the easiest way to work with Spira is to install it via Rubygems:
|
48
|
+
|
49
|
+
$ sudo gem install spira
|
50
|
+
|
51
|
+
Nonetheless, downloads are available at the Github project page.
|
52
|
+
|
53
|
+
## Defining Model Classes
|
54
|
+
|
55
|
+
To use Spira, define model classes for your RDF data. Spira classes include
|
56
|
+
RDF, and thus have access to all `RDF::Vocabulary` classes and `RDF::URI`
|
57
|
+
without the `RDF::` prefix. For example:
|
58
|
+
|
59
|
+
require 'spira'
|
60
|
+
|
61
|
+
class CD
|
62
|
+
include Spira::Resource
|
63
|
+
base_uri 'http://example.org/cds'
|
64
|
+
property :name, :predicate => DC.title, :type => XSD.string
|
65
|
+
property :artist, :predicate => URI.new('http://example.org/vocab/artist'), :type => :artist
|
66
|
+
end
|
67
|
+
|
68
|
+
class Artist
|
69
|
+
include Spira::Resource
|
70
|
+
base_uri 'http://example.org/artists'
|
71
|
+
property :name, :predicate => DC.title, :type => XSD.string
|
72
|
+
has_many :cds, :predicate => URI.new('http://example.org/vocab/published_cd'), :type => XSD.string
|
73
|
+
end
|
74
|
+
|
75
|
+
Then use your model classes, in a way more or less similar to any number of ORMs:
|
76
|
+
|
77
|
+
cd = CD.create "queens-greatest-hits"
|
78
|
+
cd.name = "Queen's greatest hits"
|
79
|
+
artist = Artist.create "queen"
|
80
|
+
artist.name = "Queen"
|
81
|
+
|
82
|
+
cd.artist = artist
|
83
|
+
cd.save!
|
84
|
+
artist.cds = [cd]
|
85
|
+
artist.save!
|
86
|
+
|
87
|
+
queen = Arist.find 'queen'
|
88
|
+
hits = CD.find 'queens-greatest-hits'
|
89
|
+
hits.artist == artist == queen
|
90
|
+
|
91
|
+
### Absolute and Relative URIs
|
92
|
+
|
93
|
+
A class with a base URI can reference objects by a short name:
|
94
|
+
|
95
|
+
Artist.find 'queen'
|
96
|
+
|
97
|
+
However, a class is not required to have a base URI, and even if it does, it
|
98
|
+
can always access classes with a full URI:
|
99
|
+
|
100
|
+
nk = Artist.find RDF::URI.new('http://example.org/my-hidden-cds/new-kids')
|
101
|
+
|
102
|
+
### Class Options
|
103
|
+
|
104
|
+
A number of options are available for Spira classes.
|
105
|
+
|
106
|
+
#### base_uri
|
107
|
+
|
108
|
+
A class with a `base_uri` set (either an `RDF::URI` or a `String`) will
|
109
|
+
use that URI as a base URI for non-absolute `create` and `find` calls.
|
110
|
+
|
111
|
+
Example
|
112
|
+
CD.find 'queens-greatest-hits' # is the same as...
|
113
|
+
CD.find RDF::URI.new('http://example.org/cds/queens-greatest-hits')
|
114
|
+
|
115
|
+
#### type
|
116
|
+
|
117
|
+
A class with a `type` set is assigned an `RDF.type` on creation and saving.
|
118
|
+
|
119
|
+
class Album
|
120
|
+
include Spira::Resource
|
121
|
+
type RDF::URI.new('http://example.org/types/album')
|
122
|
+
property :name, :predicate => DC.title
|
123
|
+
end
|
124
|
+
|
125
|
+
rolling_stones = Album.create RDF::URI.new('http://example.org/cds/rolling-stones-hits')
|
126
|
+
# See RDF.rb for more information about #has_predicate?
|
127
|
+
rolling_stones.has_predicate?(RDF.type) #=> true
|
128
|
+
Album.type #=> RDF::URI('http://example.org/types/album')
|
129
|
+
|
130
|
+
In addition, one can count the members of a class with a `type` defined:
|
131
|
+
|
132
|
+
Album.count #=> 1
|
133
|
+
|
134
|
+
#### property
|
135
|
+
|
136
|
+
A class declares property members with the `property` function. See `Property Options` for more information.
|
137
|
+
|
138
|
+
#### has_many
|
139
|
+
|
140
|
+
A class declares list members with the `has_many` function. See `Property Options` for more information.
|
141
|
+
|
142
|
+
#### default_vocabulary
|
143
|
+
|
144
|
+
A class with a `default_vocabulary` set will transparently create predicates for defined properties:
|
145
|
+
|
146
|
+
class Song
|
147
|
+
include Spira::Resource
|
148
|
+
default_vocabulary RDF::URI.new('http://example.org/vocab')
|
149
|
+
base_uri 'http://example.org/songs'
|
150
|
+
property :title
|
151
|
+
property :author, :type => :artist
|
152
|
+
end
|
153
|
+
|
154
|
+
dancing_queen = Song.create 'dancing-queen'
|
155
|
+
dancing_queen.title = "Dancing Queen"
|
156
|
+
dancing_queen.artist = abba
|
157
|
+
dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/title')) #=> true
|
158
|
+
dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/artist')) #=> true
|
159
|
+
|
160
|
+
#### default_source
|
161
|
+
|
162
|
+
Provides this class with a default repository to use instead of the `:default`
|
163
|
+
repository if one is not set.
|
164
|
+
|
165
|
+
class Song
|
166
|
+
default_source :songs
|
167
|
+
end
|
168
|
+
|
169
|
+
See 'Defining Repositories' for more information.
|
170
|
+
|
171
|
+
### Property Options
|
172
|
+
|
173
|
+
Spira classes can have properties that are either singular or a list. For a
|
174
|
+
list, define the property with `has_many`, for a property with a single item,
|
175
|
+
use `property`. The semantics are otherwise the same. A `has_many` property
|
176
|
+
will always return a list, including an empty list for no value. All options
|
177
|
+
for `property` work for `has_many`.
|
178
|
+
|
179
|
+
property :artist, :type => :artist #=> cd.artist returns a single value
|
180
|
+
has_many :cds, :type => :cd #=> artist.cds returns an array
|
181
|
+
|
182
|
+
Property always takes a symbol name as a name, and a variable list of options. The supported options are:
|
183
|
+
|
184
|
+
* `:type`: The type for this property. This can be a Ruby base class, an
|
185
|
+
RDF::XSD entry, or another Spira model class, referenced as a symbol. Default: `String`
|
186
|
+
* `:predicate`: The predicate to use for this type. This can be any RDF URI.
|
187
|
+
This option is required unless the `default_vocabulary` has been used.
|
188
|
+
|
189
|
+
### Relations
|
190
|
+
|
191
|
+
If the `:type` of a spira class is the name of another Spira class as a symbol,
|
192
|
+
such as `:artist` for `Artist`, Spira will attempt to load the referenced
|
193
|
+
object when the appropriate property is accessed.
|
194
|
+
|
195
|
+
In the RDF store, this will be represented by the URI of the referenced object.
|
196
|
+
|
197
|
+
## Defining Repositories
|
198
|
+
|
199
|
+
You can define multiple repositories with Spira, and use more than one at a time:
|
200
|
+
|
201
|
+
require 'rdf/ntriples'
|
202
|
+
require 'rdf/sesame'
|
203
|
+
Spira.add_repository! :cds, RDF::Sesame::Repository.new 'some_server'
|
204
|
+
Spira.add_repository! :albums, RDF::Repository.load('some_file.nt')
|
205
|
+
|
206
|
+
CD.repository = :cds
|
207
|
+
Album.repository = :albums
|
208
|
+
|
209
|
+
Objects can reference each other cross-repository.
|
210
|
+
|
211
|
+
If no repository has been specified, the `:default` repository will be used.
|
212
|
+
|
213
|
+
repo = RDF::Repository.new
|
214
|
+
Spira.add_repository! :default, repo
|
215
|
+
Artist.repository == repo #=> true
|
216
|
+
|
217
|
+
Classes can specify a default repository to use other than `:default` with the
|
218
|
+
`default_source` function:
|
219
|
+
|
220
|
+
class Song
|
221
|
+
default_source :songs
|
222
|
+
end
|
223
|
+
|
224
|
+
Song.repository #=> nil, won't use :default
|
225
|
+
|
226
|
+
## Validations
|
227
|
+
|
228
|
+
Before saving, each object will run a `validate` function, if one exists. You
|
229
|
+
can use the built in `assert` and assert helpers such as `assert_set` and
|
230
|
+
`asssert_numeric`.
|
231
|
+
|
232
|
+
|
233
|
+
class CD
|
234
|
+
def validate
|
235
|
+
# the only valid CDs are ABBA CD's!
|
236
|
+
assert(artist.name == "Abba","Could not save a CD made by #{artist.name}")
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
dancing-queen.artist = nil
|
241
|
+
dancing-queen.save! #=> ValidationError
|
242
|
+
|
243
|
+
dancing-queen.artist = abba
|
244
|
+
dancing-queen.save! #=> true
|
245
|
+
|
246
|
+
## Using Model Objects as RDF.rb Objects
|
247
|
+
|
248
|
+
All model objects are fully-functional as `RDF::Enumerable`, `RDF::Queryable`,
|
249
|
+
and `RDF::Mutable`. This lets you manipulate objects on the RDF statement
|
250
|
+
level. You can also access attributes that are not defined as properties.
|
251
|
+
|
252
|
+
## Support
|
253
|
+
|
254
|
+
There are a number of ways to ask for help. In declining order of likelihood of response:
|
255
|
+
|
256
|
+
* Fork the project and write a failing test, or a pending test for a feature request
|
257
|
+
* You can post issues to the Github issue queue
|
258
|
+
* (there might one day be a google group or other such support channel, but not yet)
|
259
|
+
|
260
|
+
## Authors, Development, and License
|
261
|
+
|
262
|
+
#### Authors
|
263
|
+
* Ben Lavender <blavender@gmail.com>
|
264
|
+
|
265
|
+
#### 'License'
|
266
|
+
Spira is free and unemcumbered software released into the public
|
267
|
+
domain. For more information, see the included UNLICENSE file.
|
268
|
+
|
269
|
+
#### Contributing
|
270
|
+
Fork it on Github and go. Please make sure you're kosher with the UNLICENSE
|
271
|
+
file before contributing.
|
data/UNLICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
2
|
+
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
4
|
+
distribute this software, either in source code form or as a compiled
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
6
|
+
means.
|
7
|
+
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
9
|
+
of this software dedicate any and all copyright interest in the
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
11
|
+
of the public at large and to the detriment of our heirs and
|
12
|
+
successors. We intend this dedication to be an overt act of
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
14
|
+
software under copyright law.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
For more information, please refer to <http://unlicense.org/>
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1.pre
|
data/lib/spira.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rdf'
|
2
|
+
|
3
|
+
module Spira
|
4
|
+
|
5
|
+
|
6
|
+
def repositories
|
7
|
+
settings[:repositories] ||= {}
|
8
|
+
end
|
9
|
+
module_function :repositories
|
10
|
+
|
11
|
+
def settings
|
12
|
+
Thread.current[:spira] ||= {}
|
13
|
+
end
|
14
|
+
module_function :settings
|
15
|
+
|
16
|
+
def add_repository(name, klass, *args)
|
17
|
+
repositories[name] = case klass
|
18
|
+
when RDF::Repository
|
19
|
+
klass
|
20
|
+
when Class
|
21
|
+
klass.new(*args)
|
22
|
+
else
|
23
|
+
raise ArgumentError, "Could not add repository #{klass} as #{name}; expected an RDF::Repository or class name"
|
24
|
+
end
|
25
|
+
if (name == :default) && settings[:repositories][name].nil?
|
26
|
+
warn "WARNING: Adding nil default repository"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
alias_method :add_repository!, :add_repository
|
30
|
+
module_function :add_repository, :add_repository!
|
31
|
+
|
32
|
+
def repository(name)
|
33
|
+
repositories[name]
|
34
|
+
end
|
35
|
+
module_function :repository
|
36
|
+
|
37
|
+
autoload :Resource, 'spira/resource'
|
38
|
+
|
39
|
+
class ValidationError < StandardError; end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Spira
|
2
|
+
module Resource
|
3
|
+
|
4
|
+
autoload :DSL, 'spira/resource/dsl'
|
5
|
+
autoload :ClassMethods, 'spira/resource/class_methods'
|
6
|
+
autoload :InstanceMethods, 'spira/resource/instance_methods'
|
7
|
+
autoload :Validations, 'spira/resource/validations'
|
8
|
+
|
9
|
+
def self.included(child)
|
10
|
+
child.extend DSL
|
11
|
+
child.extend ClassMethods
|
12
|
+
child.instance_eval do
|
13
|
+
class << self
|
14
|
+
attr_accessor :properties, :lists
|
15
|
+
end
|
16
|
+
@properties = {}
|
17
|
+
@lists = {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# This lets including classes reference vocabularies without the RDF:: prefix
|
22
|
+
include RDF
|
23
|
+
include InstanceMethods
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Spira
|
2
|
+
module Resource
|
3
|
+
|
4
|
+
# This module contains all class methods available to a Spira::Resource class
|
5
|
+
#
|
6
|
+
#
|
7
|
+
module ClassMethods
|
8
|
+
def repository=(repo)
|
9
|
+
@repository = repo
|
10
|
+
end
|
11
|
+
|
12
|
+
def repository
|
13
|
+
case @repository_name
|
14
|
+
when nil
|
15
|
+
Spira.repository(:default)
|
16
|
+
else
|
17
|
+
Spira.repository(@repository_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def oldrepo
|
22
|
+
case
|
23
|
+
#when !@repository.nil?
|
24
|
+
# @repository
|
25
|
+
when !@repository_name.nil?
|
26
|
+
Spira.repository(@repository_name) || raise(RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it.")
|
27
|
+
#@repository = Spira.repository(@repository_name)
|
28
|
+
#if @repository.nil?
|
29
|
+
# raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
|
30
|
+
#end
|
31
|
+
#@repository
|
32
|
+
else
|
33
|
+
@repository = Spira.repository(:default)
|
34
|
+
if @repository.nil?
|
35
|
+
raise RuntimeError, "#{self} has no configured repository and was unable to find a default repository."
|
36
|
+
end
|
37
|
+
@repository
|
38
|
+
end
|
39
|
+
#@repository
|
40
|
+
end
|
41
|
+
|
42
|
+
def find(identifier)
|
43
|
+
if repository.nil?
|
44
|
+
raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
|
45
|
+
end
|
46
|
+
uri = case identifier
|
47
|
+
when RDF::URI
|
48
|
+
identifier
|
49
|
+
when String
|
50
|
+
raise ArgumentError, "Cannot find #{self} by String without base_uri; RDF::URI required" if self.base_uri.nil?
|
51
|
+
separator = self.base_uri.to_s[-1,1] == "/" ? '' : '/'
|
52
|
+
RDF::URI.parse(self.base_uri.to_s + separator + identifier)
|
53
|
+
else
|
54
|
+
raise ArgumentError, "Cannot instantiate #{self} from #{identifier}, expected RDF::URI or String"
|
55
|
+
end
|
56
|
+
statements = self.repository.query(:subject => uri)
|
57
|
+
if statements.empty?
|
58
|
+
nil
|
59
|
+
else
|
60
|
+
self.new(identifier, :statements => statements)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def count
|
65
|
+
raise TypeError, "Cannot count a #{self} without a reference type URI." if @type.nil?
|
66
|
+
result = repository.query(:predicate => RDF.type, :object => @type)
|
67
|
+
result.count
|
68
|
+
end
|
69
|
+
|
70
|
+
def create(name, attributes = {})
|
71
|
+
# TODO: validate attributes
|
72
|
+
unless @type.nil?
|
73
|
+
if attributes[:type]
|
74
|
+
raise TypeError, "Cannot assign type to new instance of #{self}; this class is associated with #{@type}"
|
75
|
+
end
|
76
|
+
attributes[:type] = @type
|
77
|
+
end
|
78
|
+
resource = self.new(name, attributes)
|
79
|
+
end
|
80
|
+
|
81
|
+
def is_list?(property)
|
82
|
+
@lists.has_key?(property)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'promise'
|
2
|
+
|
3
|
+
module Spira
|
4
|
+
module Resource
|
5
|
+
|
6
|
+
# This module contains all user-exposed methods for use in building a model class.
|
7
|
+
# It is used to extend classes that include Spira::Resource.
|
8
|
+
# @see a little bit of magic in Spira::Resource#included as well--some
|
9
|
+
# tricks need class_eval before this module is included.
|
10
|
+
# @see Spira::Resource::ClassMethods for class methods available after class
|
11
|
+
# definition
|
12
|
+
# @see Spira::Resource::InstanceMethods for instance methods available after
|
13
|
+
# class definition
|
14
|
+
module DSL
|
15
|
+
|
16
|
+
def default_source(name)
|
17
|
+
@repository_name = name
|
18
|
+
@repository = Spira.repository(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def base_uri(uri = nil)
|
22
|
+
@base_uri = uri unless uri.nil?
|
23
|
+
@base_uri
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_vocabulary(uri)
|
27
|
+
@default_vocabulary = uri
|
28
|
+
end
|
29
|
+
|
30
|
+
def property(name, opts = {} )
|
31
|
+
add_accessors(name,opts,:hash_accessors)
|
32
|
+
end
|
33
|
+
|
34
|
+
def has_many(name, opts = {})
|
35
|
+
add_accessors(name,opts,:hash_accessors)
|
36
|
+
@lists[name] = true
|
37
|
+
end
|
38
|
+
|
39
|
+
def type(uri = nil)
|
40
|
+
unless uri.nil?
|
41
|
+
@type = case uri
|
42
|
+
when RDF::URI
|
43
|
+
uri
|
44
|
+
else
|
45
|
+
raise TypeError, "Cannot assign type #{uri} (of type #{uri.class}) to #{self}, expected RDF::URI"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
@type
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# @private
|
53
|
+
def build_value(statement, type)
|
54
|
+
case
|
55
|
+
when statement == nil
|
56
|
+
nil
|
57
|
+
when type == String
|
58
|
+
statement.object.object.to_s
|
59
|
+
when type == Integer
|
60
|
+
statement.object.object
|
61
|
+
when type.is_a?(Symbol)
|
62
|
+
klass = Kernel.const_get(type.to_s.capitalize)
|
63
|
+
raise TypeError, "#{klass} is not a Spira Resource (referenced as #{type} by #{self}" unless klass.ancestors.include? Spira::Resource
|
64
|
+
promise { klass.find(statement.object) || klass.create(statement.object) }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# @private
|
69
|
+
def build_rdf_value(value, type)
|
70
|
+
case
|
71
|
+
when value.class.ancestors.include?(Spira::Resource)
|
72
|
+
value.uri
|
73
|
+
when type == nil
|
74
|
+
value
|
75
|
+
when type == RDF::URI && value.is_a?(RDF::URI)
|
76
|
+
value
|
77
|
+
when type.is_a?(RDF::URI)
|
78
|
+
RDF::Literal.new(value, :datatype => type)
|
79
|
+
else
|
80
|
+
RDF::Literal.new(value)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def add_accessors(name, opts, accessors_method)
|
87
|
+
predicate = case
|
88
|
+
when opts[:predicate]
|
89
|
+
opts[:predicate]
|
90
|
+
when @default_vocabulary.nil?
|
91
|
+
raise TypeError, "A :predicate option is required for types without a default vocabulary"
|
92
|
+
else @default_vocabulary
|
93
|
+
separator = @default_vocabulary.to_s[-1,1] == "/" ? '' : '/'
|
94
|
+
RDF::URI.new(@default_vocabulary.to_s + separator + name.to_s)
|
95
|
+
end
|
96
|
+
|
97
|
+
type = opts[:type] || String
|
98
|
+
@properties[name] = {}
|
99
|
+
@properties[name][:predicate] = predicate
|
100
|
+
@properties[name][:type] = type
|
101
|
+
name_equals = (name.to_s + "=").to_sym
|
102
|
+
|
103
|
+
(getter,setter) = self.send(accessors_method, name, predicate, type)
|
104
|
+
self.send(:define_method,name_equals, &setter)
|
105
|
+
self.send(:define_method,name, &getter)
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
def hash_accessors(name, predicate, type)
|
110
|
+
setter = lambda do |arg|
|
111
|
+
attribute_set(name,arg)
|
112
|
+
end
|
113
|
+
|
114
|
+
getter = lambda do
|
115
|
+
attribute_get(name)
|
116
|
+
end
|
117
|
+
|
118
|
+
[getter, setter]
|
119
|
+
end
|
120
|
+
|
121
|
+
def list_accessors(name, predicate, type)
|
122
|
+
|
123
|
+
setter = lambda do |arg|
|
124
|
+
old = @repo.query(:subject => @uri, :predicate => predicate)
|
125
|
+
@repo.delete(*old.to_a) unless old.empty?
|
126
|
+
new = []
|
127
|
+
arg.each do |value|
|
128
|
+
value = self.class.build_rdf_value(value, type)
|
129
|
+
new << RDF::Statement.new(@uri, predicate, value)
|
130
|
+
end
|
131
|
+
@repo.insert(*new)
|
132
|
+
end
|
133
|
+
|
134
|
+
getter = lambda do
|
135
|
+
values = []
|
136
|
+
statements = @repo.query(:subject => @uri, :predicate => predicate)
|
137
|
+
statements.each do |statement|
|
138
|
+
values << self.class.build_value(statement, type)
|
139
|
+
end
|
140
|
+
values
|
141
|
+
end
|
142
|
+
|
143
|
+
[getter, setter]
|
144
|
+
end
|
145
|
+
|
146
|
+
def single_accessors(name, predicate, type)
|
147
|
+
setter = lambda do |arg|
|
148
|
+
old = @repo.query(:subject => @uri, :predicate => predicate)
|
149
|
+
@repo.delete(*old.to_a) unless old.empty?
|
150
|
+
arg = self.class.build_rdf_value(arg, type)
|
151
|
+
@repo.insert(RDF::Statement.new(@uri, predicate, arg))
|
152
|
+
end
|
153
|
+
|
154
|
+
getter = lambda do
|
155
|
+
statement = @repo.query(:subject => @uri, :predicate => predicate).first
|
156
|
+
self.class.build_value(statement, type)
|
157
|
+
end
|
158
|
+
|
159
|
+
[getter, setter]
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'rdf/isomorphic'
|
2
|
+
|
3
|
+
module Spira
|
4
|
+
module Resource
|
5
|
+
module InstanceMethods
|
6
|
+
|
7
|
+
attr_reader :uri
|
8
|
+
|
9
|
+
# Initialize a new instance of a spira resource.
|
10
|
+
# The new instance can be instantiated with an opts[:statements] or opts[:attributes], but not both.
|
11
|
+
def initialize(identifier, opts = {})
|
12
|
+
|
13
|
+
@attributes = {}
|
14
|
+
|
15
|
+
if identifier.is_a? RDF::URI
|
16
|
+
@uri = identifier
|
17
|
+
else
|
18
|
+
if (self.class.base_uri)
|
19
|
+
separator = self.class.base_uri.to_s[-1,1] == "/" ? '' : '/'
|
20
|
+
@uri = RDF::URI.parse(self.class.base_uri.to_s + separator + identifier)
|
21
|
+
else
|
22
|
+
raise ArgumentError, "#{self.class} has no base URI configured, and can thus only be created using RDF::URIs (got #{identifier.inspect})"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#@repo = RDF::Repository.new
|
27
|
+
#@repo.insert(*(opts[:statements])) unless opts[:statements].nil?
|
28
|
+
#@repo.insert(*[RDF::Statement.new(@uri, RDF.type, opts[:type])]) if opts[:type]
|
29
|
+
|
30
|
+
# If we got statements, we are being loaded, not created
|
31
|
+
if opts[:statements]
|
32
|
+
# Set attributes for each statement corresponding to a predicate
|
33
|
+
self.class.properties.each do |name, property|
|
34
|
+
if self.class.is_list?(name)
|
35
|
+
values = []
|
36
|
+
statements = opts[:statements].query(:subject => @uri, :predicate => property[:predicate])
|
37
|
+
unless statements.nil?
|
38
|
+
statements.each do |statement|
|
39
|
+
values << self.class.build_value(statement,property[:type])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
attribute_set(name, values)
|
43
|
+
else
|
44
|
+
statement = opts[:statements].query(:subject => @uri, :predicate => property[:predicate]).first
|
45
|
+
attribute_set(name, self.class.build_value(statement, property[:type]))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
else
|
49
|
+
self.class.properties.each do |name, predicate|
|
50
|
+
attribute_set(name, opts[name]) unless opts[name].nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
#@repo = RDF::Repository.new
|
57
|
+
#@repo.insert(*(opts[:statements])) unless opts[:statements].nil?
|
58
|
+
#@repo.insert(*[RDF::Statement.new(@uri, RDF.type, opts[:type])]) if opts[:type]
|
59
|
+
|
60
|
+
#self.class.properties.each do |name, predicate|
|
61
|
+
# send(((name.to_s)+"=").to_sym, opts[name]) unless opts[name].nil?
|
62
|
+
#end
|
63
|
+
@original_attributes = @attributes.dup
|
64
|
+
@original_attributes.each do | name, value |
|
65
|
+
@original_attributes[name] = value.dup if value.is_a?(Array)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def _destroy_attributes(attributes)
|
70
|
+
repository = repository_for_attributes(attributes)
|
71
|
+
self.class.repository.delete(*repository)
|
72
|
+
end
|
73
|
+
|
74
|
+
def destroy!
|
75
|
+
_destroy_attributes(@attributes)
|
76
|
+
end
|
77
|
+
|
78
|
+
def save!
|
79
|
+
if self.class.repository.nil?
|
80
|
+
raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
|
81
|
+
end
|
82
|
+
if respond_to?(:validate)
|
83
|
+
errors.clear
|
84
|
+
validate
|
85
|
+
if errors.empty?
|
86
|
+
_update!
|
87
|
+
else
|
88
|
+
raise(ValidationError, "Could not save #{self.inspect} due to validation errors: " + errors.join(';'))
|
89
|
+
end
|
90
|
+
else
|
91
|
+
_update!
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def _update!
|
96
|
+
_destroy_attributes(@original_attributes)
|
97
|
+
self.class.repository.insert(*self)
|
98
|
+
@original_attributes = @attributes.dup
|
99
|
+
end
|
100
|
+
|
101
|
+
def type
|
102
|
+
self.class.type
|
103
|
+
end
|
104
|
+
|
105
|
+
def type=(type)
|
106
|
+
raise TypeError, "Cannot reassign RDF.type for #{self}; consider appending to a has_many :types"
|
107
|
+
end
|
108
|
+
|
109
|
+
def inspect
|
110
|
+
"<#{self.class}:#{self.object_id} uri: #{@uri}>"
|
111
|
+
end
|
112
|
+
|
113
|
+
def each(*args, &block)
|
114
|
+
repository = repository_for_attributes(@attributes)
|
115
|
+
repository.insert(RDF::Statement.new(@uri, RDF.type, type)) unless type.nil?
|
116
|
+
if block_given?
|
117
|
+
repository.each(*args, &block)
|
118
|
+
else
|
119
|
+
::Enumerable::Enumerator.new(self, :each)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def attribute_set(name, value)
|
124
|
+
@attributes[name] = value
|
125
|
+
end
|
126
|
+
|
127
|
+
def attribute_get(name)
|
128
|
+
case self.class.is_list?(name)
|
129
|
+
when true
|
130
|
+
@attributes[name] ||= []
|
131
|
+
when false
|
132
|
+
@attributes[name]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def repository_for_attributes(attributes)
|
137
|
+
repo = RDF::Repository.new
|
138
|
+
attributes.each do | name, attribute |
|
139
|
+
if self.class.is_list?(name)
|
140
|
+
#old = @repo.query(:subject => @uri, :predicate => predicate)
|
141
|
+
#@repo.delete(*old.to_a) unless old.empty?
|
142
|
+
new = []
|
143
|
+
attribute.each do |value|
|
144
|
+
value = self.class.build_rdf_value(value, self.class.properties[name][:type])
|
145
|
+
new << RDF::Statement.new(@uri, self.class.properties[name][:predicate], value)
|
146
|
+
end
|
147
|
+
repo.insert(*new)
|
148
|
+
else
|
149
|
+
value = self.class.build_rdf_value(attribute, self.class.properties[name][:type])
|
150
|
+
repo.insert(RDF::Statement.new(@uri, self.class.properties[name][:predicate], value))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
repo
|
154
|
+
end
|
155
|
+
|
156
|
+
def ==(other)
|
157
|
+
case other
|
158
|
+
# TODO: define behavior for equality on subclasses. also subclasses.
|
159
|
+
when self.class
|
160
|
+
@uri == other.uri
|
161
|
+
when RDF::Enumerable
|
162
|
+
self.isomorphic_with?(other)
|
163
|
+
else
|
164
|
+
false
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
include ::RDF::Enumerable, ::RDF::Queryable
|
170
|
+
|
171
|
+
include Spira::Resource::Validations
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Spira
|
2
|
+
module Resource
|
3
|
+
module Validations
|
4
|
+
|
5
|
+
def errors
|
6
|
+
@errors ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def assert(boolean, message)
|
10
|
+
errors.push(message) unless boolean
|
11
|
+
end
|
12
|
+
|
13
|
+
def assert_set(name)
|
14
|
+
assert(!(self.send(name).nil?), "#{name.to_s} cannot be nil")
|
15
|
+
end
|
16
|
+
|
17
|
+
def assert_numeric(name)
|
18
|
+
assert(self.send(name).is_a?(Numeric), "#{name.to_s} must be numeric (was #{self.send(name)})")
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/spira/type.rb
ADDED
File without changes
|
File without changes
|
metadata
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spira
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: true
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- pre
|
10
|
+
version: 0.0.1.pre
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ben Lavender
|
14
|
+
autorequire:
|
15
|
+
bindir:
|
16
|
+
- bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-04-17 00:00:00 +02:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: rdf-spec
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
version: 0.1.0
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 1
|
45
|
+
- 3
|
46
|
+
- 0
|
47
|
+
version: 1.3.0
|
48
|
+
type: :development
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: yard
|
52
|
+
prerelease: false
|
53
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
- 5
|
60
|
+
- 3
|
61
|
+
version: 0.5.3
|
62
|
+
type: :development
|
63
|
+
version_requirements: *id003
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: rdf
|
66
|
+
prerelease: false
|
67
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
- 1
|
74
|
+
- 0
|
75
|
+
version: 0.1.0
|
76
|
+
type: :runtime
|
77
|
+
version_requirements: *id004
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rdf-isomorphic
|
80
|
+
prerelease: false
|
81
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
- 1
|
88
|
+
- 2
|
89
|
+
version: 0.1.2
|
90
|
+
type: :runtime
|
91
|
+
version_requirements: *id005
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: promise
|
94
|
+
prerelease: false
|
95
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
- 1
|
102
|
+
- 0
|
103
|
+
version: 0.1.0
|
104
|
+
type: :runtime
|
105
|
+
version_requirements: *id006
|
106
|
+
description: Spira is a framework for using the information in RDF.rb repositories as model objects.
|
107
|
+
email: blavender@gmail.com
|
108
|
+
executables: []
|
109
|
+
|
110
|
+
extensions: []
|
111
|
+
|
112
|
+
extra_rdoc_files: []
|
113
|
+
|
114
|
+
files:
|
115
|
+
- AUTHORS
|
116
|
+
- README
|
117
|
+
- UNLICENSE
|
118
|
+
- VERSION
|
119
|
+
- lib/spira/resource/class_methods.rb
|
120
|
+
- lib/spira/resource/dsl.rb
|
121
|
+
- lib/spira/resource/instance_methods.rb
|
122
|
+
- lib/spira/resource/validations.rb
|
123
|
+
- lib/spira/resource.rb
|
124
|
+
- lib/spira/type.rb
|
125
|
+
- lib/spira/types/boolean.rb
|
126
|
+
- lib/spira.rb
|
127
|
+
has_rdoc: false
|
128
|
+
homepage: http://spira.rubyforge.org
|
129
|
+
licenses:
|
130
|
+
- Public Domain
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
segments:
|
141
|
+
- 1
|
142
|
+
- 8
|
143
|
+
- 2
|
144
|
+
version: 1.8.2
|
145
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - ">"
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
segments:
|
150
|
+
- 1
|
151
|
+
- 3
|
152
|
+
- 1
|
153
|
+
version: 1.3.1
|
154
|
+
requirements: []
|
155
|
+
|
156
|
+
rubyforge_project: spira
|
157
|
+
rubygems_version: 1.3.6
|
158
|
+
signing_key:
|
159
|
+
specification_version: 3
|
160
|
+
summary: A framework for using the information in RDF.rb repositories as model objects.
|
161
|
+
test_files: []
|
162
|
+
|