spira 0.0.8 → 0.0.9

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/AUTHORS CHANGED
@@ -1,2 +1,3 @@
1
1
  * Ben Lavender <blavender@gmail.com>
2
+ * Arto Bendiken <arto@bendiken.net>
2
3
  * Nicholas J Humfrey <njh@aelius.com>
data/CHANGES.md CHANGED
@@ -1,4 +1,18 @@
1
1
  # Changelog for Spira <http://github.com/datagraph/spira>
2
+
3
+ ## 0.0.9
4
+ * Fix a bug with Spira::Types::Any that prevented blank node objects
5
+ * Added Spira::Resource#copy, #copy!, #copy_resource!, and #rename!
6
+ * Fix a bug with fragment RDF::URI arguments that prevented correct URI
7
+ construction
8
+ * Added Spira::Resource.project(subject, attributes, &block), which creates a
9
+ new instance without attempting to perform any base_uri logic on the given
10
+ subject. This provides a supported API entry point for implementors to
11
+ create their own domain-specific URI construction.
12
+ * Updating a value to nil will now remove it from the repository on #save!
13
+ * Tweaks to dirty tracking to correctly catch both changed and updated values.
14
+ All tests pass for the first time with this change.
15
+
2
16
  ## 0.0.8
3
17
  * Remove type checking for repository addition. More power in return for
4
18
  slightly more difficult error messages.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.8
1
+ 0.0.9
@@ -0,0 +1,14 @@
1
+ # Extensions to other modules for use by Spira
2
+ #
3
+
4
+ class RDF::Literal
5
+
6
+ def self.unserialize(rdf_value)
7
+ rdf_value.object
8
+ end
9
+
10
+ def self.unserialize(ruby_value)
11
+ self.class.new(ruby_value)
12
+ end
13
+
14
+ end
@@ -43,7 +43,7 @@ module Spira
43
43
  # Spira does not have 'find' or 'create' functions. As RDF identifiers
44
44
  # are globally unique, they all simply 'are'.
45
45
  #
46
- # On calling `for`, a new instance is created for the given URI. The
46
+ # On calling `for`, a new projection is created for the given URI. The
47
47
  # first time access is attempted on a field, the repository will be
48
48
  # queried for existing attributes, which will be used for the given URI.
49
49
  # Underlying repositories are not accessed at the time of calling `for`.
@@ -66,11 +66,28 @@ module Spira
66
66
  # @return [Spira::Resource] The newly created instance
67
67
  # @see http://rdf.rubyforge.org/RDF/URI.html
68
68
  def for(identifier, attributes = {}, &block)
69
+ self.project(id_for(identifier), attributes, &block)
70
+ end
71
+
72
+ ##
73
+ # Create a new instance with the given subjet without any modification to
74
+ # the given subject at all. This method exists to provide an entry point
75
+ # for implementing classes that want to create a more intelligent .for
76
+ # and/or .id_for for their given use cases, such as simple string
77
+ # appending to base URIs or calculated URIs from other representations.
78
+ #
79
+ # @example Using simple string concatentation with base_uri in .for instead of joining delimiters
80
+ # def for(identifier, attributes = {}, &block)
81
+ # self.project(RDF::URI(self.base_uri.to_s + identifier.to_s), attributes, &block)
82
+ # end
83
+ # @param [RDF::URI, RDF::Node] subject
84
+ # @param [Hash{Symbol => Any}] attributes Initial attributes
85
+ # @return [Spira::Resource] the newly created instance
86
+ def project(subject, attributes = {}, &block)
69
87
  if !self.type.nil? && attributes[:type]
70
88
  raise TypeError, "#{self} has an RDF type, #{self.type}, and cannot accept one as an argument."
71
89
  end
72
- subject = id_for(identifier)
73
- instance = self.new(attributes.merge(:_subject => subject), &block)
90
+ self.new(attributes.merge(:_subject => subject), &block)
74
91
  end
75
92
 
76
93
  ##
@@ -92,23 +109,40 @@ module Spira
92
109
  # @return [RDF::URI, RDF::Node]
93
110
  # @raise [ArgumentError] If this class cannot create an identifier from the given argument
94
111
  # @see http://rdf.rubyforge.org/RDF/URI.html
112
+ # @see Spira::Resource.base_uri
113
+ # @see Spira::Resource.for
95
114
  def id_for(identifier)
96
- case
97
- # Catches RDF::URI and implementing subclasses
98
- when identifier.respond_to?(:to_uri)
99
- identifier.to_uri
100
- # Catches RDF::Nodes
115
+ case
116
+ # Absolute URI's go through unchanged
117
+ when identifier.is_a?(RDF::URI) && identifier.absolute?
118
+ identifier
119
+ # We don't have a base URI to join this fragment with, so go ahead and instantiate it as-is.
120
+ when identifier.is_a?(RDF::URI) && self.base_uri.nil?
121
+ identifier
122
+ # Blank nodes go through unchanged
101
123
  when identifier.respond_to?(:node?) && identifier.node?
102
124
  identifier
