spira 0.0.1.pre → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -19,12 +19,12 @@ or to create a new store of RDF data based on simple defaults.
19
19
 
20
20
  base_uri "http://example.org/example/people"
21
21
 
22
- property :name, :predicate => RDF::FOAF.name, :type => String
23
- property :age, :predicate => RDF::FOAF.age, :type => Integer
22
+ property :name, :predicate => FOAF.name, :type => String
23
+ property :age, :predicate => FOAF.age, :type => Integer
24
24
 
25
25
  end
26
26
 
27
- bob = Person.create 'bob'
27
+ bob = RDF::URI("http://example.org/people/bob").as(Person)
28
28
  bob.age = 15
29
29
  bob.name = "Bob Smith"
30
30
  bob.save!
@@ -36,19 +36,21 @@ or to create a new store of RDF data based on simple defaults.
36
36
  ### Features
37
37
 
38
38
  * Extensible validations system
39
+ * Extensible types system
39
40
  * Easy to use multiple data sources
40
41
  * Easy to adapt models to existing data
42
+ * Open-world semantics
41
43
  * Objects are still RDF.rb-compatible enumerable objects
42
44
  * No need to put everything about an object into Spira
43
45
  * Easy to use a resource as multiple models
44
46
 
45
47
  ## Getting Started
46
48
 
47
- By far the easiest way to work with Spira is to install it via Rubygems:
49
+ The easiest way to work with Spira is to install it via Rubygems:
48
50
 
49
51
  $ sudo gem install spira
50
52
 
51
- Nonetheless, downloads are available at the Github project page.
53
+ Downloads will be available on the github project page, as well as on Rubyforge.
52
54
 
53
55
  ## Defining Model Classes
54
56
 
@@ -74,9 +76,9 @@ without the `RDF::` prefix. For example:
74
76
 
75
77
  Then use your model classes, in a way more or less similar to any number of ORMs:
76
78
 
77
- cd = CD.create "queens-greatest-hits"
79
+ cd = CD.for("queens-greatest-hits")
78
80
  cd.name = "Queen's greatest hits"
79
- artist = Artist.create "queen"
81
+ artist = Artist.for("queen")
80
82
  artist.name = "Queen"
81
83
 
82
84
  cd.artist = artist
@@ -84,21 +86,30 @@ Then use your model classes, in a way more or less similar to any number of ORMs
84
86
  artist.cds = [cd]
85
87
  artist.save!
86
88
 
87
- queen = Arist.find 'queen'
88
- hits = CD.find 'queens-greatest-hits'
89
+ queen = Arist.for('queen')
90
+ hits = CD.for 'queens-greatest-hits'
89
91
  hits.artist == artist == queen
90
92
 
91
93
  ### Absolute and Relative URIs
92
94
 
93
95
  A class with a base URI can reference objects by a short name:
94
96
 
95
- Artist.find 'queen'
97
+ Artist.for('queen')
96
98
 
97
99
  However, a class is not required to have a base URI, and even if it does, it
98
100
  can always access classes with a full URI:
99
101
 
100
- nk = Artist.find RDF::URI.new('http://example.org/my-hidden-cds/new-kids')
101
-
102
+ nk = Artist.for(RDF::URI.new('http://example.org/my-hidden-cds/new-kids'))
103
+
104
+ If you have a URI that you would like to look at as a Spira resource, you can instantiate it from the URI:
105
+
106
+ RDF::URI.new('http://example.org/my-hidden-cds/new-kids').as(Artist)
107
+ # => <Artist @uri=http://example.org/my-hidden-cds/new-kids>
108
+
109
+ Any call to 'for' with a valid identifier will always return an object with nil
110
+ fields. It's a way of looking at a given resource, not a closed-world mapping
111
+ to one.
112
+
102
113
  ### Class Options
103
114
 
104
115
  A number of options are available for Spira classes.
@@ -106,11 +117,11 @@ A number of options are available for Spira classes.
106
117
  #### base_uri
