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.
@@ -2,128 +2,216 @@ require 'rdf/isomorphic'
2
2
 
3
3
  module Spira
4
4
  module Resource
5
+
6
+ ##
7
+ # This module contains instance methods for Spira resources. See
8
+ # {Spira::Resource} for more information.
9
+ #
10
+ # @see Spira::Resource
11
+ # @see Spira::Resource::ClassMethods
12
+ # @see Spira::Resource::DSL
13
+ # @see Spira::Resource::Validations
5
14
  module InstanceMethods
6
-
15
+
16
+ ##
17
+ # This instance's URI.
18
+ #
19
+ # @return [RDF::URI]
7
20
  attr_reader :uri
8
21
 
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.
22
+ ##
23
+ # Initialize a new Spira::Resource instance of this resource class. This
24
+ # method should not be called directly, use
25
+ # {Spira::Resource::ClassMethods#for} instead.
26
+ #
27
+ # @param [Any] identifier The URI or URI fragment for this instance
28
+ # @param [Hash] opts Default attributes for this instance
29
+ # @see Spira::Resource::ClassMethods#for
11
30
  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
31
+ @uri = self.class.uri_for(identifier)
32
+ reload(opts)
33
+ end
34
+
35
+ ##
36
+ # Reload all attributes for this instance, overwriting or setting
37
+ # defaults with the given opts. This resource will block if the
38
+ # underlying repository blocks the next time it accesses attributes.
39
+ #
40
+ # @param [Hash{Symbol => Any}] opts
41
+ # @option opts [Symbol] :any A property name. Sets the given property to the given value.
42
+ def reload(opts = {})
43
+ @attributes = promise { reload_attributes }
44
+ @original_attributes = promise { @attributes.force ; @original_attributes }
45
+ self.class.properties.each do |name, predicate|
46
+ attribute_set(name, opts[name]) unless opts[name].nil?
24
47
  end
48
+ end
25
49
 
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]
50
+ ##
51
+ # Load this instance's attributes. Overwrite loaded values with attributes in the given options.
52
+ #
53
+ # @param [Hash] opts
54
+ # @return [Hash] @attributes
55
+ # @private
56
+ def reload_attributes()
57
+ if self.class.repository.nil?
58
+ raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
59
+ end
60
+ statements = self.class.repository.query(:subject => @uri)
61
+ @attributes = {}
29
62
 
30
- # If we got statements, we are being loaded, not created
31
- if opts[:statements]
63
+ unless statements.empty?
32
64
  # Set attributes for each statement corresponding to a predicate
33
65
  self.class.properties.each do |name, property|
34
66
  if self.class.is_list?(name)
67
+ # FIXME: This should not be an Array, but a Set. However, a set
68
+ # must compare its values to see if they already exist. This
69
+ # means any referenced relations will check their attributes and
70
+ # execute the promises to load those classes. Need an identity
71
+ # map of some sort to fix that.
35
72
  values = []
36
- statements = opts[:statements].query(:subject => @uri, :predicate => property[:predicate])
37
- unless statements.nil?
38
- statements.each do |statement|
73
+ collection = statements.query(:subject => @uri, :predicate => property[:predicate])
74
+ unless collection.nil?
75
+ collection.each do |statement|
39
76
  values << self.class.build_value(statement,property[:type])
40
77
  end
41
78
  end
42
79
  attribute_set(name, values)
43
80
  else
44
- statement = opts[:statements].query(:subject => @uri, :predicate => property[:predicate]).first
81
+ statement = statements.query(:subject => @uri, :predicate => property[:predicate]).first
45
82
  attribute_set(name, self.class.build_value(statement, property[:type]))
46
83
  end
47
84
  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
85
  end
54
86
 
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
87
+ # We need to load and save the original attributes so we can remove
88
+ # them from the repository on save, since RDF will happily let us add
89
+ # as many triples for a subject and predicate as we want.
90
+ @original_attributes = {}
63
91
  @original_attributes = @attributes.dup
