spira 0.0.1.pre → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README 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