spira 0.0.12 → 0.5.0

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