107
118
 
108
119
  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.
120
+ use that URI as a base URI for non-absolute `for` calls.
110
121
 
111
122
  Example
112
- CD.find 'queens-greatest-hits' # is the same as...
113
- CD.find RDF::URI.new('http://example.org/cds/queens-greatest-hits')
123
+ CD.for 'queens-greatest-hits' # is the same as...
124
+ CD.for RDF::URI.new('http://example.org/cds/queens-greatest-hits')
114
125
 
115
126
  #### type
116
127
 
@@ -118,12 +129,12 @@ A class with a `type` set is assigned an `RDF.type` on creation and saving.
118
129
 
119
130
  class Album
120
131
  include Spira::Resource
121
- type RDF::URI.new('http://example.org/types/album')
132
+ type URI.new('http://example.org/types/album')
122
133
  property :name, :predicate => DC.title
123
134
  end
124
135
 
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?
136
+ rolling_stones = Album.for RDF::URI.new('http://example.org/cds/rolling-stones-hits')
137
+ # See RDF.rb at http://rdf.rubyforge.org/RDF/Enumerable.html for more information about #has_predicate?
127
138
  rolling_stones.has_predicate?(RDF.type) #=> true
128
139
  Album.type #=> RDF::URI('http://example.org/types/album')
129
140
 
@@ -145,15 +156,16 @@ A class with a `default_vocabulary` set will transparently create predicates for
145
156
 
146
157
  class Song
147
158
  include Spira::Resource
148
- default_vocabulary RDF::URI.new('http://example.org/vocab')
159
+ default_vocabulary URI.new('http://example.org/vocab')
149
160
  base_uri 'http://example.org/songs'
150
161
  property :title
151
162
  property :author, :type => :artist
152
163
  end
153
164
 
154
- dancing_queen = Song.create 'dancing-queen'
165
+ dancing_queen = Song.for 'dancing-queen'
155
166
  dancing_queen.title = "Dancing Queen"
156
167
  dancing_queen.artist = abba
168
+ # See RDF::Enumerable for #has_predicate?
157
169
  dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/title')) #=> true
158
170
  dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/artist')) #=> true
159
171
 
@@ -168,6 +180,11 @@ repository if one is not set.
168
180
 
169
181
  See 'Defining Repositories' for more information.
170
182
 
183
+ #### validate
184
+
185
+ Provides the name of a function which does some sort of validation. See
186
+ 'Validations' for more information.
187
+
171
188
  ### Property Options
172
189
 
173
190
  Spira classes can have properties that are either singular or a list. For a
@@ -182,11 +199,17 @@ for `property` work for `has_many`.
182
199
  Property always takes a symbol name as a name, and a variable list of options. The supported options are:
183
200
 
184
201
  * `: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`
202
+ RDF::XSD entry, or another Spira model class, referenced as a symbol. See
203
+ **Types** below. Default: `Any`
186
204
  * `:predicate`: The predicate to use for this type. This can be any RDF URI.
187
205
  This option is required unless the `default_vocabulary` has been used.
188
206
 
189
- ### Relations
207
+ ### Types
208
+
209
+ A property's type can be either a class which includes Spira::Type or a
210
+ reference to another Spira model class, given as a symbol.
211
+
212
+ #### Relations
190
213
 
191
214
  If the `:type` of a spira class is the name of another Spira class as a symbol,
192
215
  such as `:artist` for `Artist`, Spira will attempt to load the referenced
@@ -194,6 +217,71 @@ object when the appropriate property is accessed.
194
217
 
195
218
  In the RDF store, this will be represented by the URI of the referenced object.
196
219
 
