spira 0.0.1.pre → 0.0.1

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,4 +1,37 @@
1
1
  module Spira
2
+
3
+ ##
4
+ # Spira::Resource is the main interface to Spira. Classes and modules
5
+ # include Spira::Resource to create projections of RDF data as a class. For
6
+ # an overview, see the {file:README}.
7
+ #
8
+ # Projections are a mapping of RDF predicates to fields.
9
+ #
10
+ # class Person
11
+ # include Spira::Resource
12
+ #
13
+ # property :name, :predicate => FOAF.name
14
+ # property :age, :predicate => FOAF.age, :type => Integer
15
+ # end
16
+ #
17
+ # RDF::URI('http://example.org/people/bob').as(Person) #=> <#Person @uri=http://example.org/people/bob>
18
+ #
19
+ # Spira resources include the RDF namespace, and can thus reference all of
20
+ # the default RDF.rb vocabularies without the RDF:: prefix:
21
+ #
22
+ # property :name, :predicate => FOAF.name
23
+ #
24
+ # The Spira::Resource documentation is broken into several parts, vaguely
25
+ # related by functionality:
26
+ # * {Spira::Resource::DSL} contains methods used during the declaration of a class or module
27
+ # * {Spira::Resource::ClassMethods} contains class methods for use by declared classes
28
+ # * {Spira::Resource::InstanceMethods} contains methods for use by instances of Spira resource classes
29
+ # * {Spira::Resource::Validations} contains some default validation functions
30
+ #
31
+ # @see Spira::Resource::DSL
32
+ # @see Spira::Resource::ClassMethods
33
+ # @see Spira::Resource::InstanceMethods
34
+ # @see Spira::Resource::Validations
2
35
  module Resource
3
36
 
4
37
  autoload :DSL, 'spira/resource/dsl'
@@ -6,20 +39,33 @@ module Spira
6
39
  autoload :InstanceMethods, 'spira/resource/instance_methods'
7
40
  autoload :Validations, 'spira/resource/validations'
8
41
 
42
+ ##
43
+ # When a child class includes Spira::Resource, this does the magic to make
44
+ # it a Spira resource.
45
+ #
46
+ # @private
9
47
  def self.included(child)
10
- child.extend DSL
11
- child.extend ClassMethods
12
- child.instance_eval do
13
- class << self
14
- attr_accessor :properties, :lists
48
+ # Don't do inclusion work twice. Checking for the properties accessor is
49
+ # a proxy for a proper check to see if this is a resource already. Ruby
50
+ # has already extended the child class' ancestors to include
51
+ # Spira::Resource by the time we get here.
52
+ # FIXME: Find a 'more correct' check.
53
+ unless child.respond_to?(:properties)
54
+ child.extend DSL
55
+ child.extend ClassMethods
56
+ child.instance_eval do
57
+ class << self
58
+ attr_accessor :properties, :lists
59
+ end
60
+ @properties = {}
61
+ @lists = {}
15
62
  end
16
- @properties = {}
17
- @lists = {}
18
63
  end
19
64
  end
20
65
 
21
66
  # This lets including classes reference vocabularies without the RDF:: prefix
22
67
  include RDF
68
+ include Spira::Types
23
69
  include InstanceMethods
24
70
 
25
71
  end
@@ -1,14 +1,21 @@
1
1
  module Spira
2
2
  module Resource
3
3
 
4
- # This module contains all class methods available to a Spira::Resource class
5
- #
4
+ ##
5
+ # This module contains all class methods available to a declared Spira::Resource class.
6
+ # {Spira::Resource} contains more information about Spira resources.
6
7
  #
8
+ # @see Spira::Resource
9
+ # @see Spira::Resource::InstanceMethods
10
+ # @see Spira::Resource::DSL
7
11
  module ClassMethods
8
- def repository=(repo)
9
- @repository = repo
10
- end
11
12
 
13
+ ##
14
+ # The current repository for this class
15
+ #
16
+ # @param [RDF::Repository] repo The repository
17
+ # @return [Void]
18
+ # @private
12
19
  def repository
13
20
  case @repository_name
14
21
  when nil
@@ -18,68 +25,127 @@ module Spira
18
25
  end
19
26
  end
20
27
 
