spira 0.0.1.pre → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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