spira 0.0.12 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,567 +0,0 @@
1
- require 'rdf/isomorphic'
2
- require 'set'
3
-
4
- module Spira
5
- module Resource
6
-
7
- ##
8
- # This module contains instance methods for Spira resources. See
9
- # {Spira::Resource} for more information.
10
- #
11
- # @see Spira::Resource
12
- # @see Spira::Resource::ClassMethods
13
- # @see Spira::Resource::DSL
14
- # @see Spira::Resource::Validations
15
- module InstanceMethods
16
-
17
- # Marker for whether or not a field has been set or not; distinguishes
18
- # nil and unset.
19
- # @private
20
- NOT_SET = ::Object.new.freeze
21
-
22
- ##
23
- # This instance's URI.
24
- #
25
- # @return [RDF::URI]
26
- attr_reader :subject
27
-
28
- ##
29
- # Initialize a new Spira::Resource instance of this resource class using
30
- # a new blank node subject. Accepts a hash of arguments for initial
31
- # attributes. To use a URI or existing blank node as a subject, use
32
- # {Spira::Resource::ClassMethods#for} instead.
33
- #
34
- # @param [Hash{Symbol => Any}] opts Default attributes for this instance
35
- # @yield [self] Executes a given block and calls `#save!`
36
- # @yieldparam [self] self The newly created instance
37
- # @see Spira::Resource::ClassMethods#for
38
- # @see RDF::URI#as
39
- # @see RDF::Node#as
40
- def initialize(opts = {})
41
- @subject = opts[:_subject] || RDF::Node.new
42
- reload(opts)
43
- if block_given?
44
- yield(self)
45
- save!
46
- end
47
- self
48
- end
49
-
50
- ##
51
- # Reload all attributes for this instance, overwriting or setting
52
- # defaults with the given opts. This resource will block if the
53
- # underlying repository blocks the next time it accesses attributes.
54
- #
55
- # @param [Hash{Symbol => Any}] opts
56
- # @option opts [Symbol] :any A property name. Sets the given property to the given value.
57
- def reload(opts = {})
58
- @cache = opts[:_cache] || RDF::Util::Cache.new
59
- @cache[subject] = self
60
- @dirty = {}
61
- @attributes = {}
62
- @attributes[:current] = {}
63
- @attributes[:copied] = {}
64
- self.class.properties.each do |name, predicate|
65
- case opts[name].nil?
66
- when false
67
- attribute_set(name, opts[name])
68
- when true
69
- @attributes[:copied][name] = NOT_SET
70
- end
71
- end
72
- @attributes[:original] = promise { reload_attributes }
73
- end
74
-
75
- ##
76
- # Load this instance's attributes. Overwrite loaded values with attributes in the given options.
77
- #
78
- # @return [Hash{Symbol => Any}] attributes
79
- # @private
80
- def reload_attributes()
81
- statements = self.class.repository_or_fail.query(:subject => @subject).to_a
82
- attributes = {}
83
-
84
- # Set attributes for each statement corresponding to a predicate
85
- self.class.properties.each do |name, property|
86
- if self.class.is_list?(name)
87
- values = Set.new
88
- collection = statements.select{|s| s.subject == @subject && s.predicate == property[:predicate]} unless statements.empty?
89
- unless collection.nil?
90
- collection.each do |statement|
91
- values << self.class.build_value(statement,property[:type], @cache)
92
- end
93
- end
94
- attributes[name] = values
95
- else
96
- statement = statements.select{|s| s.subject == @subject && s.predicate == property[:predicate]}.first unless statements.empty?
97
- attributes[name] = self.class.build_value(statement, property[:type], @cache)
98
- end
99
- end
100
- attributes
101
- end
102
-
103
- ##
104
- # Returns a hash of name => value for this instance's attributes
105
- #
106
- # @return [Hash{Symbol => Any}] attributes
107
- def attributes
108
- attributes = {}
109
- self.class.properties.keys.each do |property|
110
- attributes[property] = attribute_get(property)
111
- end
112
- attributes
113
- end
114
-
115
- ##
116
- # Remove the given attributes from the repository
117
- #
118
- # @param [Hash] attributes The hash of attributes to delete
119
- # @param [Hash{Symbol => Any}] opts Options for deletion
120
- # @option opts [true] :destroy_type Destroys the `RDF.type` statement associated with this class as well
121
- # @private
122
- def _destroy_attributes(attributes, opts = {})
123
- repository = repository_for_attributes(attributes)
124
- repository.insert([@subject, RDF.type, self.class.type]) if (self.class.type && opts[:destroy_type])
125
- self.class.repository_or_fail.delete(*repository)
126
- end
127
-
128
- ##
129
- # Delete this instance from the repository.
130
- #
131
- # @param [Symbol] what
132
- # @example Delete all fields defined in the model
133
- # @object.destroy!
134
- # @example Delete all instances of this object as the subject of a triple, including non-model data @object.destroy!
135
- # @object.destroy!(:subject)
136
- # @example Delete all instances of this object as the object of a triple
137
- # @object.destroy!(:object)
138
- # @example Delete all triples with this object as the subject or object
139
- # @object.destroy!(:completely)
140
- # @return [true, false] Whether or not the destroy was successful
141
- def destroy!(what = nil)
142
- before_destroy if self.respond_to?(:before_destroy)
143
- result = case what
144
- when nil
145
- _destroy_attributes(attributes, :destroy_type => true) != nil
146
- when :subject
147
- self.class.repository_or_fail.delete([subject, nil, nil]) != nil
148
- when :object
149
- self.class.repository_or_fail.delete([nil, nil, subject]) != nil
150
- when :completely
151
- destroy!(:subject) && destroy!(:object)
152
- end
153
- after_destroy if self.respond_to?(:after_destroy) if result
154
- result
155
- end
156
-
157
- ##
158
- # Save changes in this instance to the repository.
159
- #
160
- # @return [self] self
161
- def save!
162
- existed = (self.respond_to?(:before_create) || self.respond_to?(:after_create)) && !self.type.nil? && exists?
163
- before_create if self.respond_to?(:before_create) && !self.type.nil? && !existed
164
- before_save if self.respond_to?(:before_save)
165
- # we use the non-raising validate and check it to make a slightly different error message. worth it?...
166
- case validate
167
- when true
168
- _update!
169
- when false
170
- raise(ValidationError, "Could not save #{self.inspect} due to validation errors: " + errors.each.join(';'))
171
- end
172
- after_create if self.respond_to?(:after_create) && !self.type.nil? && !existed
173
- after_save if self.respond_to?(:after_save)
174
- self
175
- end
176
-
177
- ##
178
- # Update multiple attributes of this repository.
179
- #
180
- # @example Update multiple attributes
181
- # person.update(:name => 'test', :age => 10)
182
- # #=> person
183
- # person.name
184
- # #=> 'test'
185
- # person.age
186
- # #=> 10
187
- # person.dirty?
188
- # #=> true
189
- # @param [Hash{Symbol => Any}] properties
190
- # @return [self]
191
- def update(properties)
192
- properties.each do |property, value|
193
- attribute_set(property, value)
194
- end
195
- after_update if self.respond_to?(:after_update)
196
- self
197
- end
198
-
199
- ##
200
- # Equivalent to #update followed by #save!
201
- #
202
- # @example Update multiple attributes and save the changes
203
- # person.update!(:name => 'test', :age => 10)
204
- # #=> person
205
- # person.name
206
- # #=> 'test'
207
- # person.age
208
- # #=> 10
209
- # person.dirty?
210
- # #=> false
211
- # @param [Hash{Symbol => Any}] properties
212
- # @return [self]
213
- def update!(properties)
214
- update(properties)
215
- save!
216
- end
217
-
218
- ##
219
- # Save changes to the repository
220
- #
221
- # @private
222
- def _update!
223
- self.class.properties.each do |property, predicate|
224
- if dirty?(property)
225
- self.class.repository_or_fail.delete([subject, predicate[:predicate], nil])
226
- if self.class.is_list?(property)
227
- repo = RDF::Repository.new
228
- attribute_get(property).each do |value|
229
- repo << RDF::Statement.new(subject, predicate[:predicate], self.class.build_rdf_value(value, self.class.properties[property][:type]))
230
- end
231
- self.class.repository_or_fail.insert(*repo)
232
- else
233
- 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]))) unless attribute_get(property).nil?
234
- end
235
- end
236
- @attributes[:original][property] = attribute_get(property)
237
- @dirty[property] = nil
238
- @attributes[:copied][property] = NOT_SET
239
- end
240
- self.class.repository_or_fail.insert(RDF::Statement.new(@subject, RDF.type, type)) unless type.nil?
241
- end
242
-
243
- ##
244
- # The `RDF.type` associated with this class.
245
- #
246
- # @return [nil,RDF::URI] The RDF type associated with this instance's class.
247
- def type
248
- self.class.type
249
- end
250
-
251
- ##
252
- # `type` is a special property which is associated with the class and not
253
- # the instance. Always raises a TypeError to try and assign it.
254
- #
255
- # @raise [TypeError] always
256
- def type=(type)
257
- raise TypeError, "Cannot reassign RDF.type for #{self}; consider appending to a has_many :types"
258
- end
259
-
260
- ##
261
- # Returns the RDF representation of this resource.
262
- #
263
- # @return [RDF::Enumerable]
264
- def to_rdf
265
- self
266
- end
267
-
268
- ##
269
- # A developer-friendly view of this projection
270
- #
271
- # @private
272
- def inspect
273
- "<#{self.class}:#{self.object_id} @subject: #{@subject}>"
274
- end
275
-
276
- ##
277
- # Enumerate each RDF statement that makes up this projection. This makes
278
- # each instance an `RDF::Enumerable`, with all of the nifty benefits
279
- # thereof. See <http://rdf.rubyforge.org/RDF/Enumerable.html> for
280
- # information on arguments.
281
- #
282
- # @see http://rdf.rubyforge.org/RDF/Enumerable.html
283
- def each(*args, &block)
284
- return enum_for(:each) unless block_given?
285
- repository = repository_for_attributes(attributes)
286
- repository.insert(RDF::Statement.new(@subject, RDF.type, type)) unless type.nil?
287
- repository.each(*args, &block)
288
- end
289
-
290
- ##
291
- # The number of RDF::Statements this projection has.
292
- #
293
- # @see http://rdf.rubyforge.org/RDF/Enumerable.html#count
294
- def count
295
- each.size
296
- end
297
-
298
- ##
299
- # Sets the given attribute to the given value.
300
- #
301
- # @private
302
- def attribute_set(name, value)
303
- @dirty[name] = true
304
- @attributes[:current][name] = value
305
- end
306
-
307
- ##
308
- # Returns true if the given attribute has been changed from the backing store
309
- #
310
- def dirty?(name = nil)
311
- case name
312
- when nil
313
- self.class.properties.keys.any? { |key| dirty?(key) }
314
- else
315
- case
316
- when @dirty[name] == true
317
- true
318
- else
319
- case @attributes[:copied][name]
320
- when NOT_SET
321
- false
322
- else
323
- @attributes[:copied][name] != @attributes[:original][name]
324
- end
325
- end
326
- end
327
- end
328
-
329
- ##
330
- # Get the current value for the given attribute
331
- #
332
- # @private
333
- def attribute_get(name)
334
- case @dirty[name]
335
- when true
336
- @attributes[:current][name]
337
- else
338
- case @attributes[:copied][name].equal?(NOT_SET)
339
- when true
340
- dup = if @attributes[:original][name].is_a?(Spira::Resource)
341
- @attributes[:original][name]
342
- else
343
- begin
344
- @attributes[:original][name].dup
345
- rescue TypeError
346
- @attributes[:original][name]
347
- end
348
- end
349
- @attributes[:copied][name] = dup
350
- when false
351
- @attributes[:copied][name]
352
- end
353
- end
354
- end
355
-
356
- ##
357
- # Create an RDF::Repository for the given attributes hash. This could
358
- # just as well be a class method but is only used here in #save! and
359
- # #destroy!, so it is defined here for simplicity.
360
- #
361
- # @param [Hash] attributes The attributes to create a repository for
362
- # @private
363
- def repository_for_attributes(attributes)
364
- repo = RDF::Repository.new
365
- attributes.each do | name, attribute |
366
- if self.class.is_list?(name)
367
- new = []
368
- attribute.each do |value|
369
- value = self.class.build_rdf_value(value, self.class.properties[name][:type])
370
- new << RDF::Statement.new(@subject, self.class.properties[name][:predicate], value)
371
- end
372
- repo.insert(*new)
373
- else
374
- value = self.class.build_rdf_value(attribute, self.class.properties[name][:type])
375
- repo.insert(RDF::Statement.new(@subject, self.class.properties[name][:predicate], value))
376
- end
377
- end
378
- repo
379
- end
380
-
381
- ##
382
- # Compare this instance with another instance. The comparison is done on
383
- # an RDF level, and will work across subclasses as long as the attributes
384
- # are the same.
385
- #
386
- # @see http://rdf.rubyforge.org/isomorphic/
387
- def ==(other)
388
- case other
389
- # TODO: define behavior for equality on subclasses.
390
- # TODO: should we compare attributes here?
391
- when self.class
392
- @subject == other.uri
393
- when RDF::Enumerable
394
- self.isomorphic_with?(other)
395
- else
396
- false
397
- end
398
- end
399
-
400
- ##
401
- # Returns true for :to_uri if this instance's subject is a URI, and false if it is not.
402
- # Returns true for :to_node if this instance's subject is a Node, and false if it is not.
403
- # Calls super otherwise.
404
- #
405
- # @private
406
- def respond_to?(*args)
407
- case args[0]
408
- when :to_uri
409
- @subject.respond_to?(:to_uri)
410
- when :to_node
411
- @subject.node?
412
- else
413
- super(*args)
414
- end
415
- end
416
-
417
- ##
418
- # Returns the RDF::URI associated with this instance if this instance's
419
- # subject is an RDF::URI, and nil otherwise.
420
- #
421
- # @return [RDF::URI,nil]
422
- def uri
423
- @subject.respond_to?(:to_uri) ? @subject : nil
424
- end
425
-
426
- ##
427
- # Returns the URI representation of this resource, if available. If this
428
- # resource's subject is a BNode, raises a NoMethodError.
429
- #
430
- # @return [RDF::URI]
431
- # @raise [NoMethodError]
432
- def to_uri
433
- uri || (raise NoMethodError, "No such method: :to_uri (this instance's subject is not a URI)")
434
- end
435
-
436
- ##
437
- # Returns true if the subject associated with this instance is a blank node.
438
- #
439
- # @return [true, false]
440
- def node?
441
- @subject.node?
442
- end
443
-
444
- ##
445
- # Returns the Node subject of this resource, if available. If this
446
- # resource's subject is a URI, raises a NoMethodError.
447
- #
448
- # @return [RDF::Node]
449
- # @raise [NoMethodError]
450
- def to_node
451
- @subject.node? ? @subject : (raise NoMethodError, "No such method: :to_uri (this instance's subject is not a URI)")
452
- end
453
-
454
- ##
455
- # The validation errors collection associated with this instance.
456
- #
457
- # @return [Spira::Errors]
458
- # @see Spira::Errors
459
- def errors
460
- @errors ||= Spira::Errors.new
461
- end
462
-
463
- ##
464
- # Run any model validations and populate the errors object accordingly.
465
- # Returns true if the model is valid, false otherwise
466
- #
467
- # @return [True, False]
468
- def validate
469
- unless self.class.validators.empty?
470
- errors.clear
471
- self.class.validators.each do | validator | self.send(validator) end
472
- end
473
- errors.empty?
474
- end
475
-
476
- ##
477
- # Run validations on this model and raise a Spira::ValidationError if the validations fail.
478
- #
479
- # @see #validate
480
- # @return true
481
- def validate!
482
- validate || raise(ValidationError, "Failed to validate #{self.inspect}: " + errors.each.join(';'))
483
- end
484
-
485
- ##
486
- # Returns true if any data exists for this subject in the backing RDF store
487
- #
488
- # @return [Boolean]
489
- def exists?
490
- !data.empty?
491
- end
492
- alias_method :exist?, :exists?
493
-
494
- ##
495
- # Returns an Enumerator of all RDF data for this subject, not just model data.
496
- #
497
- # @see #each
498
- # @see http://rdf.rubyforge.org/RDF/Enumerable.html
499
- # @return [Enumerator]
500
- def data
501
- self.class.repository.query(:subject => subject)
502
- end
503
-
504
- ##
505
- # Returns a new instance of this class with the new subject instead of self.subject
506
- #
507
- # @param [RDF::Resource] new_subject
508
- # @return [Spira::Resource] copy
509
- def copy(new_subject)
510
- copy = self.class.for(new_subject)
511
- self.class.properties.each_key { |property| copy.attribute_set(property, self.attribute_get(property)) }
512
- copy
513
- end
514
-
515
- ##
516
- # Returns a new instance of this class with the new subject instead of
517
- # self.subject after saving the new copy to the repository.
518
- #
519
- # @param [RDF::Resource] new_subject
520
- # @return [Spira::Resource, String] copy
521
- def copy!(new_subject)
522
- copy(new_subject).save!
523
- end
524
-
525
- ##
526
- # Copies all data, including non-model data, about this resource to
527
- # another URI. The copy is immediately saved to the repository.
528
- #
529
- # @param [RDF::Resource] new_subject
530
- # @return [Spira::Resource, String] copy
531
- def copy_resource!(new_subject)
532
- new_subject = self.class.id_for(new_subject)
533
- update_repository = RDF::Repository.new
534
- data.each do |statement|
535
- update_repository << RDF::Statement.new(new_subject, statement.predicate, statement.object)
536
- end
537
- self.class.repository.insert(update_repository)
538
- new_subject.as(self.class)
539
- end
540
-
541
- ##
542
- # Rename this resource in the repository to the new given subject.
543
- # Changes are immediately saved to the repository.
544
- #
545
- # @param [RDF::Resource] new_subject
546
- # @return [Spira::Resource, String] new_resource
547
- def rename!(new_subject)
548
- new = copy_resource!(new_subject)
549
- object_statements = self.class.repository.query(:object => subject)
550
- update_repository = RDF::Repository.new
551
- object_statements.each do |statement|
552
- update_repository << RDF::Statement.new(statement.subject, statement.predicate, new.subject)
553
- end
554
- self.class.repository.insert(update_repository)
555
- destroy!(:completely)
556
- new
557
- end
558
-
559
- ## We have defined #each and can do this fun RDF stuff by default
560
- include ::RDF::Enumerable, ::RDF::Queryable
561
-
562
- ## Include the base validation functions
563
- include Spira::Resource::Validations
564
-
565
- end
566
- end
567
- end