21
- def oldrepo
22
- case
23
- #when !@repository.nil?
24
- # @repository
25
- when !@repository_name.nil?
26
- Spira.repository(@repository_name) || raise(RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it.")
27
- #@repository = Spira.repository(@repository_name)
28
- #if @repository.nil?
29
- # raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
30
- #end
31
- #@repository
32
- else
33
- @repository = Spira.repository(:default)
34
- if @repository.nil?
35
- raise RuntimeError, "#{self} has no configured repository and was unable to find a default repository."
36
- end
37
- @repository
28
+ ##
29
+ # Create a new projection instance of this class for the given URI. If a
30
+ # class has a base_uri given, and the argument is not an `RDF::URI`, the
31
+ # given identifier will be appended to the base URI.
32
+ #
33
+ # Spira does not have 'find' or 'create' functions. As RDF identifiers
34
+ # are globally unique, they all simply 'are'.
35
+ #
36
+ # On calling `for`, a new instance is created for the given URI. The
37
+ # first time access is attempted on a field, the repository will be
38
+ # queried for existing attributes, which will be used for the given URI.
39
+ # Underlying repositories are not accessed at the time of calling `for`.
40
+ #
41
+ # A class with a base URI may still be projected for any URI, whether or
42
+ # not it uses the given resource class' base URI.
43
+ #
44
+ # @raise [TypeError] if an RDF type is given in the attributes and one is
45
+ # given in the attributes.
46
+ # @raise [ArgumentError] if a non-URI is given and the class does not
47
+ # have a base URI.
48
+ # @overload for(uri, attributes = {})
49
+ # @param [RDF::URI] uri The URI to create an instance for
50
+ # @param [Hash{Symbol => Any}] attributes Initial attributes
51
+ # @overload for(identifier, attributes = {})
52
+ # @param [Any] uri The identifier to append to the base URI for this class
53
+ # @param [Hash{Symbol => Any}] attributes Initial attributes
54
+ # @return [Spira::Resource] The newly created instance
55
+ # @see http://rdf.rubyforge.org/RDF/URI.html
56
+ def for(identifier, attributes = {})
57
+ if !self.type.nil? && attributes[:type]
58
+ raise TypeError, "#{self} has an RDF type, #{self.type}, and cannot accept one as an argument."
38
59
  end
39
- #@repository
60
+ uri = uri_for(identifier)
61
+ self.new(uri, attributes)
40
62
  end
41
63
 
42
- def find(identifier)
43
- if repository.nil?
44
- raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
45
- end
46
- uri = case identifier
64
+ ##
65
+ # Creates a URI based on a base_uri and string or URI
66
+ #
67
+ # @param [Any] Identifier
68
+ # @return [RDF::URI]
69
+ # @raise [ArgumentError] If this class cannot create an identifier from the given string
70
+ # @see http://rdf.rubyforge.org/RDF/URI.html
71
+ def uri_for(identifier)
72
+ case identifier
47
73
  when RDF::URI
48
74
  identifier
49
75
  when String
50
- raise ArgumentError, "Cannot find #{self} by String without base_uri; RDF::URI required" if self.base_uri.nil?
76
+ uri = RDF::URI.new(identifier)
77
+ return uri if uri.absolute?
78
+ raise ArgumentError, "Cannot create identifier for #{self} by String without base_uri; RDF::URI required" if self.base_uri.nil?
51
79
  separator = self.base_uri.to_s[-1,1] == "/" ? '' : '/'
52
- RDF::URI.parse(self.base_uri.to_s + separator + identifier)
80
+ RDF::URI.new(self.base_uri.to_s + separator + identifier)
53
81
  else
54
- raise ArgumentError, "Cannot instantiate #{self} from #{identifier}, expected RDF::URI or String"
55
- end
56
- statements = self.repository.query(:subject => uri)
57
- if statements.empty?
58
- nil
59
- else
60
- self.new(identifier, :statements => statements)
82
+ raise ArgumentError, "Cannot create an identifier for #{self} from #{identifier}, expected RDF::URI or String"
61
83
  end
62
84
  end
63
-
85
+
86
+
87
+ ##
88
+ # The number of URIs projectable as a given class in the repository.
89
+ # This method is only valid for classes which declare a `type` with the
90
+ # `type` method in the DSL.
91
+ #
92
+ # @raise [TypeError] if the resource class does not have an RDF type declared
93
+ # @return [Integer] the count
94
+ # @see Spira::Resource::DSL
64
95
  def count
65
96
  raise TypeError, "Cannot count a #{self} without a reference type URI." if @type.nil?
66
97
  result = repository.query(:predicate => RDF.type, :object => @type)
67
98
  result.count
68
99
  end
69
100
 
70
- def create(name, attributes = {})
71
- # TODO: validate attributes
72
- unless @type.nil?
73
- if attributes[:type]
74
- raise TypeError, "Cannot assign type to new instance of #{self}; this class is associated with #{@type}"
101
+ ##
102
+ # Returns true if the given property is a has_many property, false otherwise
103
+ #
104
+ # @return [true, false]
105
+ def is_list?(property)
106
+ @lists.has_key?(property)
107
+ end
108
+
109
+ ##
110
+ # Handling inheritance
111
+ #
112
+ # @private
113
+ def inherited(child)
114
+ child.instance_eval do
115
+ include Spira::Resource
116
+ end
117
+ # FIXME: This is clearly brittle and ugly.
118
+ [:@base_uri, :@default_vocabulary, :@repository_name, :@type].each do |variable|
119
+ value = instance_variable_get(variable).nil? ? nil : instance_variable_get(variable).dup
120
+ child.instance_variable_set(variable, value)
121
+ end
122
+ [:@properties, :@lists, :@validators].each do |variable|
123
+ if child.instance_variable_get(variable).nil?
124
+ if instance_variable_get(variable).nil?
125
+ child.instance_variable_set(variable, nil)
126
+ else
127
+ child.instance_variable_set(variable, instance_variable_get(variable).dup)
128
+ end
129
+ elsif !(instance_variable_get(variable).nil?)
130
+ child.instance_variable_set(variable, instance_variable_get(variable).dup.merge(child.instance_variable_get(variable)))
75
131
  end
76
- attributes[:type] = @type
77
132
  end
78
- resource = self.new(name, attributes)
79
133
  end
80
134
 
81
- def is_list?(property)
82
- @lists.has_key?(property)
135
+ ##
136
+ # Handling module inclusions
137
+ #
138
+ # @private
139
+ def included(child)
140
+ inherited(child)
141
+ end
142
+
143
+ ##
144
+ # The list of validation functions for this projection
145
+ #
146
+ # @return [Array<Symbol>]
147
+ def validators
148
+ @validators ||= []
83
149
  end
84
150
 
85
151
  end
@@ -1,41 +1,122 @@
1
1
  require 'promise'
2
2
 
3
3
  module Spira
4
- module Resource
5
-
6
- # This module contains all user-exposed methods for use in building a model class.
7
- # It is used to extend classes that include Spira::Resource.
8
- # @see a little bit of magic in Spira::Resource#included as well--some
9
- # tricks need class_eval before this module is included.
10
- # @see Spira::Resource::ClassMethods for class methods available after class
11
- # definition
12
- # @see Spira::Resource::InstanceMethods for instance methods available after
13
- # class definition
4
+ module Resource
5
+
6
+ ##
7
+ # This module consists of Spira::Resource class methods which correspond to
8
+ # the Spira resource class declaration DSL. See {Spira::Resource} for more
9
+ # information.
10
+ #
11
+ # @see Spira::Resource
12
+ # @see Spira::Resource::ClassMethods
13
+ # @see Spira::Resource::InstanceMethods
14
+ # @see Spira::Resource::Validations
14
15
  module DSL
15
16
 
17
+ ##
18
+ # The name of the default repository to use for this class. This
19
+ # repository will be queried and written to instead of the :default
20
+ # repository.
21
+ #
22
+ # @param [Symbol] name
23
+ # @return [Void]
16
24
  def default_source(name)
17
25
  @repository_name = name
18
26
  @repository = Spira.repository(name)
19
27
  end
20
-
28
+
29
+ ##
30
+ # The base URI for this class. Attempts to create instances for non-URI
31
+ # objects will be appended to this base URI.
32
+ #
33
+ # @param [String, RDF::URI] base uri
34
+ # @return [Void]
21
35
  def base_uri(uri = nil)
22
36
  @base_uri = uri unless uri.nil?
23
37
  @base_uri
24
38
  end
25
-
39
+
40
+ ##
41
+ # The default vocabulary for this class. Setting a default vocabulary
42
+ # will allow properties to be defined without a `:predicate` option.
43
+ # Predicates will instead be created by appending the property name to
44
+ # the given string.
45
+ #
46
+ # @param [String, RDF::URI] base uri
47
+ # @return [Void]
26
48
  def default_vocabulary(uri)
27
49
  @default_vocabulary = uri
28
50
  end
29
51
 
52
+
53
+ ##
54
+ # Add a property to this class. A property is an accessor field that
55
+ # represents an RDF predicate.
56
+ #
57
+ # @example A simple string property
58
+ # property :name, :predicate => FOAF.name, :type => String
59
+ # @example A property which defaults to {Spira::Types::Any}
60
+ # property :name, :predicate => FOAF.name
61
+ # @example An integer property
62
+ # property :age, :predicate => FOAF.age, :type => Integer
63
+ # @param [Symbol] name The name of this property
64
+ # @param [Hash{Symbol => Any}] opts property options
65
+ # @option opts [RDF::URI] :predicate The RDF predicate which will refer to this property
66
+ # @option opts [Spira::Type, String] :type (Spira::Types::Any) The
67
+ # type for this property. If a Spira::Type is given, that class will be
68
+ # used to serialize and unserialize values. If a String is given, it
69
+ # should be the String form of a Spira::Resource class name (Strings are
70
+ # used to prevent issues with load order).
71
+ # @see Spira::Types
72
+ # @see Spira::Type
73
+ # @return [Void]
30
74
  def property(name, opts = {} )
31
75
  add_accessors(name,opts,:hash_accessors)
32
76
  end
33
77
 
78
+ ##
79
+ # The plural form of `property`. `Has_many` has the same options as
80
+ # `property`, but instead of a single value, a Ruby Array of objects will
81
+ # be created instead. Be warned that this should be a Set to match RDF
82
+ # semantics, but this is not currently implemented. Duplicate values of
83
+ # an array will be lost on save.
84
+ #
85
+ # @see Spira::Resource::DSL#property
34
86
  def has_many(name, opts = {})
35
87
  add_accessors(name,opts,:hash_accessors)
36
88
  @lists[name] = true
37
89
  end
38
90
 
91
+ ##
92
+ # Validate this model with the given validator function name.
93
+ #
94
+ # @example
95
+ # class Person
96
+ # include Spira::Resource
97
+ # property :name, :predicate => FOAF.name
98
+ # validate :is_awesome
99
+ # def is_awesome
100
+ # assert(name =~ /Thor/, :name, "not awesome")
101
+ # end
102
+ # end
103
+ # @param [Symbol] validator
104
+ # @return [Void]
105
+ def validate(validator)
106
+ validators << validator unless validators.include?(validator)
107
+ end
108
+
109
+
110
+ ##
111
+ # Associate an RDF type with this class. RDF resources can be multiple
112
+ # types at once, but if they have an `RDF.type` statement for the given
113
+ # URI, this class can #count them.
114
+ #
115
+ # @param [RDF::URI] uri The URI object of the `RDF.type` triple
116
+ # @return [Void]
117
+ # @see http://rdf.rubyforge.net/RDF/URI.html
118
+ # @see http://rdf.rubyforge.org/RDF.html#type-class_method
119
+ # @see Spira::Resource::ClassMethods#count
39
120
  def type(uri = nil)
40
121
  unless uri.nil?
41
122
  @type = case uri
@@ -48,41 +129,55 @@ module Spira
48
129
  @type
49
130
  end
50
131
 
132
+ # Build a Ruby value from an RDF value.
51
133
  #
52
134
  # @private
53
- def build_value(statement, type)
135
+ def build_value(statement, type, existing_relation = nil)
54
136
  case
55
137
  when statement == nil
56
138
  nil
57
- when type == String
58
- statement.object.object.to_s
59
- when type == Integer
60
- statement.object.object
61
- when type.is_a?(Symbol)
62
- klass = Kernel.const_get(type.to_s.capitalize)
63
- raise TypeError, "#{klass} is not a Spira Resource (referenced as #{type} by #{self}" unless klass.ancestors.include? Spira::Resource
64
- promise { klass.find(statement.object) || klass.create(statement.object) }
139
+ when type.is_a?(Class) && type.ancestors.include?(Spira::Type)
140
+ type.unserialize(statement.object)
141
+ when type.is_a?(Symbol) || type.is_a?(String)
142
+ klass = begin
143
+ Kernel.const_get(type.to_s)
144
+ rescue NameError
145
+ unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Resource)
146
+ raise TypeError, "#{type} is not a Spira Resource (referenced as #{type} by #{self}"
147
+ end
148
+ end
149
+ case
150
+ when false && existing_relation && (existing_relation.uri == statement.object.to_uri)
151
+ existing_relation
152
+ else
153
+ promise { klass.for(statement.object) ||
154
+ klass.create(statement.object) }
155
+ end
156
+ else
157
+ raise TypeError, "Unable to unserialize #{statement.object} for #{type}"
65
158
  end