220
+ #### Type Classes
221
+
222
+ A type class includes Spira::Type, and can implement serialization and
223
+ deserialization functions, and register aliases to themselves if their datatype
224
+ is usually expressed as a URI. Here is the built-in Spira Integer class:
225
+
226
+ module Spira::Types
227
+ class Integer
228
+
229
+ include Spira::Type
230
+
231
+ def self.unserialize(value)
232
+ value.object
233
+ end
234
+
235
+ def self.serialize(value)
236
+ RDF::Literal.new(value)
237
+ end
238
+
239
+ register_alias XSD.integer
240
+ end
241
+ end
242
+
243
+ Classes can now use this particular type like so:
244
+
245
+ class Test
246
+ include Spira::Resource
247
+ property :test1, :type => Integer
248
+ property :test2, :type => XSD.integer
249
+ end
250
+
251
+ Spira classes include the Spira::Types namespace, where several default types
252
+ are implemented:
253
+
254
+ * `Integer`
255
+ * `Float`
256
+ * `Boolean`
257
+ * `String`
258
+ * `Any`
259
+
260
+ The default type for a Spira property is `Spira::Types::Any`, which uses
261
+ `RDF::Literal`'s automatic boxing/unboxing of XSD types as best it can. See
262
+ `[RDF::Literal](http://rdf.rubyforge.org/RDF/Literal.html)` for more information.
263
+
264
+ You can implement your own types as well. Your class' serialize method should
265
+ turn an RDF::Value into a ruby object, and vice versa.
266
+
267
+ module MyModule
268
+ class MyType
269
+ include Spira::Type
270
+ def self.serialize(value)
271
+ ...
272
+ end
273
+
274
+ def self.unserialize(value)
275
+ ...
276
+ end
277
+ end
278
+ end
279
+
280
+ class MyClass
281
+ include Spira::Resource
282
+ property :property1, :type => MyModule::MyType
283
+ end
284
+
197
285
  ## Defining Repositories
198
286
 
199
287
  You can define multiple repositories with Spira, and use more than one at a time:
@@ -225,24 +313,55 @@ Classes can specify a default repository to use other than `:default` with the
225
313
 
226
314
  ## Validations
227
315
 
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
316
+ You may declare any number of validation functions with the `validate` function.
317
+ Before saving, each referenced validation will be run, and the instance's
318
+ {Spira::Errors} object will be populated with any errors. You can use the
319
+ built in `assert` and assert helpers such as `assert_set` and
230
320
  `asssert_numeric`.
231
321
 
232
322
 
233
323
  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}")
324
+ validate :is_real_music
325
+ def is_real_music
326
+ assert(artist.name != "Nickelback", :artist, "cannot be Nickelback")
327
+ end
328
+
329
+ validate :track_count_numeric
330
+ def track_count_numeric
331
+ assert_numeric(track_count)
237
332
  end
238
333
  end
239
334
 
240
- dancing-queen.artist = nil
335
+ dancing-queen.artist = nickelback
241
336
  dancing-queen.save! #=> ValidationError
337
+ dancing-queen.errors.each => ["artist cannot be Nickelback"]
242
338
 
243
339
  dancing-queen.artist = abba
244
340
  dancing-queen.save! #=> true
245
341
 
342
+
343
+ ## Inheritance
344
+
345
+ You can extend Spira resources without a problem:
346
+
347
+ class BoxedSet < CD
348
+ include Spira::Resource
349
+ property cd_count, :predicate => CD.count, :type => Integer
350
+ end
351
+
352
+ You can also make Spira modules and include them into other classes:
353
+
354
+ module Media
355
+ include Spira::Resource
356
+ property :format, :predicate => Media.format
357
+ end
358
+
359
+ class CD
360
+ include Spira::Resource
361
+ include Media
362
+ end
363
+
364
+
246
365
  ## Using Model Objects as RDF.rb Objects
247
366
 
248
367
  All model objects are fully-functional as `RDF::Enumerable`, `RDF::Queryable`,
@@ -251,9 +370,10 @@ level. You can also access attributes that are not defined as properties.
251
370
 
252
371
  ## Support
253
372
 
254
- There are a number of ways to ask for help. In declining order of likelihood of response:
373
+ There are a number of ways to ask for help. In declining order of preference:
255
374
 