64
92
  @original_attributes.each do | name, value |
65
93
  @original_attributes[name] = value.dup if value.is_a?(Array)
66
94
  end
95
+
96
+ @attributes
67
97
  end
68
-
69
- def _destroy_attributes(attributes)
98
+
99
+ ##
100
+ # Remove the given attributes from the repository
101
+ #
102
+ # @param [Hash] attributes The hash of attributes to delete
103
+ # @param [Hash{Symbol => Any}] opts Options for deletion
104
+ # @option opts [true] :destroy_type Destroys the `RDF.type` statement associated with this class as well
105
+ # @private
106
+ def _destroy_attributes(attributes, opts = {})
70
107
  repository = repository_for_attributes(attributes)
108
+ repository.insert([@uri, RDF.type, self.class.type]) if (self.class.type && opts[:destroy_type])
71
109
  self.class.repository.delete(*repository)
72
110
  end
73
-
111
+
112
+ ##
113
+ # Remove this instance from the repository. Will not delete statements
114
+ # not associated with this model.
115
+ #
116
+ # @return [true, false] Whether or not the destroy was successful
74
117
  def destroy!
75
- _destroy_attributes(@attributes)
118
+ _destroy_attributes(@attributes, :destroy_type => true)
119
+ reload
120
+ end
121
+
122
+ ##
123
+ # Remove all statements associated with this instance from the
124
+ # repository. This will delete statements unassociated with the current
125
+ # projection.
126
+ #
127
+ # @return [true, false] Whether or not the destroy was successful
128
+ def destroy_resource!
129
+ self.class.repository.delete([@uri,nil,nil])
76
130
  end
77
131
 
132
+ ##
133
+ # Save changes in this instance to the repository.
134
+ #
135
+ # @return [true, false] Whether or not the save was successful
78
136
  def save!
79
137
  if self.class.repository.nil?
80
138
  raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
81
139
  end
82
- if respond_to?(:validate)
140
+ unless self.class.validators.empty?
83
141
  errors.clear
84
- validate
142
+ self.class.validators.each do | validator | self.send(validator) end
85
143
  if errors.empty?
86
144
  _update!
87
145
  else
88
- raise(ValidationError, "Could not save #{self.inspect} due to validation errors: " + errors.join(';'))
146
+ raise(ValidationError, "Could not save #{self.inspect} due to validation errors: " + errors.each.join(';'))
89
147
  end
90
148
  else
91
149
  _update!
92
150
  end
93
151
  end
94
152
 
153
+ ##
154
+ # Save changes to the repository
155
+ #
156
+ # @private
95
157
  def _update!
96
158
  _destroy_attributes(@original_attributes)
97
159
  self.class.repository.insert(*self)
98
160
  @original_attributes = @attributes.dup
99
161
  end
100
-
162
+
163
+ ##
164
+ # The `RDF.type` associated with this class.
165
+ #
166
+ # @return [nil,RDF::URI] The RDF type associated with this instance's class.
101
167
  def type
102
168
  self.class.type
103
169
  end
104
-
170
+
171
+ ##
172
+ # `type` is a special property which is associated with the class and not
173
+ # the instance. Always raises a TypeError to try and assign it.
174
+ #
175
+ # @raise [TypeError] always
105
176
  def type=(type)
106
177
  raise TypeError, "Cannot reassign RDF.type for #{self}; consider appending to a has_many :types"
107
178
  end
108
-
179
+
180
+ ##
181
+ # A developer-friendly view of this projection
182
+ #
183
+ # @private
109
184
  def inspect
110
185
  "<#{self.class}:#{self.object_id} uri: #{@uri}>"
111
186
  end
112
-
187
+
188
+ ##
189
+ # Enumerate each RDF statement that makes up this projection. This makes
190
+ # each instance an `RDF::Enumerable`, with all of the nifty benefits
191
+ # thereof. See <http://rdf.rubyforge.org/RDF/Enumerable.html> for
192
+ # information on arguments.
193
+ #
194
+ # @see http://rdf.rubyforge.org/RDF/Enumerable.html
113
195
  def each(*args, &block)