125
+ # Anything that can be an RDF::URI, we re-run this case statement
126
+ # on it for the fragment logic above.
127
+ when identifier.respond_to?(:to_uri) && !identifier.is_a?(RDF::URI)
128
+ id_for(identifier.to_uri)
129
+ # see comment with #to_uri above, this might be a fragment
103
130
  when identifier.is_a?(Addressable::URI)
104
- RDF::URI.new(identifier)
105
- # Treat identifier as a string, and create a URI out of it.
131
+ id_for(RDF::URI.new(identifier))
132
+ # This is a #to_s or a URI fragment with a base uri. We'll treat them the same.
133
+ # FIXME: when #/ makes it into RDF.rb proper, this can all be wrapped
134
+ # into the one case statement above.
106
135
  else
107
- uri = RDF::URI.new(identifier.to_s)
108
- return uri if uri.absolute?
109
- raise ArgumentError, "Cannot create identifier for #{self} by String without base_uri; RDF::URI required" if self.base_uri.nil?
110
- separator = self.base_uri.to_s[-1,1] =~ /(\/|#)/ ? '' : '/'
111
- RDF::URI.new(self.base_uri.to_s + separator + identifier.to_s)
136
+ uri = identifier.is_a?(RDF::URI) ? identifier : RDF::URI.new(identifier.to_s)
137
+ case
138
+ when uri.absolute?
139
+ uri
140
+ when self.base_uri.nil?
141
+ raise ArgumentError, "Cannot create identifier for #{self} by String without base_uri; an RDF::URI is required" if self.base_uri.nil?
142
+ else
143
+ separator = self.base_uri.to_s[-1,1] =~ /(\/|#)/ ? '' : '/'
144
+ RDF::URI.new(self.base_uri.to_s + separator + identifier.to_s)
145
+ end
112
146
  end
113
147
  end
114
148
 
@@ -12,8 +12,13 @@ module Spira
12
12
  # @see Spira::Resource::ClassMethods
13
13
  # @see Spira::Resource::DSL
14
14
  # @see Spira::Resource::Validations
15
- module InstanceMethods
16
-
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
+
17
22
  ##
18
23
  # This instance's URI.
19
24
  #
@@ -39,6 +44,7 @@ module Spira
39
44
  yield(self)
40
45
  save!
41
46
  end
47
+ self
42
48
  end
43
49
 
44
50
  ##
@@ -52,12 +58,18 @@ module Spira
52
58
  @cache = opts[:_cache] || RDF::Util::Cache.new
53
59
  @cache[subject] = self
54
60
  @dirty = {}
55
- # We need to save all attributes twice to track state changes in
56
- # mutable objects, like lists
57
- @attributes = promise { reload_attributes }
61
+ @attributes = {}
62
+ @attributes[:current] = {}
63
+ @attributes[:copied] = {}
58
64
  self.class.properties.each do |name, predicate|
59
- attribute_set(name, opts[name]) unless opts[name].nil?
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
60
71
  end
72
+ @attributes[:original] = promise { reload_attributes }
61
73
  end
62
74
 
63
75
  ##
@@ -68,8 +80,6 @@ module Spira
68
80
  def reload_attributes()
69
81
  statements = self.class.repository_or_fail.query(:subject => @subject)
70
82
  attributes = {}
71
- attributes[:original] = {}
72
- attributes[:current] = {}
73
83
 
74
84
  # Set attributes for each statement corresponding to a predicate
75
85
  self.class.properties.each do |name, property|
@@ -81,22 +91,12 @@ module Spira
81
91
  values << self.class.build_value(statement,property[:type], @cache)
82
92
  end
83
93
  end
84
- attributes[:current][name] = values
85
- attributes[:original][name] = values.dup
94
+ attributes[name] = values
86
95
  else
87
96
  statement = statements.query(:subject => @subject, :predicate => property[:predicate]).first unless statements.empty?
88
- attributes[:current][name] = self.class.build_value(statement, property[:type], @cache)
89
-
90
- # Lots of things like Fixnums and Nil can't be dup'd, but they are
91
- # all immutable, so if we can't dup, it's no problem for dirty tracking.
92
- begin
93
- attributes[:original][name] = attributes[:current][name].dup
94
- rescue TypeError
95
- attributes[:original][name] = attributes[:current][name]
96
- end
97
+ attributes[name] = self.class.build_value(statement, property[:type], @cache)
97
98
  end
98
99
  end
99
-
100
100
  attributes
101
101
  end
102
102
 
@@ -225,11 +225,12 @@ module Spira
225
225
  end
226
226
  self.class.repository_or_fail.insert(*repo)
227
227
  else
228
- 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])))
228
+ 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?
229
229
  end
230
230
  end
231
- @dirty[property] = nil
232
231
  @attributes[:original][property] = attribute_get(property)
232
+ @dirty[property] = nil
233
+ @attributes[:copied][property] = NOT_SET
233
234
  end
234
235
  self.class.repository_or_fail.insert(RDF::Statement.new(@subject, RDF.type, type)) unless type.nil?
235
236
  end
@@ -309,8 +310,13 @@ module Spira
309
310
  case
310
311
  when @dirty[name] == true
