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.
- data/README +151 -29
- data/VERSION +1 -1
- data/lib/spira.rb +86 -1
- data/lib/spira/errors.rb +94 -0
- data/lib/spira/resource.rb +53 -7
- data/lib/spira/resource/class_methods.rb +113 -47
- data/lib/spira/resource/dsl.rb +137 -70
- data/lib/spira/resource/instance_methods.rb +165 -56
- data/lib/spira/resource/validations.rb +32 -8
- data/lib/spira/type.rb +82 -0
- data/lib/spira/types.rb +25 -0
- data/lib/spira/types/any.rb +23 -0
- data/lib/spira/types/boolean.rb +31 -0
- data/lib/spira/types/float.rb +28 -0
- data/lib/spira/types/integer.rb +27 -0
- data/lib/spira/types/string.rb +27 -0
- data/lib/spira/version.rb +23 -0
- metadata +13 -9
@@ -2,128 +2,216 @@ require 'rdf/isomorphic'
|
|
2
2
|
|
3
3
|
module Spira
|
4
4
|
module Resource
|
5
|
+
|
6
|
+
##
|
7
|
+
# This module contains instance methods for Spira resources. See
|
8
|
+
# {Spira::Resource} for more information.
|
9
|
+
#
|
10
|
+
# @see Spira::Resource
|
11
|
+
# @see Spira::Resource::ClassMethods
|
12
|
+
# @see Spira::Resource::DSL
|
13
|
+
# @see Spira::Resource::Validations
|
5
14
|
module InstanceMethods
|
6
|
-
|
15
|
+
|
16
|
+
##
|
17
|
+
# This instance's URI.
|
18
|
+
#
|
19
|
+
# @return [RDF::URI]
|
7
20
|
attr_reader :uri
|
8
21
|
|
9
|
-
|
10
|
-
#
|
22
|
+
##
|
23
|
+
# Initialize a new Spira::Resource instance of this resource class. This
|
24
|
+
# method should not be called directly, use
|
25
|
+
# {Spira::Resource::ClassMethods#for} instead.
|
26
|
+
#
|
27
|
+
# @param [Any] identifier The URI or URI fragment for this instance
|
28
|
+
# @param [Hash] opts Default attributes for this instance
|
29
|
+
# @see Spira::Resource::ClassMethods#for
|
11
30
|
def initialize(identifier, opts = {})
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
31
|
+
@uri = self.class.uri_for(identifier)
|
32
|
+
reload(opts)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Reload all attributes for this instance, overwriting or setting
|
37
|
+
# defaults with the given opts. This resource will block if the
|
38
|
+
# underlying repository blocks the next time it accesses attributes.
|
39
|
+
#
|
40
|
+
# @param [Hash{Symbol => Any}] opts
|
41
|
+
# @option opts [Symbol] :any A property name. Sets the given property to the given value.
|
42
|
+
def reload(opts = {})
|
43
|
+
@attributes = promise { reload_attributes }
|
44
|
+
@original_attributes = promise { @attributes.force ; @original_attributes }
|
45
|
+
self.class.properties.each do |name, predicate|
|
46
|
+
attribute_set(name, opts[name]) unless opts[name].nil?
|
24
47
|
end
|
48
|
+
end
|
25
49
|
|
26
|
-
|
27
|
-
|
28
|
-
|
50
|
+
##
|
51
|
+
# Load this instance's attributes. Overwrite loaded values with attributes in the given options.
|
52
|
+
#
|
53
|
+
# @param [Hash] opts
|
54
|
+
# @return [Hash] @attributes
|
55
|
+
# @private
|
56
|
+
def reload_attributes()
|
57
|
+
if self.class.repository.nil?
|
58
|
+
raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
|
59
|
+
end
|
60
|
+
statements = self.class.repository.query(:subject => @uri)
|
61
|
+
@attributes = {}
|
29
62
|
|
30
|
-
|
31
|
-
if opts[:statements]
|
63
|
+
unless statements.empty?
|
32
64
|
# Set attributes for each statement corresponding to a predicate
|
33
65
|
self.class.properties.each do |name, property|
|
34
66
|
if self.class.is_list?(name)
|
67
|
+
# FIXME: This should not be an Array, but a Set. However, a set
|
68
|
+
# must compare its values to see if they already exist. This
|
69
|
+
# means any referenced relations will check their attributes and
|
70
|
+
# execute the promises to load those classes. Need an identity
|
71
|
+
# map of some sort to fix that.
|
35
72
|
values = []
|
36
|
-
|
37
|
-
unless
|
38
|
-
|
73
|
+
collection = statements.query(:subject => @uri, :predicate => property[:predicate])
|
74
|
+
unless collection.nil?
|
75
|
+
collection.each do |statement|
|
39
76
|
values << self.class.build_value(statement,property[:type])
|
40
77
|
end
|
41
78
|
end
|
42
79
|
attribute_set(name, values)
|
43
80
|
else
|
44
|
-
statement =
|
81
|
+
statement = statements.query(:subject => @uri, :predicate => property[:predicate]).first
|
45
82
|
attribute_set(name, self.class.build_value(statement, property[:type]))
|
46
83
|
end
|
47
84
|
end
|
48
|
-
else
|
49
|
-
self.class.properties.each do |name, predicate|
|
50
|
-
attribute_set(name, opts[name]) unless opts[name].nil?
|
51
|
-
end
|
52
|
-
|
53
85
|
end
|
54
86
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
#self.class.properties.each do |name, predicate|
|
61
|
-
# send(((name.to_s)+"=").to_sym, opts[name]) unless opts[name].nil?
|
62
|
-
#end
|
87
|
+
# We need to load and save the original attributes so we can remove
|
88
|
+
# them from the repository on save, since RDF will happily let us add
|
89
|
+
# as many triples for a subject and predicate as we want.
|
90
|
+
@original_attributes = {}
|
63
91
|
@original_attributes = @attributes.dup
|
64
92
|
@original_attributes.each do | name, value |
|
65
93
|
@original_attributes[name] = value.dup if value.is_a?(Array)
|
66
94
|
end
|
95
|
+
|
96
|
+
@attributes
|
67
97
|
end
|
68
|
-
|
69
|
-
|
98
|
+
|
99
|
+
##
|
100
|
+
# Remove the given attributes from the repository
|
101
|
+
#
|
102
|
+
# @param [Hash] attributes The hash of attributes to delete
|
103
|
+
# @param [Hash{Symbol => Any}] opts Options for deletion
|
104
|
+
# @option opts [true] :destroy_type Destroys the `RDF.type` statement associated with this class as well
|
105
|
+
# @private
|
106
|
+
def _destroy_attributes(attributes, opts = {})
|
70
107
|
repository = repository_for_attributes(attributes)
|
108
|
+
repository.insert([@uri, RDF.type, self.class.type]) if (self.class.type && opts[:destroy_type])
|
71
109
|
self.class.repository.delete(*repository)
|
72
110
|
end
|
73
|
-
|
111
|
+
|
112
|
+
##
|
113
|
+
# Remove this instance from the repository. Will not delete statements
|
114
|
+
# not associated with this model.
|
115
|
+
#
|
116
|
+
# @return [true, false] Whether or not the destroy was successful
|
74
117
|
def destroy!
|
75
|
-
_destroy_attributes(@attributes)
|
118
|
+
_destroy_attributes(@attributes, :destroy_type => true)
|
119
|
+
reload
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Remove all statements associated with this instance from the
|
124
|
+
# repository. This will delete statements unassociated with the current
|
125
|
+
# projection.
|
126
|
+
#
|
127
|
+
# @return [true, false] Whether or not the destroy was successful
|
128
|
+
def destroy_resource!
|
129
|
+
self.class.repository.delete([@uri,nil,nil])
|
76
130
|
end
|
77
131
|
|
132
|
+
##
|
133
|
+
# Save changes in this instance to the repository.
|
134
|
+
#
|
135
|
+
# @return [true, false] Whether or not the save was successful
|
78
136
|
def save!
|
79
137
|
if self.class.repository.nil?
|
80
138
|
raise RuntimeError, "#{self} is configured to use #{@repository_name} as a repository, but was unable to find it."
|
81
139
|
end
|
82
|
-
|
140
|
+
unless self.class.validators.empty?
|
83
141
|
errors.clear
|
84
|
-
|
142
|
+
self.class.validators.each do | validator | self.send(validator) end
|
85
143
|
if errors.empty?
|
86
144
|
_update!
|
87
145
|
else
|
88
|
-
raise(ValidationError, "Could not save #{self.inspect} due to validation errors: " + errors.join(';'))
|
146
|
+
raise(ValidationError, "Could not save #{self.inspect} due to validation errors: " + errors.each.join(';'))
|
89
147
|
end
|
90
148
|
else
|
91
149
|
_update!
|
92
150
|
end
|
93
151
|
end
|
94
152
|
|
153
|
+
##
|
154
|
+
# Save changes to the repository
|
155
|
+
#
|
156
|
+
# @private
|
95
157
|
def _update!
|
96
158
|
_destroy_attributes(@original_attributes)
|
97
159
|
self.class.repository.insert(*self)
|
98
160
|
@original_attributes = @attributes.dup
|
99
161
|
end
|
100
|
-
|
162
|
+
|
163
|
+
##
|
164
|
+
# The `RDF.type` associated with this class.
|
165
|
+
#
|
166
|
+
# @return [nil,RDF::URI] The RDF type associated with this instance's class.
|
101
167
|
def type
|
102
168
|
self.class.type
|
103
169
|
end
|
104
|
-
|
170
|
+
|
171
|
+
##
|
172
|
+
# `type` is a special property which is associated with the class and not
|
173
|
+
# the instance. Always raises a TypeError to try and assign it.
|
174
|
+
#
|
175
|
+
# @raise [TypeError] always
|
105
176
|
def type=(type)
|
106
177
|
raise TypeError, "Cannot reassign RDF.type for #{self}; consider appending to a has_many :types"
|
107
178
|
end
|
108
|
-
|
179
|
+
|
180
|
+
##
|
181
|
+
# A developer-friendly view of this projection
|
182
|
+
#
|
183
|
+
# @private
|
109
184
|
def inspect
|
110
185
|
"<#{self.class}:#{self.object_id} uri: #{@uri}>"
|
111
186
|
end
|
112
|
-
|
187
|
+
|
188
|
+
##
|
189
|
+
# Enumerate each RDF statement that makes up this projection. This makes
|
190
|
+
# each instance an `RDF::Enumerable`, with all of the nifty benefits
|
191
|
+
# thereof. See <http://rdf.rubyforge.org/RDF/Enumerable.html> for
|
192
|
+
# information on arguments.
|
193
|
+
#
|
194
|
+
# @see http://rdf.rubyforge.org/RDF/Enumerable.html
|
113
195
|
def each(*args, &block)
|
196
|
+
return RDF::Enumerator.new(self, :each) unless block_given?
|
114
197
|
repository = repository_for_attributes(@attributes)
|
115
198
|
repository.insert(RDF::Statement.new(@uri, RDF.type, type)) unless type.nil?
|
116
|
-
|
117
|
-
repository.each(*args, &block)
|
118
|
-
else
|
119
|
-
::Enumerable::Enumerator.new(self, :each)
|
120
|
-
end
|
199
|
+
repository.each(*args, &block)
|
121
200
|
end
|
122
201
|
|
202
|
+
##
|
203
|
+
# Safely set a given attribute. Currently not needed and marked as
|
204
|
+
# private.
|
205
|
+
#
|
206
|
+
# @private
|
123
207
|
def attribute_set(name, value)
|
124
208
|
@attributes[name] = value
|
125
209
|
end
|
126
210
|
|
211
|
+
##
|
212
|
+
# Safely get a given attribute.
|
213
|
+
#
|
214
|
+
# @private
|
127
215
|
def attribute_get(name)
|
128
216
|
case self.class.is_list?(name)
|
129
217
|
when true
|
@@ -133,12 +221,17 @@ module Spira
|
|
133
221
|
end
|
134
222
|
end
|
135
223
|
|
224
|
+
##
|
225
|
+
# Create an RDF::Repository for the given attributes hash. This could
|
226
|
+
# just as well be a class method but is only used here in #save! and
|
227
|
+
# #destroy!, so it is defined here for simplicity.
|
228
|
+
#
|
229
|
+
# @param [Hash] attributes The attributes to create a repository for
|
230
|
+
# @private
|
136
231
|
def repository_for_attributes(attributes)
|
137
232
|
repo = RDF::Repository.new
|
138
233
|
attributes.each do | name, attribute |
|
139
234
|
if self.class.is_list?(name)
|
140
|
-
#old = @repo.query(:subject => @uri, :predicate => predicate)
|
141
|
-
#@repo.delete(*old.to_a) unless old.empty?
|
142
235
|
new = []
|
143
236
|
attribute.each do |value|
|
144
237
|
value = self.class.build_rdf_value(value, self.class.properties[name][:type])
|
@@ -153,9 +246,15 @@ module Spira
|
|
153
246
|
repo
|
154
247
|
end
|
155
248
|
|
249
|
+
##
|
250
|
+
# Compare this instance with another instance. The comparison is done on
|
251
|
+
# an RDF level, and will work across subclasses as long as the attributes
|
252
|
+
# are the same.
|
253
|
+
#
|
254
|
+
# @see http://rdf.rubyforge.org/isomorphic/
|
156
255
|
def ==(other)
|
157
256
|
case other
|
158
|
-
# TODO: define behavior for equality on subclasses.
|
257
|
+
# TODO: define behavior for equality on subclasses.
|
159
258
|
when self.class
|
160
259
|
@uri == other.uri
|
161
260
|
when RDF::Enumerable
|
@@ -165,9 +264,19 @@ module Spira
|
|
165
264
|
end
|
166
265
|
end
|
167
266
|
|
267
|
+
##
|
268
|
+
# The validation errors collection associated with this instance.
|
269
|
+
#
|
270
|
+
# @return [Spira::Errors]
|
271
|
+
# @see Spira::Errors
|
272
|
+
def errors
|
273
|
+
@errors ||= Spira::Errors.new
|
274
|
+
end
|
168
275
|
|
276
|
+
## We have defined #each and can do this fun RDF stuff by default
|
169
277
|
include ::RDF::Enumerable, ::RDF::Queryable
|
170
278
|
|
279
|
+
## Include the base validation functions
|
171
280
|
include Spira::Resource::Validations
|
172
281
|
|
173
282
|
end
|
@@ -1,21 +1,45 @@
|
|
1
1
|
module Spira
|
2
2
|
module Resource
|
3
|
-
module Validations
|
4
3
|
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
##
|
5
|
+
# Instance methods relating to validations for a Spira resource. This
|
6
|
+
# includes the default assertions.
|
7
|
+
#
|
8
|
+
# @see Spira::Resource::InstanceMethods
|
9
|
+
# @see Spira::Resource::ClassMethods
|
10
|
+
# @see Spira::Resource::DSL
|
11
|
+
module Validations
|
8
12
|
|
9
|
-
|
10
|
-
|
13
|
+
##
|
14
|
+
# Assert a fact about this instance. If the given expression is false,
|
15
|
+
# an error will be noted.
|
16
|
+
#
|
17
|
+
# @example Assert that a title is correct
|
18
|
+
# assert(title == 'xyz', :title, 'bad title')
|
19
|
+
# @param [Any] boolean The expression to evaluate
|
20
|
+
# @param [Symbol] property The property or has_many to mark as incorrect on failure
|
21
|
+
# @param [String] message The message to record if this assertion fails
|
22
|
+
# @return [Void]
|
23
|
+
def assert(boolean, property, message)
|
24
|
+
errors.add(property, message) unless boolean
|
11
25
|
end
|
12
26
|
|
27
|
+
##
|
28
|
+
# A default helper assertion. Asserts that a given property is set.
|
29
|
+
#
|
30
|
+
# @param [Symbol] name The property to check
|
31
|
+
# @return [Void]
|
13
32
|
def assert_set(name)
|
14
|
-
assert(!(self.send(name).nil?), "#{name.to_s} cannot be nil")
|
33
|
+
assert(!(self.send(name).nil?), name, "#{name.to_s} cannot be nil")
|
15
34
|
end
|
16
35
|
|
36
|
+
##
|
37
|
+
# A default helper assertion. Asserts that a given property is numeric.
|
38
|
+
#
|
39
|
+
# @param [Symbol] name The property to check
|
40
|
+
# @return [Void]
|
17
41
|
def assert_numeric(name)
|
18
|
-
assert(self.send(name).is_a?(Numeric), "#{name.to_s} must be numeric (was #{self.send(name)})")
|
42
|
+
assert(self.send(name).is_a?(Numeric), name, "#{name.to_s} must be numeric (was #{self.send(name)})")
|
19
43
|
end
|
20
44
|
|
21
45
|
end
|
data/lib/spira/type.rb
CHANGED
@@ -0,0 +1,82 @@
|
|
1
|
+
module Spira
|
2
|
+
|
3
|
+
##
|
4
|
+
# Spira::Type can be included by classes to create new property types for
|
5
|
+
# Spira. These types are responsible for serialization a Ruby value into an
|
6
|
+
# `RDF::Value`, and deserialization of an `RDF::Value` into a Ruby value.
|
7
|
+
#
|
8
|
+
# A simple example:
|
9
|
+
#
|
10
|
+
# class Integer
|
11
|
+
#
|
12
|
+
# include Spira::Type
|
13
|
+
#
|
14
|
+
# def self.unserialize(value)
|
15
|
+
# value.object
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def self.serialize(value)
|
19
|
+
# RDF::Literal.new(value)
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# register_alias XSD.integer
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# This example will serialize and deserialize integers. It's included with
|
26
|
+
# Spira by default. It allows either of the following forms to declare an
|
27
|
+
# integer property on a Spira resource:
|
28
|
+
#
|
29
|
+
# property :age, :predicate => FOAF.age, :type => Integer
|
30
|
+
# property :age, :predicate => FOAF.age, :type => XSD.integer
|
31
|
+
#
|
32
|
+
# `Spira::Type`s include the RDF namespace and thus have all of the base RDF
|
33
|
+
# vocabularies available to them without the `RDF::` prefix.
|
34
|
+
#
|
35
|
+
# @see http://rdf.rubyforge.org/RDF/Value.html
|
36
|
+
# @see Spira::Resource
|
37
|
+
module Type
|
38
|
+
|
39
|
+
##
|
40
|
+
# Make the DSL available to a child class.
|
41
|
+
#
|
42
|
+
# @private
|
43
|
+
def self.included(child)
|
44
|
+
child.extend(ClassMethods)
|
45
|
+
Spira.type_alias(child,child)
|
46
|
+
end
|
47
|
+
|
48
|
+
include RDF
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
|
52
|
+
##
|
53
|
+
# Register an alias that this type can be referred to as, such as an RDF
|
54
|
+
# URI. The alias can be any object, symbol, or constant.
|
55
|
+
#
|
56
|
+
# @param [Any] identifier The new alias in property declarations for this class
|
57
|
+
# @return [Void]
|
58
|
+
def register_alias(any)
|
59
|
+
Spira.type_alias(any, self)
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Serialize a given value to RDF.
|
64
|
+
#
|
65
|
+
# @param [Any] value The Ruby value to be serialized
|
66
|
+
# @return [RDF::Value] The RDF form of this value
|
67
|
+
def serialize(value)
|
68
|
+
value
|
69
|
+
end
|
70
|
+
|
71
|
+
##
|
72
|
+
# Unserialize a given RDF value to Ruby
|
73
|
+
#
|
74
|
+
# @param [RDF::Value] value The RDF form of this value
|
75
|
+
# @return [Any] The Ruby form of this value
|
76
|
+
def unserialize(value)
|
77
|
+
value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|