196
+ return RDF::Enumerator.new(self, :each) unless block_given?
114
197
  repository = repository_for_attributes(@attributes)
115
198
  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
199
+ repository.each(*args, &block)
121
200
  end
122
201
 
202
+ ##
203
+ # Safely set a given attribute. Currently not needed and marked as
204
+ # private.
205
+ #
206
+ # @private
123
207
  def attribute_set(name, value)
124
208
  @attributes[name] = value
125
209
  end
126
210
 
211
+ ##
212
+ # Safely get a given attribute.
213
+ #
214
+ # @private
127
215
  def attribute_get(name)
128
216
  case self.class.is_list?(name)
129
217
  when true
@@ -133,12 +221,17 @@ module Spira
133
221
  end
134
222
  end
135
223
 
224
+ ##
225
+ # Create an RDF::Repository for the given attributes hash. This could
226
+ # just as well be a class method but is only used here in #save! and
227
+ # #destroy!, so it is defined here for simplicity.
228
+ #
229
+ # @param [Hash] attributes The attributes to create a repository for
230
+ # @private
136
231
  def repository_for_attributes(attributes)
137
232
  repo = RDF::Repository.new
138
233
  attributes.each do | name, attribute |
139
234
  if self.class.is_list?(name)
140
- #old = @repo.query(:subject => @uri, :predicate => predicate)
141
- #@repo.delete(*old.to_a) unless old.empty?
142
235
  new = []
143
236
  attribute.each do |value|
144
237
  value = self.class.build_rdf_value(value, self.class.properties[name][:type])
@@ -153,9 +246,15 @@ module Spira
153
246
  repo
154
247
  end
155
248
 
249
+ ##
250
+ # Compare this instance with another instance. The comparison is done on
251
+ # an RDF level, and will work across subclasses as long as the attributes
252
+ # are the same.
253
+ #
254
+ # @see http://rdf.rubyforge.org/isomorphic/
156
255
  def ==(other)
157
256
  case other
158
- # TODO: define behavior for equality on subclasses. also subclasses.
257
+ # TODO: define behavior for equality on subclasses.
159
258
  when self.class
160
259
  @uri == other.uri
161
260
  when RDF::Enumerable
@@ -165,9 +264,19 @@ module Spira
165
264
  end
166
265
  end
167
266
 
267
+ ##
268
+ # The validation errors collection associated with this instance.
269
+ #
270
+ # @return [Spira::Errors]
271
+ # @see Spira::Errors
272
+ def errors
273
+ @errors ||= Spira::Errors.new
274
+ end
168
275
 
276
+ ## We have defined #each and can do this fun RDF stuff by default
169
277
  include ::RDF::Enumerable, ::RDF::Queryable
170
278
 
279
+ ## Include the base validation functions
171
280
  include Spira::Resource::Validations
172
281
 
173
282
  end
@@ -1,21 +1,45 @@
1
1
  module Spira
2
2
  module Resource
3
- module Validations
4
3
 
5
- def errors
6
- @errors ||= []
7
- end
4
+ ##
5
+ # Instance methods relating to validations for a Spira resource. This
6
+ # includes the default assertions.
7
+ #
8
+ # @see Spira::Resource::InstanceMethods
9
+ # @see Spira::Resource::ClassMethods
10
+ # @see Spira::Resource::DSL
11
+ module Validations
8
12
 
9
- def assert(boolean, message)
10
- errors.push(message) unless boolean
13
+ ##
14
+ # Assert a fact about this instance. If the given expression is false,
15
+ # an error will be noted.
16
+ #
17
+ # @example Assert that a title is correct
18
+ # assert(title == 'xyz', :title, 'bad title')
19
+ # @param [Any] boolean The expression to evaluate
20
+ # @param [Symbol] property The property or has_many to mark as incorrect on failure
21
+ # @param [String] message The message to record if this assertion fails
22
+ # @return [Void]
23
+ def assert(boolean, property, message)
24
+ errors.add(property, message) unless boolean
11
25
  end
