paulcarey-relaxdb 0.1.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.
@@ -0,0 +1,29 @@
1
+ module RelaxDB
2
+
3
+ class BelongsToProxy
4
+
5
+ attr_reader :target
6
+
7
+ def initialize(client, relationship)
8
+ @client = client
9
+ @relationship = relationship
10
+ @target = nil
11
+ end
12
+
13
+ def target
14
+ return @target if @target
15
+
16
+ id = @client.instance_variable_get("@#{@relationship}_id")
17
+ @target = RelaxDB.load(id) if id
18
+ end
19
+
20
+ def target=(new_target)
21
+ id = new_target ? new_target._id : nil
22
+ @client.instance_variable_set("@#{@relationship}_id", id)
23
+
24
+ @target = new_target
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,50 @@
1
+ module RelaxDB
2
+
3
+ class DesignDocument
4
+
5
+ def initialize(client_class, data)
6
+ @client_class = client_class
7
+ @data = data
8
+ end
9
+
10
+ def add_map_view(view_name, function)
11
+ add_view(view_name, "map", function)
12
+ end
13
+
14
+ def add_reduce_view(view_name, function)
15
+ add_view(view_name, "reduce", function)
16
+ end
17
+
18
+ def add_view(view_name, type, function)
19
+ @data["views"] ||= {}
20
+ @data["views"][view_name] ||= {}
21
+ @data["views"][view_name][type] = function
22
+ self
23
+ end
24
+
25
+ def save
26
+ database = RelaxDB.db
27
+ resp = database.put("#{@data['_id']}", @data.to_json)
28
+ @data["_rev"] = JSON.parse(resp.body)["rev"]
29
+ self
30
+ end
31
+
32
+ def self.get(client_class)
33
+ begin
34
+ database = RelaxDB.db
35
+ resp = database.get("_design/#{client_class}")
36
+ DesignDocument.new(client_class, JSON.parse(resp.body))
37
+ rescue => e
38
+ DesignDocument.new(client_class, {"_id" => "_design/#{client_class}"} )
39
+ end
40
+ end
41
+
42
+ def destroy!
43
+ # Implicitly prevent the object from being resaved by failing to update its revision
44
+ RelaxDB.db.delete("#{@data["_id"]}?rev=#{@data["_rev"]}")
45
+ self
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,298 @@
1
+ module RelaxDB
2
+
3
+ class Document
4
+
5
+ # Used to store validation messages
6
+ attr_accessor :errors
7
+
8
+ # Define properties and property methods
9
+
10
+ def self.property(prop, opts={})
11
+ # Class instance varibles are not inherited, so the default properties must be explicitly listed
12
+ # Perhaps a better solution exists. Revise. I think extlib contains a solution for this...
13
+ @properties ||= [:_id, :_rev]
14
+ @properties << prop
15
+
16
+ define_method(prop) do
17
+ instance_variable_get("@#{prop}".to_sym)
18
+ end
19
+
20
+ define_method("#{prop}=") do |val|
21
+ instance_variable_set("@#{prop}".to_sym, val)
22
+ end
23
+
24
+ if opts[:default]
25
+ define_method("set_default_#{prop}") do
26
+ default = opts[:default]
27
+ default = default.is_a?(Proc) ? default.call : default
28
+ instance_variable_set("@#{prop}".to_sym, default)
29
+ end
30
+ end
31
+
32
+ if opts[:validator]
33
+ define_method("validate_#{prop}") do |prop_val|
34
+ opts[:validator].call(prop_val)
35
+ end
36
+ end
37
+
38
+ if opts[:validation_msg]
39
+ define_method("#{prop}_validation_msg") do
40
+ opts[:validation_msg]
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ def self.properties
47
+ # Ensure that classes that don't define their own properties still function as CouchDB objects
48
+ @properties ||= [:_id, :_rev]
49
+ end
50
+
51
+ def properties
52
+ self.class.properties
53
+ end
54
+
55
+ # Specifying these properties here (after property method has been defined)
56
+ # is kinda ugly. Consider a better solution.
57
+ property :_id
58
+ property :_rev
59
+
60
+ def initialize(hash={})
61
+ # The default _id will be overwritten if loaded from CouchDB
62
+ self._id = UuidGenerator.uuid
63
+
64
+ @errors = {}
65
+
66
+ # Set default properties if this object has not known CouchDB
67
+ unless hash["_rev"]
68
+ properties.each do |prop|
69
+ if methods.include?("set_default_#{prop}")
70
+ send("set_default_#{prop}")
71
+ end
72
+ end
73
+ end
74
+
75
+ set_attributes(hash)
76
+ end
77
+
78
+ def set_attributes(data)
79
+ data.each do |key, val|
80
+ # Only set instance variables on creation - object references are resolved on demand
81
+
82
+ # If the variable name ends in _at try to convert it to a Time
83
+ if key =~ /_at$/
84
+ val = Time.local(*ParseDate.parsedate(val)) rescue val
85
+ end
86
+
87
+ # Ignore param keys that don't have a corresponding writer
88
+ # This allows us to comfortably accept a hash containing superflous data
89
+ # such as a params hash in a controller
90
+ if methods.include? "#{key}="
91
+ send("#{key}=".to_sym, val)
92
+ end
93
+
94
+ end
95
+ end
96
+
97
+ def inspect
98
+ s = "#<#{self.class}:#{self.object_id}"
99
+ properties.each do |prop|
100
+ prop_val = instance_variable_get("@#{prop}".to_sym)
101
+ s << ", #{prop}: #{prop_val.inspect}" if prop_val
102
+ end
103
+ self.class.belongs_to_rels.each do |relationship|
104
+ id = instance_variable_get("@#{relationship}_id".to_sym)
105
+ s << ", #{relationship}_id: #{id}" if id
106
+ end
107
+ s << ">"
108
+ end
109
+
110
+ def to_json
111
+ data = {}
112
+ self.class.belongs_to_rels.each do |relationship|
113
+ id = instance_variable_get("@#{relationship}_id".to_sym)
114
+ data["#{relationship}_id"] = id if id
115
+ end
116
+ properties.each do |prop|
117
+ prop_val = instance_variable_get("@#{prop}".to_sym)
118
+ data["#{prop}"] = prop_val if prop_val
119
+ end
120
+ data["class"] = self.class.name
121
+ data.to_json
122
+ end
123
+
124
+ def save
125
+ set_created_at_if_new
126
+
127
+ if validates?
128
+ resp = RelaxDB.db.put("#{_id}", to_json)
129
+ self._rev = JSON.parse(resp.body)["rev"]
130
+ self
131
+ else
132
+ false
133
+ end
134
+ end
135
+
136
+ def validates?
137
+ success = true
138
+ properties.each do |prop|
139
+ if methods.include? "validate_#{prop}"
140
+ prop_val = instance_variable_get("@#{prop}")
141
+ unless send("validate_#{prop}", prop_val)
142
+ success = false
143
+ if methods.include? "#{prop}_validation_msg"
144
+ @errors["#{prop}".to_sym] = send("#{prop}_validation_msg")
145
+ end
146
+ end
147
+ end
148
+ end
149
+ success
150
+ end
151
+
152
+ def unsaved?
153
+ instance_variable_get(:@_rev).nil?
154
+ end
155
+
156
+ def set_created_at_if_new
157
+ if unsaved? and methods.include? "created_at"
158
+ # Don't override it if it's already been set
159
+ unless instance_variable_get(:@created_at)
160
+ instance_variable_set(:@created_at, Time.now)
161
+ end
162
+ end
163
+ end
164
+
165
+ def create_or_get_proxy(klass, relationship, opts=nil)
166
+ proxy_sym = "@proxy_#{relationship}".to_sym
167
+ proxy = instance_variable_get(proxy_sym)
168
+ unless proxy
169
+ proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
170
+ end
171
+ instance_variable_set(proxy_sym, proxy)
172
+ proxy
173
+ end
174
+
175
+ # Returns true if CouchDB considers other to be the same as self
176
+ def ==(other)
177
+ other && _id == other._id
178
+ end
179
+
180
+ # Deprecated. This method was experimental and will be removed
181
+ # once multi key GETs are available in CouchDB.
182
+ def self.references_many(relationship, opts={})
183
+ # Treat the representation as a standard property
184
+ properties << relationship
185
+
186
+ # Keep track of the relationship so peers can be disassociated on destroy
187
+ @references_many_rels ||= []
188
+ @references_many_rels << relationship
189
+
190
+ define_method(relationship) do
191
+ array_sym = "@#{relationship}".to_sym
192
+ instance_variable_set(array_sym, []) unless instance_variable_defined? array_sym
193
+
194
+ create_or_get_proxy(RelaxDB::ReferencesManyProxy, relationship, opts)
195
+ end
196
+
197
+ define_method("#{relationship}=") do |val|
198
+ # Sharp edge - do not invoke this method
199
+ instance_variable_set("@#{relationship}".to_sym, val)
200
+ end
201
+ end
202
+
203
+ def self.references_many_rels
204
+ # Don't force clients to check its instantiated
205
+ @references_many_rels ||= []
206
+ end
207
+
208
+ def self.has_many(relationship, opts={})
209
+ @has_many_rels ||= []
210
+ @has_many_rels << relationship
211
+
212
+ define_method(relationship) do
213
+ create_or_get_proxy(HasManyProxy, relationship, opts)
214
+ end
215
+
216
+ define_method("#{relationship}=") do
217
+ raise "You may not currently assign to a has_many relationship - may be implemented"
218
+ end
219
+ end
220
+
221
+ def self.has_many_rels
222
+ # Don't force clients to check its instantiated
223
+ @has_many_rels ||= []
224
+ end
225
+
226
+ def self.has_one(relationship)
227
+ @has_one_rels ||= []
228
+ @has_one_rels << relationship
229
+
230
+ define_method(relationship) do
231
+ create_or_get_proxy(HasOneProxy, relationship).target
232
+ end
233
+
234
+ define_method("#{relationship}=") do |new_target|
235
+ create_or_get_proxy(HasOneProxy, relationship).target = new_target
236
+ end
237
+ end
238
+
239
+ def self.has_one_rels
240
+ @has_one_rels ||= []
241
+ end
242
+
243
+ def self.belongs_to(relationship)
244
+ @belongs_to_rels ||= []
245
+ @belongs_to_rels << relationship
246
+
247
+ define_method(relationship) do
248
+ create_or_get_proxy(BelongsToProxy, relationship).target
249
+ end
250
+
251
+ define_method("#{relationship}=") do |new_target|
252
+ create_or_get_proxy(BelongsToProxy, relationship).target = new_target
253
+ end
254
+
255
+ # Allows all writers to be invoked from the hash passed to initialize
256
+ define_method("#{relationship}_id=") do |id|
257
+ instance_variable_set("@#{relationship}_id".to_sym, id)
258
+ end
259
+
260
+ end
261
+
262
+ def self.belongs_to_rels
263
+ # Don't force clients to check that it's instantiated
264
+ @belongs_to_rels ||= []
265
+ end
266
+
267
+ def self.all_relationships
268
+ belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
269
+ end
270
+
271
+ def self.all
272
+ @all_delegator ||= AllDelegator.new(self)
273
+ end
274
+
275
+ # destroy! nullifies all relationships with peers and children before deleting
276
+ # itself in CouchDB
277
+ # The nullification and deletion are not performed in a transaction
278
+ def destroy!
279
+ self.class.references_many_rels.each do |rel|
280
+ send(rel).clear
281
+ end
282
+
283
+ self.class.has_many_rels.each do |rel|
284
+ send(rel).clear
285
+ end
286
+
287
+ self.class.has_one_rels.each do |rel|
288
+ send("#{rel}=".to_sym, nil)
289
+ end
290
+
291
+ # Implicitly prevent the object from being resaved by failing to update its revision
292
+ RelaxDB.db.delete("#{_id}?rev=#{_rev}")
293
+ self
294
+ end
295
+
296
+ end
297
+
298
+ end
@@ -0,0 +1,81 @@
1
+ module RelaxDB
2
+
3
+ class HasManyProxy
4
+
5
+ include Enumerable
6
+
7
+ def initialize(client, relationship, opts)
8
+ @client = client
9
+ @relationship = relationship
10
+ @opts = opts
11
+
12
+ @target_class = opts[:class]
13
+ @relationship_as_viewed_by_target = (opts[:known_as] || client.class.name.downcase).to_s
14
+
15
+ @children = load_children
16
+ end
17
+
18
+ def <<(obj)
19
+ return false unless obj.validates?
20
+ return false if @children.include?(obj)
21
+
22
+ obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
23
+ obj.save
24
+ @children << obj
25
+ self
26
+ end
27
+
28
+ def clear
29
+ @children.each do |c|
30
+ break_back_link c
31
+ end
32
+ @children.clear
33
+ end
34
+
35
+ def delete(obj)
36
+ obj = @children.delete(obj)
37
+ break_back_link(obj) if obj
38
+ end
39
+
40
+ def break_back_link(obj)
41
+ if obj
42
+ obj.send("#{@relationship_as_viewed_by_target}=".to_sym, nil)
43
+ obj.save
44
+ end
45
+ end
46
+
47
+ def empty?
48
+ @children.empty?
49
+ end
50
+
51
+ def size
52
+ @children.size
53
+ end
54
+
55
+ def [](*args)
56
+ @children[*args]
57
+ end
58
+
59
+ def each(&blk)
60
+ @children.each(&blk)
61
+ end
62
+
63
+ def reload
64
+ @children = load_children
65
+ end
66
+
67
+ def load_children
68
+ view_path = "_view/#{@client.class}/#{@relationship}?key=\"#{@client._id}\""
69
+ design_doc = @client.class
70
+ view_name = @relationship
71
+ map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
72
+ @children = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
73
+ end
74
+
75
+ def inspect
76
+ @children.inspect
77
+ end
78
+
79
+ end
80
+
81
+ end
@@ -0,0 +1,45 @@
1
+ module RelaxDB
2
+
3
+ class HasOneProxy
4
+
5
+ def initialize(client, relationship)
6
+ @client = client
7
+ @relationship = relationship
8
+ @target_class = @relationship.to_s.capitalize
9
+ @relationship_as_viewed_by_target = client.class.to_s.downcase
10
+
11
+ @target = nil
12
+ end
13
+
14
+ def target
15
+ return @target if @target
16
+ @target = load_target
17
+ end
18
+
19
+ # All database changes performed by this method would ideally be done in a transaction
20
+ def target=(new_target)
21
+ # Nullify any existing relationship on assignment
22
+ old_target = target
23
+ if old_target
24
+ old_target.send("#{@relationship_as_viewed_by_target}=".to_sym, nil)
25
+ old_target.save
26
+ end
27
+
28
+ @target = new_target
29
+ unless @target.nil?
30
+ @target.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
31
+ @target.save
32
+ end
33
+ end
34
+
35
+ def load_target
36
+ design_doc = @client.class
37
+ view_name = @relationship
38
+ view_path = "_view/#{design_doc}/#{view_name}?key=\"#{@client._id}\""
39
+ map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
40
+ RelaxDB.retrieve(view_path, design_doc, view_name, map_function)[0]
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,48 @@
1
+ module RelaxDB
2
+
3
+ # A Query is used to build the query string made against a view
4
+ # All parameter values are first JSON encoded and then URL encoded
5
+ # Nil values are set to the empty string
6
+ # All parameter calls return self so calls may be chained => q.startkey("foo").endkey("bar").count(2)
7
+
8
+ #
9
+ # The query object is currently inconsistent with the RelaxDB object idiom. Consider
10
+ # paul = User.new(:name => "paul").save; Event.new(:host=>paul).save
11
+ # but an event query requires
12
+ # Event.all.sorted_by(:host_id) { |q| q.key(paul._id) }
13
+ # rather than
14
+ # Event.all.sorted_by(:host) { |q| q.key(paul) }
15
+ # I feel that both forms should be supported
16
+ #
17
+ class Query
18
+
19
+ @@params = %w(key startkey startkey_docid endkey endkey_docid count update descending skip group)
20
+
21
+ @@params.each do |param|
22
+ define_method(param.to_sym) do |val|
23
+ val ||= ""
24
+ instance_variable_set("@#{param}".to_sym, val)
25
+ self
26
+ end
27
+ end
28
+
29
+ def initialize(design_doc, view_name)
30
+ @design_doc = design_doc
31
+ @view_name = view_name
32
+ end
33
+
34
+ def view_path
35
+ uri = "_view/#{@design_doc}/#{@view_name}"
36
+
37
+ query = ""
38
+ @@params.each do |param|
39
+ val = instance_variable_get("@#{param}")
40
+ query << "&#{param}=#{::CGI::escape(val.to_json)}" if val
41
+ end
42
+
43
+ uri << query.sub(/^&/, "?")
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,99 @@
1
+ module RelaxDB
2
+
3
+ class ReferencesManyProxy
4
+
5
+ include Enumerable
6
+
7
+ def initialize(client, relationship, opts)
8
+ @client = client
9
+ @relationship = relationship
10
+
11
+ @target_class = opts[:class]
12
+ @relationship_as_viewed_by_target = opts[:known_as].to_s
13
+ end
14
+
15
+ def <<(obj, reciprocal_invocation=false)
16
+ return false if peer_ids.include? obj._id
17
+
18
+ @peers << obj if @peers
19
+ peer_ids << obj._id
20
+
21
+ unless reciprocal_invocation
22
+ # Set the other side of the relationship, ensuring this method isn't called again
23
+ obj.send(@relationship_as_viewed_by_target).send(:<<, @client, true)
24
+
25
+ # Bulk save to ensure relationship is persisted on both sides
26
+ RelaxDB.bulk_save(@client, obj)
27
+ end
28
+
29
+ self
30
+ end
31
+
32
+ def clear
33
+ resolve
34
+ @peers.each do |peer|
35
+ peer.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
36
+ end
37
+
38
+ # Important to resolve in the database before in memory, although an examination of the
39
+ # contents of the bulk_save will look wrong as this object will still list all its peers
40
+ RelaxDB.bulk_save(@client, *@peers)
41
+
42
+ peer_ids.clear
43
+ @peers.clear
44
+ end
45
+
46
+ def delete(obj)
47
+ deleted = obj.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
48
+ if deleted
49
+ delete_from_self(obj)
50
+ RelaxDB.bulk_save(@client, obj)
51
+ end
52
+ deleted
53
+ end
54
+
55
+ def delete_from_self(obj)
56
+ @peers.delete(obj) if @peers
57
+ peer_ids.delete(obj._id)
58
+ end
59
+
60
+ def empty?
61
+ peer_ids.empty?
62
+ end
63
+
64
+ def size
65
+ peer_ids.size
66
+ end
67
+
68
+ def [](*args)
69
+ resolve
70
+ @peers[*args]
71
+ end
72
+
73
+ def each(&blk)
74
+ resolve
75
+ @peers.each(&blk)
76
+ end
77
+
78
+ def inspect
79
+ @client.instance_variable_get("@#{@relationship}".to_sym).inspect
80
+ end
81
+
82
+ private
83
+
84
+ def peer_ids
85
+ @client.instance_variable_get("@#{@relationship}".to_sym)
86
+ end
87
+
88
+ # Resolves the actual ids into real objects via a single GET to CouchDB. Called internally by each
89
+ def resolve
90
+ design_doc = @client.class
91
+ view_name = @relationship
92
+ view_path = "_view/#{design_doc}/#{view_name}?key=\"#{@client._id}\""
93
+ map_function = ViewCreator.has_many_through(@target_class, @relationship_as_viewed_by_target)
94
+ @peers = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
95
+ end
96
+
97
+ end
98
+
99
+ end