spira 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog for Spira <http://github.com/datagraph/spira>
2
2
 
3
+ ## 0.0.6
4
+ * Added #exists?, which returns a boolean if an instance exists in
5
+ the backing store.
6
+ * Added #data, which returns an enumerator of all RDF data for a subject, not
7
+ just model data.
8
+ * #save! and #update! now return self for chaining
9
+ * Implemented #update and #update!, which allow setting multiple properties
10
+ at once
11
+ * Existing values not matching a model's defined type will now be deleted on
12
+ #save!
13
+ * Saving resources will now only update dirty fields
14
+ * Saving resources now removes all existing triples for a given predicate
15
+ if the field was updated instead of only removing one.
16
+ * Implemented and documented #destroy!, #destroy!(:subject),
17
+ #destroy!(:object), and #destroy!(:completely). Removed #destroy_resource!
18
+ * has_many collections are now Sets and not Arrays, more accurately reflecting
19
+ RDF semantics.
20
+ * The Any (default) property type will now work fine with URIs
21
+ * Added ResourceDeclarationError to replace various errors that occur during
22
+ invalid class declarations via the DSL.
23
+ * Raise an error if a non-URI predicate is given in the DSL
24
+ * Small updates for RDF.rb 0.2.0
25
+ * Implemented dirty field tracking. Resource#dirty?(:name) will now report if
26
+ a field has not been saved.
27
+
3
28
  ## 0.0.5
4
29
  * Relations can now find related classes in modules, either by absolute
5
30
  reference, or by class name if they are in the same namespace.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.5
1
+ 0.0.6
@@ -12,4 +12,8 @@ module Spira
12
12
  ##
13
13
  # For cases in which a repository is required but none has been given
14
14
  class NoRepositoryError < StandardError ; end
15
+
16
+ ##
17
+ # For errors in the DSL, such as invalid predicates
18
+ class ResourceDeclarationError < StandardError ; end
15
19
  end
@@ -221,11 +221,14 @@ module Spira
221
221
  when opts[:predicate]
222
222
  opts[:predicate]
223
223
  when @default_vocabulary.nil?
224
- raise TypeError, "A :predicate option is required for types without a default vocabulary"
224
+ raise ResourceDeclarationError, "A :predicate option is required for types without a default vocabulary"
225
225
  else @default_vocabulary