12
26
 
27
+ ##
28
+ # A default helper assertion. Asserts that a given property is set.
29
+ #
30
+ # @param [Symbol] name The property to check
31
+ # @return [Void]
13
32
  def assert_set(name)
14
- assert(!(self.send(name).nil?), "#{name.to_s} cannot be nil")
33
+ assert(!(self.send(name).nil?), name, "#{name.to_s} cannot be nil")
15
34
  end
16
35
 
36
+ ##
37
+ # A default helper assertion. Asserts that a given property is numeric.
38
+ #
39
+ # @param [Symbol] name The property to check
40
+ # @return [Void]
17
41
  def assert_numeric(name)
18
- assert(self.send(name).is_a?(Numeric), "#{name.to_s} must be numeric (was #{self.send(name)})")
42
+ assert(self.send(name).is_a?(Numeric), name, "#{name.to_s} must be numeric (was #{self.send(name)})")
19
43
  end
20
44
 
21
45
  end
data/lib/spira/type.rb CHANGED
@@ -0,0 +1,82 @@
1
+ module Spira
2
+
3
+ ##
4
+ # Spira::Type can be included by classes to create new property types for
5
+ # Spira. These types are responsible for serialization a Ruby value into an
6
+ # `RDF::Value`, and deserialization of an `RDF::Value` into a Ruby value.
7
+ #
8
+ # A simple example:
9
+ #
10
+ # class Integer
11
+ #
12
+ # include Spira::Type
13
+ #
14
+ # def self.unserialize(value)
15
+ # value.object
16
+ # end
17
+ #
18
+ # def self.serialize(value)
19
+ # RDF::Literal.new(value)
20
+ # end
21
+ #
22
+ # register_alias XSD.integer
23
+ # end
24
+ #
25
+ # This example will serialize and deserialize integers. It's included with
26
+ # Spira by default. It allows either of the following forms to declare an
27
+ # integer property on a Spira resource:
28
+ #
29
+ # property :age, :predicate => FOAF.age, :type => Integer
30
+ # property :age, :predicate => FOAF.age, :type => XSD.integer
31
+ #
32
+ # `Spira::Type`s include the RDF namespace and thus have all of the base RDF
33
+ # vocabularies available to them without the `RDF::` prefix.
34
+ #
35
+ # @see http://rdf.rubyforge.org/RDF/Value.html
36
+ # @see Spira::Resource
37
+ module Type
38
+
39
+ ##
40
+ # Make the DSL available to a child class.
41
+ #
42
+ # @private
43
+ def self.included(child)
44
+ child.extend(ClassMethods)
45
+ Spira.type_alias(child,child)
46
+ end
47
+
48
+ include RDF
49
+
50
+ module ClassMethods
51
+
52
+ ##
53
+ # Register an alias that this type can be referred to as, such as an RDF
54
+ # URI. The alias can be any object, symbol, or constant.
55
+ #
56
+ # @param [Any] identifier The new alias in property declarations for this class
57
+ # @return [Void]
58
+ def register_alias(any)
59
+ Spira.type_alias(any, self)
60
+ end
61
+
62
+ ##
63
+ # Serialize a given value to RDF.
64
+ #
65
+ # @param [Any] value The Ruby value to be serialized
66
+ # @return [RDF::Value] The RDF form of this value
67
+ def serialize(value)
68
+ value
69
+ end
70
+
71
+ ##
72
+ # Unserialize a given RDF value to Ruby
73
+ #
74
+ # @param [RDF::Value] value The RDF form of this value
75
+ # @return [Any] The Ruby form of this value
76
+ def unserialize(value)
77
+ value
78
+ end
79
+ end
80
+
81
+ end
82
+ end