spira 0.0.5 → 0.0.6

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/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.