256
375
  * Fork the project and write a failing test, or a pending test for a feature request
376
+ * Ask on the [public-rdf-ruby w3c mailing list][]
257
377
  * You can post issues to the Github issue queue
258
378
  * (there might one day be a google group or other such support channel, but not yet)
259
379
 
@@ -269,3 +389,5 @@ domain. For more information, see the included UNLICENSE file.
269
389
  #### Contributing
270
390
  Fork it on Github and go. Please make sure you're kosher with the UNLICENSE
271
391
  file before contributing.
392
+
393
+ [public-rdf-ruby w3c mailing list]: http://lists.w3.org/Archives/Public/public-rdf-ruby/
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1.pre
1
+ 0.0.1
data/lib/spira.rb CHANGED
@@ -1,18 +1,62 @@
1
1
  require 'rdf'
2
2
 
3
+ ##
4
+ # Spira is a framework for building projections of RDF data into Ruby classes.
5
+ # It is built on top of RDF.rb.
6
+ #
7
+ # @see http://rdf.rubyforge.org
8
+ # @see http://github.com/bhuga/spira
9
+ # @see Spira::Resource
3
10
  module Spira
4
11
 
5
-
12
+ ##
13
+ # The list of repositories available for Spira resources
14
+ #
15
+ # @see http://rdf.rubyforge.org/RDF/Repository.html
16
+ # @return [Hash{Symbol => RDF::Repository}]
17
+ # @private
6
18
  def repositories
7
19
  settings[:repositories] ||= {}
8
20
  end
9
21
  module_function :repositories
10
22
 
23
+ ##
24
+ # The list of all property types available for Spira resources
25
+ #
26
+ # @see Spira::Types
27
+ # @return [Hash{Symbol => Spira::Type}]
28
+ def types
29
+ settings[:types] ||= {}
30
+ end
31
+ module_function :types
32
+
33
+ ##
34
+ # A thread-local hash for storing settings. Used by Resource classes.
35
+ #
36
+ # @see Spira::Resource
37
+ # @see Spira.repositories
38
+ # @see Spira.types
11
39
  def settings
12
40
  Thread.current[:spira] ||= {}
13
41
  end
14
42
  module_function :settings
15
43
 
44
+ ##
45
+ # Add a repository to Spira's list of repositories.
46
+ #
47
+ # @overload add_repository(name, repo)
48
+ # @param [Symbol] name The name of this repository
49
+ # @param [RDF::Repository] repo An RDF::Repository
50
+ # @overload add_repository(name, klass, *args)
51
+ # @param [Symbol] name The name of this repository
52
+ # @param [RDF::Repository, Class] repo A Class that inherits from RDF::Repository
53
+ # @param [*Object] The list of arguments to instantiate the class
54
+ # @example Adding an ntriples file as a repository
55
+ # Spira.add_repository(:default, RDF::Repository.load('http://datagraph.org/jhacker/foaf.nt'))
56
+ # @example Adding an empty repository to be instantiated on use
57
+ # Spira.add_repository(:default, RDF::Repository)
58
+ # @return [Void]
59
+ # @see RDF::Repository
16
60
  def add_repository(name, klass, *args)
17
61
  repositories[name] = case klass
18
62
  when RDF::Repository
@@ -29,12 +73,53 @@ module Spira
29
73
  alias_method :add_repository!, :add_repository
30
74
  module_function :add_repository, :add_repository!
31
75
 
76
+ ##
77
+ # The RDF::Repository for the named repository
78
+ #
79
+ # @param [Symbol] name The name of the repository
80
+ # @return [RDF::Repository]
81
+ # @see RDF::Repository
32
82
  def repository(name)
33
83
  repositories[name]
34
84
  end
35
85
  module_function :repository
36
86
 