311
312
  true
312
- else
313
- @attributes[:current][name] != @attributes[:original][name]
313
+ else
314
+ case @attributes[:copied][name]
315
+ when NOT_SET
316
+ false
317
+ else
318
+ @attributes[:copied][name] != @attributes[:original][name]
319
+ end
314
320
  end
315
321
  end
316
322
  end
@@ -320,7 +326,26 @@ module Spira
320
326
  #
321
327
  # @private
322
328
  def attribute_get(name)
323
- @attributes[:current][name]
329
+ case @dirty[name]
330
+ when true
331
+ @attributes[:current][name]
332
+ else
333
+ case @attributes[:copied][name].equal?(NOT_SET)
334
+ when true
335
+ dup = if @attributes[:original][name].is_a?(Spira::Resource)
336
+ @attributes[:original][name]
337
+ else
338
+ begin
339
+ @attributes[:original][name].dup
340
+ rescue TypeError
341
+ @attributes[:original][name]
342
+ end
343
+ end
344
+ @attributes[:copied][name] = dup
345
+ when false
346
+ @attributes[:copied][name]
347
+ end
348
+ end
324
349
  end
325
350
 
326
351
  ##
@@ -449,6 +474,61 @@ module Spira
449
474
  self.class.repository.query(:subject => subject)
450
475
  end
451
476
 
477
+ ##
478
+ # Returns a new instance of this class with the new subject instead of self.subject
479
+ #
480
+ # @param [RDF::Resource] new_subject
481
+ # @return [Spira::Resource] copy
482
+ def copy(new_subject)
483
+ copy = self.class.for(new_subject)
484
+ self.class.properties.each_key { |property| copy.attribute_set(property, self.attribute_get(property)) }
485
+ copy
486
+ end
487
+
488
+ ##
489
+ # Returns a new instance of this class with the new subject instead of
490
+ # self.subject after saving the new copy to the repository.
491
+ #
492
+ # @param [RDF::Resource] new_subject
493
+ # @return [Spira::Resource, String] copy
494
+ def copy!(new_subject)
495
+ copy(new_subject).save!
496
+ end
497
+
498
+ ##
499
+ # Copies all data, including non-model data, about this resource to
500
+ # another URI. The copy is immediately saved to the repository.
501
+ #
502
+ # @param [RDF::Resource] new_subject
503
+ # @return [Spira::Resource, String] copy
504
+ def copy_resource!(new_subject)
505
+ new_subject = self.class.id_for(new_subject)
506
+ update_repository = RDF::Repository.new
507
+ data.each do |statement|
508
+ update_repository << RDF::Statement.new(new_subject, statement.predicate, statement.object)
509
+ end
510
+ self.class.repository.insert(update_repository)
511
+ new_subject.as(self.class)
512
+ end
513
+
514
+ ##
515
+ # Rename this resource in the repository to the new given subject.
516
+ # Changes are immediately saved to the repository.
517
+ #
518
+ # @param [RDF::Resource] new_subject
519
+ # @return [Spira::Resource, String] new_resource
520
+ def rename!(new_subject)
521
+ new = copy_resource!(new_subject)
522
+ object_statements = self.class.repository.query(:object => subject)
523
+ update_repository = RDF::Repository.new
524
+ object_statements.each do |statement|
525
+ update_repository << RDF::Statement.new(statement.subject, statement.predicate, new.subject)
526
+ end
527
+ self.class.repository.insert(update_repository)
528
+ destroy!(:completely)
529
+ new
530
+ end
531
+
452
532
  ## We have defined #each and can do this fun RDF stuff by default
453
533
  include ::RDF::Enumerable, ::RDF::Queryable
454
534
 
@@ -12,12 +12,12 @@ module Spira::Types
12
12
  include Spira::Type
13
13
 
14
14
  def self.unserialize(value)
15
- value.respond_to?(:to_uri) ? value.to_uri : value.object
15
+ value.respond_to?(:object) ? value.object : value
16
16
  end
17
17
 
18
18
  def self.serialize(value)
19
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
+ value.is_a?(RDF::Value) ? value : RDF::Literal.new(value)
21
21
  end
22
22
 
23
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 = 8
5
+ TINY = 9
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
- - 8
9
- version: 0.0.8
8
+ - 9
9
+ version: 0.0.9
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-09-22 00:00:00 -05:00
18
+ date: 2010-10-20 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -70,8 +70,8 @@ dependencies:
70
70
  segments:
71
71
  - 0
72
72
  - 2
73
- - 2
74
- version: 0.2.2
73
+ - 3
74
+ version: 0.2.3
75
75
  type: :runtime
76
76
  version_requirements: *id004
77
77
  - !ruby/object:Gem::Dependency
@@ -119,6 +119,7 @@ files:
119
119
  - lib/spira/base.rb
120
120
  - lib/spira/errors.rb
121
121
  - lib/spira/exceptions.rb
122
+ - lib/spira/extensions.rb
122
123
  - lib/spira/resource/class_methods.rb
123
124
  - lib/spira/resource/dsl.rb
124
125
  - lib/spira/resource/instance_methods.rb