66
159
  end
67
160
 
161
+ # Build an RDF value from a Ruby value for a property
68
162
  # @private
69
163
  def build_rdf_value(value, type)
70
164
  case
71
- when value.class.ancestors.include?(Spira::Resource)
165
+ when type.is_a?(Class) && type.ancestors.include?(Spira::Type)
166
+ type.serialize(value)
167
+ when value && value.class.ancestors.include?(Spira::Resource)
72
168
  value.uri
73
- when type == nil
74
- value
75
169
  when type == RDF::URI && value.is_a?(RDF::URI)
76
170
  value
77
- when type.is_a?(RDF::URI)
78
- RDF::Literal.new(value, :datatype => type)
79
171
  else
80
- RDF::Literal.new(value)
172
+ raise TypeError, "Unable to serialize #{value} for #{type}"
81
173
  end
82
174
  end
83
175
 
84
176
  private
85
177
 
178
+ ##
179
+ # Add getters and setters for a property or list.
180
+ # @private
86
181
  def add_accessors(name, opts, accessors_method)
87
182
  predicate = case
88
183
  when opts[:predicate]
@@ -93,8 +188,16 @@ module Spira
93
188
  separator = @default_vocabulary.to_s[-1,1] == "/" ? '' : '/'
94
189
  RDF::URI.new(@default_vocabulary.to_s + separator + name.to_s)