87
+ ##
88
+ # Alias a property type to another. This allows a range of options to be
89
+ # specified for a property type which all reference one Spira::Type
90
+ #
91
+ # @param [Any] new The new symbol or reference
92
+ # @param [Any] original The type the new symbol should refer to
93
+ # @return [Void]
94
+ # @private
95
+ def type_alias(new, original)
96
+ types[new] = original
97
+ end
98
+ module_function :type_alias
99
+
37
100
  autoload :Resource, 'spira/resource'
101
+ autoload :Type, 'spira/type'
102
+ autoload :Types, 'spira/types'
103
+ autoload :Errors, 'spira/errors'
104
+ autoload :VERSION, 'spira/version'
38
105
 
39
106
  class ValidationError < StandardError; end
40
107
  end
108
+
109
+ module RDF
110
+ class URI
111
+ ##
112
+ # Create a projection of this URI as the given Spira::Resource class.
113
+ # Equivalent to `klass.for(self, *args)`
114
+ #
115
+ # @example Instantiating a URI as a Spira Resource
116
+ # RDF::URI('http://example.org/person/bob').as(Person)
117
+ # @param [Class] klass
118
+ # @param [*Any] args Any arguments to pass to klass.for
119
+ # @return [Klass] An instance of klass
120
+ def as(klass, *args)
121
+ raise ArgumentError, "#{klass} is not a Spira resource" unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Resource)
122
+ klass.for(self, *args)
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,94 @@
1
+ module Spira
2
+
3
+ ##
4
+ # Spira::Errors represents a collection of validation errors for a Spira
5
+ # resource. It tracks a list of errors for each field.
6
+ #
7
+ # This class does not perform validations. It only tracks the results of
8
+ # them.
9
+ class Errors
10
+
11
+ ##
12
+ # Creates a new Spira::Errors
13
+ #
14
+ # @return [Spira::Errors]
15
+ def initialize
16
+ @errors = {}
17
+ end
18
+
19
+ ##
20
+ # Returns true if there are no errors, false otherwise
21
+ #
22
+ # @return [true, false]
23
+ def empty?
24
+ @errors.all? do |field, errors| errors.empty? end
25
+ end
26
+
27
+ ##
28
+ # Returns true if there are errors, false otherwise
29
+ #
30
+ # @return [true, false]
31
+ def any?
32
+ !empty?
33
+ end
34
+
35
+ ##
36
+ # Returns true if the given property or list has any errors
37
+ #
38
+ # @param [Symbol] name The name of the property or list
39
+ # @return [true, false]
40
+ def any_for?(property)
41
+ !(@errors[property].nil?) && !(@errors[property].empty?)
42
+ end
43
+
44
+ ##
45
+ # Add an error to a given property or list
46
+ #
47
+ # @example Add an error to a property
48
+ # errors.add(:username, "cannot be nil")
49
+ # @param [Symbol] property The property or list to add the error to
50
+ # @param [String] problem The error
51
+ # @return [Void]
52
+ def add(property, problem)
53
+ @errors[property] ||= []
54
+ @errors[property].push problem
55
+ end
56
+
57
+ ##
58
+ # The list of errors for a given property or list
59
+ #
60
+ # @example Get the errors for the `:username` field
61
+ # errors.add(:username, "cannot be nil")
62
+ # errors.for(:username) #=> ["cannot be nil"]
63
+ # @param [Symbol] property The property or list to check
64
+ # @return [Array<String>] The list of errors
65
+ def for(property)
66
+ @errors[property]
67
+ end
68
+
69
+ ##
70
+ # Clear all errors
71
+ #
72
+ # @return [Void]
73
+ def clear
74
+ @errors = {}
75
+ end
76
+
77
+ ##
78
+ # Return all errors as strings
79
+ #
80
+ # @example Get all errors
81
+ # errors.add(:username, "cannot be nil")
82
+ # errors.each #=> ["username cannot be nil"]
83
+ # @return [Array<String>]
84
+ def each
85
+ @errors.map do |property, problems|
86
+ problems.map do |problem|
87
+ property.to_s + " " + problem
88
+ end
89
+ end.flatten
90
+ end
91
+
92
+
93
+ end
94
+ end