spira 0.0.1.pre
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/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
|
+
|