226
226
  separator = @default_vocabulary.to_s[-1,1] =~ /(\/|#)/ ? '' : '/'
227
227
  RDF::URI.new(@default_vocabulary.to_s + separator + name.to_s)
228
228
  end
229
+ if !(predicate.respond_to?(:to_uri))
230
+ raise ResourceDeclarationError, ":predicate options must be RDF::URIs or strings with a default vocabulary declared"
231
+ end
229
232
  type = case
230
233
  when opts[:type].nil?
231
234
  Spira::Types::Any
@@ -241,27 +244,13 @@ module Spira
241
244
  @properties[name][:type] = type
242
245
  name_equals = (name.to_s + "=").to_sym
243
246
 
244
- (getter,setter) = self.send(accessors_method, name, predicate, type)
245
- self.send(:define_method,name_equals, &setter)
246
- self.send(:define_method,name, &getter)
247
-
248
- end
249
-
250
- ##
251
- # Getter and Setter methods for predicates.
252
- # FIXME: this and add_accessors are from an older version in which
253
- # multiple versions of accessors existed, and can be refactored.
254
- # @private
255
- def hash_accessors(name, predicate, type)
256
- setter = lambda do |arg|
257
- attribute_set(name,arg)
247
+ self.send(:define_method,name_equals) do |arg|
248
+ attribute_set(name, arg)
258
249
  end
259
-
260
- getter = lambda do
250
+ self.send(:define_method,name) do
261
251
  attribute_get(name)
262
252
  end
263
253
 
264
- [getter, setter]
265
254
  end
266
255
 
267
256
  end
@@ -1,4 +1,5 @@
1
1
  require 'rdf/isomorphic'
2
+ require 'set'
2
3
 
3
4
  module Spira
4
5
  module Resource
@@ -25,7 +26,7 @@ module Spira
25
26
  # attributes. To use a URI or existing blank node as a subject, use
26
27
  # {Spira::Resource::ClassMethods#for} instead.
27
28
  #
28
- # @param [Hash] opts Default attributes for this instance
29
+ # @param [Hash{Symbol => Any}] opts Default attributes for this instance
29
30
  # @see Spira::Resource::ClassMethods#for
30
31
  # @see RDF::URI#as
31
32
  # @see RDF::Node#as
@@ -42,8 +43,10 @@ module Spira
42
43
  # @param [Hash{Symbol => Any}] opts
43
44
  # @option opts [Symbol] :any A property name. Sets the given property to the given value.
44
45
  def reload(opts = {})
46
+ @dirty = {}
47
+ # We need to save all attributes twice to track state changes in
48
+ # mutable objects, like lists
45
49
  @attributes = promise { reload_attributes }
46
- @original_attributes = promise { @attributes.force ; @original_attributes }
47
50
  self.class.properties.each do |name, predicate|
48
51
  attribute_set(name, opts[name]) unless opts[name].nil?
49
52
  end
@@ -52,47 +55,53 @@ module Spira
52
55
  ##
53
56
  # Load this instance's attributes. Overwrite loaded values with attributes in the given options.
54
57
  #
55
- # @param [Hash] opts
56
- # @return [Hash] @attributes
58
+ # @return [Hash{Symbol => Any}] attributes
57
59
  # @private
58
60
  def reload_attributes()
59
61
  statements = self.class.repository_or_fail.query(:subject => @subject)
60
- @attributes = {}
62
+ attributes = {}
63
+ attributes[:original] = {}
64
+ attributes[:current] = {}
61
65
 
62
- unless statements.empty?
63
- # Set attributes for each statement corresponding to a predicate
64
- self.class.properties.each do |name, property|
65
- if self.class.is_list?(name)
66
- # FIXME: This should not be an Array, but a Set. However, a set
67
- # must compare its values to see if they already exist. This
68
- # means any referenced relations will check their attributes and
69
- # execute the promises to load those classes. Need an identity
70
- # map of some sort to fix that.
71
- values = []
72
- collection = statements.query(:subject => @subject, :predicate => property[:predicate])
73
- unless collection.nil?
74
- collection.each do |statement|
75
- values << self.class.build_value(statement,property[:type])
76
- end
66
+ # Set attributes for each statement corresponding to a predicate
67
+ self.class.properties.each do |name, property|
68
+ if self.class.is_list?(name)
69
+ values = Set.new
70
+ collection = statements.query(:subject => @subject, :predicate => property[:predicate]) unless statements.empty?
71
+ unless collection.nil?
72
+ collection.each do |statement|
73
+ values << self.class.build_value(statement,property[:type])
77
74
  end
78
- attribute_set(name, values)
79
- else
80
- statement = statements.query(:subject => @subject, :predicate => property[:predicate]).first
81
- attribute_set(name, self.class.build_value(statement, property[:type]))
75
+ end
76
+ attributes[:current][name] = values
77
+ attributes[:original][name] = values.dup
78
+ else
79
+ statement = statements.query(:subject => @subject, :predicate => property[:predicate]).first unless statements.empty?
80
+ attributes[:current][name] = self.class.build_value(statement, property[:type])
81
+
82
+ # Lots of things like Fixnums and Nil can't be dup'd, but they are
83
+ # all immutable, so if we can't dup, it's no problem for dirty tracking.
84
+ begin
85
+ attributes[:original][name] = attributes[:current][name].dup
86
+ rescue TypeError
87
+ attributes[:original][name] = attributes[:current][name]
82
88
  end
83
89
  end
84
90
  end
85
91
 
86
- # We need to load and save the original attributes so we can remove
87
- # them from the repository on save, since RDF will happily let us add
88
- # as many triples for a subject and predicate as we want.
89
- @original_attributes = {}
90
- @original_attributes = @attributes.dup
91
- @original_attributes.each do | name, value |
92
- @original_attributes[name] = value.dup if value.is_a?(Array)
93
- end
92
+ attributes
93
+ end
94
94
 
95
- @attributes
95
+ ##
96
+ # Returns a hash of name => value for this instance's attributes
97
+ #
98
+ # @return [Hash{Symbol => Any}] attributes
99
+ def attributes
100
+ attributes = {}
101
+ self.class.properties.keys.each do |property|
102
+ attributes[property] = attribute_get(property)
103
+ end
104
+ attributes
96
105
  end
97
106
 
98
107
  ##
@@ -109,29 +118,35 @@ module Spira
109
118
  end
110
119
 
111
120
  ##
112
- # Remove this instance from the repository. Will not delete statements
113
- # not associated with this model.
121
+ # Delete this instance from the repository.
114
122
  #
123
+ # @param [Symbol] what
124
+ # @example Delete all fields defined in the model
125
+ # @object.destroy!
126
+ # @example Delete all instances of this object as the subject of a triple, including non-model data @object.destroy!
127
+ # @object.destroy!(:subject)
128
+ # @example Delete all instances of this object as the object of a triple
129
+ # @object.destroy!(:object)
130
+ # @example Delete all triples with this object as the subject or object
131
+ # @object.destroy!(:completely)
115
132
  # @return [true, false] Whether or not the destroy was successful
116
- def destroy!
117
- _destroy_attributes(@attributes, :destroy_type => true)
118
- reload
119
- end
120
-
121
- ##
122
- # Remove all statements associated with this instance from the
123
- # repository. This will delete statements unassociated with the current
124
- # projection.
125
- #
126
- # @return [true, false] Whether or not the destroy was successful
127
- def destroy_resource!
128
- self.class.repository_or_fail.delete([@subject,nil,nil])
133
+ def destroy!(what = nil)
134
+ case what
135
+ when nil
136
+ _destroy_attributes(attributes, :destroy_type => true) != nil
137
+ when :subject
138
+ self.class.repository_or_fail.delete([subject, nil, nil]) != nil
139
+ when :object
140
+ self.class.repository_or_fail.delete([nil, nil, subject]) != nil
141
+ when :completely
142
+ destroy!(:subject) && destroy!(:object)
143
+ end
129
144
  end
130
145
 
131
146
  ##
132
147
  # Save changes in this instance to the repository.
133
148
  #
134
- # @return [true, false] Whether or not the save was successful
149
+ # @return [self] self
135
150
  def save!
136
151
  unless self.class.validators.empty?
137
152
  errors.clear
@@ -144,6 +159,47 @@ module Spira
144
159
  else
145
160
  _update!
146
161
  end
162
+ self
163
+ end
164
+
165
+ ##
166
+ # Update multiple attributes of this repository.
167
+ #
168
+ # @example Update multiple attributes
169
+ # person.update(:name => 'test', :age => 10)
170
+ # #=> person
171
+ # person.name
172
+ # #=> 'test'
173
+ # person.age
174
+ # #=> 10
175
+ # person.dirty?
176
+ # #=> true
177
+ # @param [Hash{Symbol => Any}] properties
178
+ # @return [self]
179
+ def update(properties)
180
+ properties.each do |property, value|
181
+ attribute_set(property, value)
182
+ end
183
+ self
184
+ end
185
+
186
+ ##
187
+ # Equivalent to #update followed by #save!
188
+ #
189
+ # @example Update multiple attributes and save the changes
190
+ # person.update!(:name => 'test', :age => 10)
191
+ # #=> person
192
+ # person.name
193
+ # #=> 'test'
194
+ # person.age
195
+ # #=> 10
196
+ # person.dirty?
197
+ # #=> false
198
+ # @param [Hash{Symbol => Any}] properties
199
+ # @return [self]
200
+ def update!(properties)
201
+ update(properties)
202
+ save!
147
203
  end
148
204
 
149
205
  ##
@@ -151,9 +207,23 @@ module Spira
151
207
  #
152
208
  # @private
153
209
  def _update!
154
- _destroy_attributes(@original_attributes)
155
- self.class.repository_or_fail.insert(*self)
156
- @original_attributes = @attributes.dup
210
+ self.class.properties.each do |property, predicate|
211
+ if dirty?(property)
212
+ self.class.repository_or_fail.delete([subject, predicate[:predicate], nil])
213
+ if self.class.is_list?(property)
214
+ repo = RDF::Repository.new
215
+ attribute_get(property).each do |value|
216
+ repo << RDF::Statement.new(subject, predicate[:predicate], self.class.build_rdf_value(value, self.class.properties[property][:type]))
217
+ end
218
+ self.class.repository_or_fail.insert(*repo)
219
+ else
220
+ self.class.repository_or_fail.insert(RDF::Statement.new(subject, predicate[:predicate], self.class.build_rdf_value(attribute_get(property), self.class.properties[property][:type])))
221
+ end
222
+ end
223
+ @dirty[property] = nil
224
+ @attributes[:original][property] = attribute_get(property)
225
+ end
226
+ self.class.repository_or_fail.insert(RDF::Statement.new(@subject, RDF.type, type)) unless type.nil?
157
227
  end
158
228
 
159
229
  ##
@@ -197,18 +267,44 @@ module Spira
197
267
  #
198
268
  # @see http://rdf.rubyforge.org/RDF/Enumerable.html
199
269
  def each(*args, &block)
200
- return RDF::Enumerator.new(self, :each) unless block_given?
201
- repository = repository_for_attributes(@attributes)
270
+ return enum_for(:each) unless block_given?
271
+ repository = repository_for_attributes(attributes)
202
272
  repository.insert(RDF::Statement.new(@subject, RDF.type, type)) unless type.nil?
203
273
  repository.each(*args, &block)
204
274
  end
205
275
 
276
+ ##
277
+ # The number of RDF::Statements this projection has.
278
+ #
279
+ # @see http://rdf.rubyforge.org/RDF/Enumerable.html#count
280
+ def count
281
+ each.size
282
+ end
283
+
206
284
  ##
207
285
  # Sets the given attribute to the given value.
208
286
  #
209
287
  # @private
210
288
  def attribute_set(name, value)
211
- @attributes[name] = value
289
+ @dirty[name] = true
290
+ @attributes[:current][name] = value
291
+ end
292
+
293
+ ##
294
+ # Returns true if the given attribute has been changed from the backing store
295
+ #
296
+ def dirty?(name = nil)
297
+ case name
298
+ when nil
299
+ self.class.properties.keys.any? { |key| dirty?(key) }
300
+ else
301
+ case
302
+ when @dirty[name] == true
303
+ true
304
+ else
305
+ @attributes[:current][name] != @attributes[:original][name]
306
+ end
307
+ end
212
308
  end
213
309
 
214
310
  ##
@@ -216,12 +312,7 @@ module Spira
216
312
  #
217
313
  # @private
218
314
  def attribute_get(name)
219
- case self.class.is_list?(name)
220
- when true
221
- @attributes[name] ||= []
222
- when false
223
- @attributes[name]
224
- end
315
+ @attributes[:current][name]
225
316
  end
226
317
 
227
318
  ##
@@ -331,6 +422,25 @@ module Spira
331
422
  @errors ||= Spira::Errors.new
332
423
  end
333
424
 
425
+ ##
426
+ # Returns true if any data exists for this subject in the backing RDF store
427
+ #
428
+ # @return [Boolean]
429
+ def exists?
430
+ !data.empty?
431
+ end
432
+ alias_method :exist?, :exists?
433
+
434
+ ##
435
+ # Returns an Enumerator of all RDF data for this subject, not just model data.
436
+ #
437
+ # @see #each
438
+ # @see http://rdf.rubyforge.org/RDF/Enumerable.html
439
+ # @return [Enumerator]
440
+ def data
441
+ self.class.repository.query(:subject => subject)
442
+ end
443
+
334
444
  ## We have defined #each and can do this fun RDF stuff by default
335
445
  include ::RDF::Enumerable, ::RDF::Queryable
336
446
 
@@ -12,11 +12,12 @@ module Spira::Types
12
12
  include Spira::Type
13
13
 
14
14
  def self.unserialize(value)
15
- value.object
15
+ value.respond_to?(:to_uri) ? value.to_uri : value.object
16
16
  end
17
17
 
18
18
  def self.serialize(value)
19
- RDF::Literal.new(value)
19
+ raise TypeError, "Spira::Types::Any cannot serialize collections" if value.is_a?(Array)
20
+ value.respond_to?(:to_uri) ? value.to_uri : RDF::Literal.new(value)
20
21
  end
21
22
 
22
23
  end
data/lib/spira/version.rb CHANGED
@@ -2,7 +2,7 @@ module Spira
2
2
  module VERSION
3
3
  MAJOR = 0
4
4
  MINOR = 0
5
- TINY = 5
5
+ TINY = 6
6
6
  EXTRA = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, TINY].join('.')
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 5
9
- version: 0.0.5
8
+ - 6
9
+ version: 0.0.6
10
10
  platform: ruby