95
190
  end
96
-
97
- type = opts[:type] || String
191
+ type = case
192
+ when opts[:type].nil?
193
+ Spira::Types::Any
194
+ when opts[:type].is_a?(Symbol) || opts[:type].is_a?(String)
195
+ opts[:type]
196
+ when !(Spira.types[opts[:type]].nil?)
197
+ Spira.types[opts[:type]]
198
+ else
199
+ raise TypeError, "Unrecognized type: #{opts[:type]}"
200
+ end
98
201
  @properties[name] = {}
99
202
  @properties[name][:predicate] = predicate
100
203
  @properties[name][:type] = type
@@ -106,6 +209,11 @@ module Spira
106
209
 
107
210
  end
108
211
 
212
+ ##
213
+ # Getter and Setter methods for predicates.
214
+ # FIXME: this and add_accessors are from an older version in which
215
+ # multiple versions of accessors existed, and can be refactored.
216
+ # @private
109
217
  def hash_accessors(name, predicate, type)
110
218
  setter = lambda do |arg|
111
219
  attribute_set(name,arg)
@@ -118,47 +226,6 @@ module Spira
118
226
  [getter, setter]
119
227
  end
120
228
 
121
- def list_accessors(name, predicate, type)
122
-
123
- setter = lambda do |arg|
124
- old = @repo.query(:subject => @uri, :predicate => predicate)
125
- @repo.delete(*old.to_a) unless old.empty?
126
- new = []
127
- arg.each do |value|
128
- value = self.class.build_rdf_value(value, type)
129
- new << RDF::Statement.new(@uri, predicate, value)
130
- end
131
- @repo.insert(*new)
132
- end
133
-
134
- getter = lambda do
135
- values = []
136
- statements = @repo.query(:subject => @uri, :predicate => predicate)
137
- statements.each do |statement|
138
- values << self.class.build_value(statement, type)
139
- end
140
- values
141
- end
142
-
143
- [getter, setter]
144
- end
145
-
146
- def single_accessors(name, predicate, type)
147
- setter = lambda do |arg|
148
- old = @repo.query(:subject => @uri, :predicate => predicate)
149
- @repo.delete(*old.to_a) unless old.empty?
150
- arg = self.class.build_rdf_value(arg, type)
151
- @repo.insert(RDF::Statement.new(@uri, predicate, arg))
152
- end
153
-
154
- getter = lambda do
155
- statement = @repo.query(:subject => @uri, :predicate => predicate).first
156
- self.class.build_value(statement, type)
157
- end
158
-
159
- [getter, setter]
160
- end
161
-
162
229
  end
163
230
  end
164
231
  end