11
11
  authors:
12
12
  - Ben Lavender
@@ -15,7 +15,7 @@ bindir:
15
15
  - bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-06-28 00:00:00 -05:00
18
+ date: 2010-08-23 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -28,8 +28,8 @@ dependencies:
28
28
  segments:
29
29
  - 0
30
30
  - 2
31
- - 0
32
- version: 0.2.0
31
+ - 2
32
+ version: 0.2.2
33
33
  type: :development
34
34
  version_requirements: *id001
35
35
  - !ruby/object:Gem::Dependency
@@ -70,8 +70,8 @@ dependencies:
70
70
  segments:
71
71
  - 0
72
72
  - 2
73
- - 0
74
- version: 0.2.0
73
+ - 2
74
+ version: 0.2.2
75
75
  type: :runtime
76
76
  version_requirements: *id004
77
77
  - !ruby/object:Gem::Dependency
@@ -97,9 +97,9 @@ dependencies:
97
97
  - !ruby/object:Gem::Version
98
98
  segments:
99
99
  - 0
100
- - 2
100
+ - 3
101
101
  - 0
102
- version: 0.2.0
102
+ version: 0.3.0
103
103
  type: :runtime
104
104
  version_requirements: *id006
105
105
  description: Spira is a framework for using the information in RDF.rb repositories as model objects.