relaxdb 0.3.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +200 -0
  3. data/Rakefile +63 -0
  4. data/docs/spec_results.html +1059 -0
  5. data/lib/more/atomic_bulk_save_support.rb +18 -0
  6. data/lib/more/grapher.rb +48 -0
  7. data/lib/relaxdb.rb +50 -0
  8. data/lib/relaxdb/all_delegator.rb +44 -0
  9. data/lib/relaxdb/belongs_to_proxy.rb +29 -0
  10. data/lib/relaxdb/design_doc.rb +57 -0
  11. data/lib/relaxdb/document.rb +600 -0
  12. data/lib/relaxdb/extlib.rb +24 -0
  13. data/lib/relaxdb/has_many_proxy.rb +101 -0
  14. data/lib/relaxdb/has_one_proxy.rb +42 -0
  15. data/lib/relaxdb/migration.rb +40 -0
  16. data/lib/relaxdb/migration_version.rb +21 -0
  17. data/lib/relaxdb/net_http_server.rb +61 -0
  18. data/lib/relaxdb/paginate_params.rb +53 -0
  19. data/lib/relaxdb/paginator.rb +88 -0
  20. data/lib/relaxdb/query.rb +76 -0
  21. data/lib/relaxdb/references_many_proxy.rb +97 -0
  22. data/lib/relaxdb/relaxdb.rb +250 -0
  23. data/lib/relaxdb/server.rb +109 -0
  24. data/lib/relaxdb/taf2_curb_server.rb +63 -0
  25. data/lib/relaxdb/uuid_generator.rb +21 -0
  26. data/lib/relaxdb/validators.rb +11 -0
  27. data/lib/relaxdb/view_object.rb +34 -0
  28. data/lib/relaxdb/view_result.rb +18 -0
  29. data/lib/relaxdb/view_uploader.rb +49 -0
  30. data/lib/relaxdb/views.rb +114 -0
  31. data/readme.rb +80 -0
  32. data/spec/belongs_to_spec.rb +124 -0
  33. data/spec/callbacks_spec.rb +80 -0
  34. data/spec/derived_properties_spec.rb +112 -0
  35. data/spec/design_doc_spec.rb +34 -0
  36. data/spec/doc_inheritable_spec.rb +100 -0
  37. data/spec/document_spec.rb +545 -0
  38. data/spec/has_many_spec.rb +202 -0
  39. data/spec/has_one_spec.rb +123 -0
  40. data/spec/migration_spec.rb +97 -0
  41. data/spec/migration_version_spec.rb +28 -0
  42. data/spec/paginate_params_spec.rb +15 -0
  43. data/spec/paginate_spec.rb +360 -0
  44. data/spec/query_spec.rb +90 -0
  45. data/spec/references_many_spec.rb +173 -0
  46. data/spec/relaxdb_spec.rb +364 -0
  47. data/spec/server_spec.rb +32 -0
  48. data/spec/spec.opts +1 -0
  49. data/spec/spec_helper.rb +65 -0
  50. data/spec/spec_models.rb +199 -0
  51. data/spec/view_by_spec.rb +76 -0
  52. data/spec/view_object_spec.rb +47 -0
  53. data/spec/view_spec.rb +23 -0
  54. metadata +137 -0
@@ -0,0 +1,18 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require 'relaxdb'
3
+ require File.dirname(__FILE__) + '/../../spec/spec_models.rb'
4
+
5
+ RelaxDB.configure :host => "localhost", :port => 5984
6
+ RelaxDB.delete_db "relaxdb_spec" rescue :ok
7
+ RelaxDB.use_db "relaxdb_spec"
8
+
9
+ a1 = Atom.new.save!
10
+ a1_dup = a1.dup
11
+ a1.save!
12
+ begin
13
+ RelaxDB.bulk_save! a1_dup
14
+ puts "Atomic bulk_save _not_ supported"
15
+ rescue RelaxDB::UpdateConflict
16
+ puts "Atomic bulk_save supported"
17
+ end
18
+
@@ -0,0 +1,48 @@
1
+ module RelaxDB
2
+
3
+ #
4
+ # The GraphCreator uses dot to create a graphical model of an entire CouchDB database
5
+ # It probably only makes sense to run it on a database of a limited size
6
+ # The created graphs can be very useful for exploring relationships
7
+ # Run ruby scratch/grapher_demo.rb for an example
8
+ #
9
+ class GraphCreator
10
+
11
+ def self.create
12
+ system "mkdir -p graphs"
13
+
14
+ data = JSON.parse(RelaxDB.db.get("_all_docs").body)
15
+ all_ids = data["rows"].map { |r| r["id"] }
16
+ all_ids = all_ids.reject { |id| id =~ /_/ }
17
+
18
+ dot = "digraph G { \nrankdir=LR;\nnode [shape=record];\n"
19
+ all_ids.each do |id|
20
+ doc = RelaxDB.load(id)
21
+ atts = "#{doc.class}\\l|"
22
+ doc.properties.each do |prop|
23
+ # we don't care about the revision
24
+ next if prop == :_rev
25
+
26
+ prop_val = doc.instance_variable_get("@#{prop}".to_sym)
27
+ atts << "#{prop}\\l#{prop_val}|" if prop_val
28
+ end
29
+ atts = atts[0, atts.length-1]
30
+
31
+ dot << %Q%#{doc._id} [ label ="#{atts}"];\n%
32
+
33
+ doc.class.belongs_to_rels.each do |relationship, opts|
34
+ id = doc.instance_variable_get("@#{relationship}_id".to_sym)
35
+ dot << %Q%#{id} -> #{doc._id} [ label = "#{relationship}"];\n% if id
36
+ end
37
+
38
+ end
39
+ dot << "}"
40
+
41
+ File.open("graphs/data.dot", "w") { |f| f.write(dot) }
42
+
43
+ system "dot -Tpng -o graphs/all_docs.png graphs/data.dot"
44
+ end
45
+
46
+ end
47
+
48
+ end
data/lib/relaxdb.rb ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'extlib'
3
+ require 'json'
4
+ require 'uuid'
5
+
6
+ require 'cgi'
7
+ require 'net/http'
8
+ require 'logger'
9
+ require 'parsedate'
10
+ require 'pp'
11
+ require 'tempfile'
12
+
13
+ $:.unshift(File.dirname(__FILE__)) unless
14
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
15
+
16
+ require 'relaxdb/validators'
17
+
18
+ begin
19
+ gem 'taf2-curb'
20
+ require 'curb'
21
+ require 'relaxdb/taf2_curb_server'
22
+ rescue LoadError
23
+ require 'relaxdb/net_http_server'
24
+ end
25
+
26
+ require 'relaxdb/all_delegator'
27
+ require 'relaxdb/belongs_to_proxy'
28
+ require 'relaxdb/design_doc'
29
+ require 'relaxdb/document'
30
+ require 'relaxdb/extlib'
31
+ require 'relaxdb/has_many_proxy'
32
+ require 'relaxdb/has_one_proxy'
33
+ require 'relaxdb/migration'
34
+ require 'relaxdb/paginate_params'
35
+ require 'relaxdb/paginator'
36
+ require 'relaxdb/query'
37
+ require 'relaxdb/references_many_proxy'
38
+ require 'relaxdb/relaxdb'
39
+ require 'relaxdb/server'
40
+ require 'relaxdb/uuid_generator'
41
+ require 'relaxdb/view_object'
42
+ require 'relaxdb/view_result'
43
+ require 'relaxdb/view_uploader'
44
+ require 'relaxdb/views'
45
+ require 'more/grapher.rb'
46
+
47
+ require 'relaxdb/migration_version'
48
+
49
+ module RelaxDB
50
+ end
@@ -0,0 +1,44 @@
1
+ module RelaxDB
2
+
3
+ #
4
+ # The AllDelegator allows clients to query CouchDB in a natural way
5
+ # FooDoc.all - returns all docs in CouchDB of type FooDoc
6
+ # FooDoc.all.size - issues a query to a reduce function that returns the total number of docs for that class
7
+ # FooDoc.all.destroy! - TODO - better description
8
+ #
9
+ class AllDelegator < Delegator
10
+
11
+ def initialize(class_name, params)
12
+ super([])
13
+ @class_name = class_name
14
+ @params = params
15
+ end
16
+
17
+ def __getobj__
18
+ unless @objs
19
+ @objs = RelaxDB.rf_view "#{@class_name}_all", @params
20
+ end
21
+ @objs
22
+ end
23
+
24
+ def size
25
+ size = RelaxDB.view "#{@class_name}_all", :reduce => true
26
+ size || 0
27
+ end
28
+
29
+ # TODO: destroy in a bulk_save if feasible
30
+ def destroy!
31
+ __getobj__
32
+ @objs.each do |o|
33
+ # A reload is required for deleting objects with a self referential references_many relationship
34
+ # This makes all.destroy! very slow. Change if needed
35
+ # obj = RelaxDB.load(o._id)
36
+ # obj.destroy!
37
+
38
+ o.destroy!
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -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,57 @@
1
+ module RelaxDB
2
+
3
+ class DesignDocument
4
+
5
+ attr_reader :data
6
+
7
+ def initialize(design_doc_name, data)
8
+ @design_doc_name = design_doc_name
9
+ @data = data
10
+ end
11
+
12
+ def add_map_view(view_name, function)
13
+ add_view(view_name, "map", function)
14
+ end
15
+
16
+ def add_reduce_view(view_name, function)
17
+ add_view(view_name, "reduce", function)
18
+ end
19
+
20
+ def add_validation_func(function)
21
+ @data["validate_doc_update"] = function
22
+ self
23
+ end
24
+
25
+ def add_view(view_name, type, function)
26
+ @data["views"] ||= {}
27
+ @data["views"][view_name] ||= {}
28
+ @data["views"][view_name][type] = function
29
+ self
30
+ end
31
+
32
+ def save
33
+ database = RelaxDB.db
34
+ resp = database.put(@data["_id"], @data.to_json)
35
+ @data["_rev"] = JSON.parse(resp.body)["rev"]
36
+ self
37
+ end
38
+
39
+ def self.get(design_doc_name)
40
+ begin
41
+ database = RelaxDB.db
42
+ resp = database.get("_design/#{design_doc_name}")
43
+ DesignDocument.new(design_doc_name, JSON.parse(resp.body))
44
+ rescue HTTP_404
45
+ DesignDocument.new(design_doc_name, {"_id" => "_design/#{design_doc_name}"} )
46
+ end
47
+ end
48
+
49
+ def destroy!
50
+ # Implicitly prevent the object from being resaved by failing to update its revision
51
+ RelaxDB.db.delete("#{@data["_id"]}?rev=#{@data["_rev"]}")
52
+ self
53
+ end
54
+
55
+ end
56
+
57
+ end
@@ -0,0 +1,600 @@
1
+ module RelaxDB
2
+
3
+ class Document
4
+
5
+ include RelaxDB::Validators
6
+
7
+ # Used to store validation messages
8
+ attr_accessor :errors
9
+
10
+ # A call issued to save_all will save this object and the
11
+ # contents of the save_list. This allows secondary object to
12
+ # be saved at the same time as this object.
13
+ attr_accessor :save_list
14
+
15
+ # Attribute symbols added to this list won't be validated on save
16
+ attr_accessor :validation_skip_list
17
+
18
+ class_inheritable_accessor :properties, :reader => true
19
+ self.properties = []
20
+
21
+ class_inheritable_accessor :derived_prop_writers
22
+ self.derived_prop_writers = {}
23
+
24
+ class_inheritable_accessor :__view_by_list__
25
+ self.__view_by_list__ = []
26
+
27
+ class_inheritable_accessor :belongs_to_rels, :reader => true
28
+ self.belongs_to_rels = {}
29
+
30
+ def self.property(prop, opts={})
31
+ properties << prop
32
+
33
+ define_method(prop) do
34
+ instance_variable_get("@#{prop}".to_sym)
35
+ end
36
+
37
+ define_method("#{prop}=") do |val|
38
+ instance_variable_set("@#{prop}".to_sym, val)
39
+ end
40
+
41
+ if opts[:default]
42
+ define_method("set_default_#{prop}") do
43
+ default = opts[:default]
44
+ default = default.is_a?(Proc) ? default.call : default
45
+ instance_variable_set("@#{prop}".to_sym, default)
46
+ end
47
+ end
48
+
49
+ if opts[:validator]
50
+ create_validator(prop, opts[:validator])
51
+ end
52
+
53
+ if opts[:validation_msg]
54
+ create_validation_msg(prop, opts[:validation_msg])
55
+ end
56
+
57
+ if opts[:derived]
58
+ add_derived_prop(prop, opts[:derived])
59
+ end
60
+ end
61
+
62
+ property :_id
63
+ property :_rev
64
+ property :_conflicts
65
+
66
+ def self.create_validator(att, v)
67
+ method_name = "validate_#{att}"
68
+ if v.is_a? Proc
69
+ v.arity == 1 ?
70
+ define_method(method_name) { |att_val| v.call(att_val) } :
71
+ define_method(method_name) { |att_val| v.call(att_val, self) }
72
+ elsif instance_methods.include? "validator_#{v}"
73
+ define_method(method_name) { |att_val| send("validator_#{v}", att_val, self) }
74
+ else
75
+ define_method(method_name) { |att_val| send(v, att_val) }
76
+ end
77
+ end
78
+
79
+ def self.create_validation_msg(att, validation_msg)
80
+ if validation_msg.is_a?(Proc)
81
+ validation_msg.arity == 1 ?
82
+ define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val) } :
83
+ define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val, self) }
84
+ else
85
+ define_method("#{att}_validation_msg") { validation_msg }
86
+ end
87
+ end
88
+
89
+ # See derived_properties_spec.rb for usage
90
+ def self.add_derived_prop(prop, deriver)
91
+ source, writer = deriver[0], deriver[1]
92
+ derived_prop_writers[source] ||= {}
93
+ derived_prop_writers[source][prop] = writer
94
+ end
95
+
96
+ #
97
+ # The rationale for rescuing the send below is that the lambda for a derived
98
+ # property shouldn't need to concern itself with checking the validity of
99
+ # the underlying property. Nor, IMO, should clients be exposed to the
100
+ # possibility of a writer raising an exception.
101
+ #
102
+ def write_derived_props(source)
103
+ writers = self.class.derived_prop_writers
104
+ writers = writers && writers[source]
105
+ if writers
106
+ writers.each do |prop, writer|
107
+ current_val = send(prop)
108
+ begin
109
+ send("#{prop}=", writer.call(current_val, self))
110
+ rescue => e
111
+ RelaxDB.logger.error "Deriving #{prop} from #{source} raised #{e}"
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ def initialize(hash={})
118
+ unless hash["_id"]
119
+ self._id = UuidGenerator.uuid
120
+ end
121
+
122
+ @errors = Errors.new
123
+ @save_list = []
124
+ @validation_skip_list = []
125
+
126
+ # Set default properties if this object isn't being loaded from CouchDB
127
+ unless hash["_rev"]
128
+ properties.each do |prop|
129
+ if methods.include?("set_default_#{prop}")
130
+ send("set_default_#{prop}")
131
+ end
132
+ end
133
+ end
134
+
135
+ @set_derived_props = hash["_rev"] ? false : true
136
+ set_attributes(hash)
137
+ @set_derived_props = true
138
+ end
139
+
140
+ def set_attributes(data)
141
+ data.each do |key, val|
142
+ # Only set instance variables on creation - object references are resolved on demand
143
+
144
+ # If the variable name ends in _at, _on or _date try to convert it to a Time
145
+ if [/_at$/, /_on$/, /_date$/, /_time$/].inject(nil) { |i, r| i ||= (key =~ r) }
146
+ val = Time.parse(val).utc rescue val
147
+ end
148
+
149
+ # Ignore param keys that don't have a corresponding writer
150
+ # This allows us to comfortably accept a hash containing superflous data
151
+ # such as a params hash in a controller
152
+ send("#{key}=".to_sym, val) if methods.include? "#{key}="
153
+ end
154
+ end
155
+
156
+ def inspect
157
+ s = "#<#{self.class}:#{self.object_id}"
158
+ properties.each do |prop|
159
+ prop_val = instance_variable_get("@#{prop}".to_sym)
160
+ s << ", #{prop}: #{prop_val.inspect}" if prop_val
161
+ end
162
+ self.class.belongs_to_rels.each do |relationship, opts|
163
+ id = instance_variable_get("@#{relationship}_id".to_sym)
164
+ s << ", #{relationship}_id: #{id}" if id
165
+ end
166
+ s << ", errors: #{errors.inspect}" unless errors.empty?
167
+ s << ", save_list: #{save_list.map { |o| o.inspect }.join ", " }" unless save_list.empty?
168
+ s << ">"
169
+ end
170
+
171
+ alias_method :to_s, :inspect
172
+
173
+ def to_json
174
+ data = {}
175
+ self.class.belongs_to_rels.each do |relationship, opts|
176
+ id = instance_variable_get("@#{relationship}_id".to_sym)
177
+ data["#{relationship}_id"] = id if id
178
+ end
179
+ properties.each do |prop|
180
+ prop_val = instance_variable_get("@#{prop}".to_sym)
181
+ data["#{prop}"] = prop_val if prop_val
182
+ end
183
+ data["errors"] = errors unless errors.empty?
184
+ data["relaxdb_class"] = self.class.name
185
+ data.to_json
186
+ end
187
+
188
+ # Not yet sure of final implemention for hooks - may lean more towards DM than AR
189
+ def save
190
+ if pre_save && save_to_couch
191
+ after_save
192
+ self
193
+ else
194
+ false
195
+ end
196
+ end
197
+
198
+ def save_to_couch
199
+ begin
200
+ resp = RelaxDB.db.put(_id, to_json)
201
+ self._rev = JSON.parse(resp.body)["rev"]
202
+ rescue HTTP_409
203
+ conflicted
204
+ return false
205
+ end
206
+ end
207
+
208
+ def conflicted
209
+ @update_conflict = true
210
+ on_update_conflict
211
+ end
212
+
213
+ def on_update_conflict
214
+ # override with any behaviour you want to happen when
215
+ # CouchDB returns DocumentConflict on an attempt to save
216
+ end
217
+
218
+ def update_conflict?
219
+ @update_conflict
220
+ end
221
+
222
+ def pre_save
223
+ set_timestamps
224
+ return false unless validates?
225
+ return false unless before_save
226
+ true
227
+ end
228
+
229
+ def post_save
230
+ after_save
231
+ end
232
+
233
+ # save_all and save_all! are untested
234
+ def save_all
235
+ RelaxDB.bulk_save self, *save_list
236
+ end
237
+
238
+ def save_all!
239
+ RelaxDB.bulk_save! self, *save_list
240
+ end
241
+
242
+ def save!
243
+ if save
244
+ self
245
+ elsif update_conflict?
246
+ raise UpdateConflict, self
247
+ else
248
+ raise ValidationFailure, self.errors.to_json
249
+ end
250
+ end
251
+
252
+ def validates?
253
+ props = properties - validation_skip_list
254
+ prop_vals = props.map { |prop| instance_variable_get("@#{prop}") }
255
+
256
+ rels = self.class.belongs_to_rels.keys - validation_skip_list
257
+ rel_vals = rels.map { |rel| instance_variable_get("@#{rel}_id") }
258
+
259
+ att_names = props + rels
260
+ att_vals = prop_vals + rel_vals
261
+
262
+ total_success = true
263
+ att_names.each_index do |i|
264
+ att_name, att_val = att_names[i], att_vals[i]
265
+ if methods.include? "validate_#{att_name}"
266
+ total_success &= validate_att(att_name, att_val)
267
+ end
268
+ end
269
+
270
+ total_success
271
+ end
272
+ alias_method :validate, :validates?
273
+
274
+ def validate_att(att_name, att_val)
275
+ begin
276
+ success = send("validate_#{att_name}", att_val)
277
+ rescue => e
278
+ RelaxDB.logger.warn "Validating #{att_name} with #{att_val} raised #{e}"
279
+ succes = false
280
+ end
281
+
282
+ unless success
283
+ if methods.include? "#{att_name}_validation_msg"
284
+ begin
285
+ @errors[att_name] = send("#{att_name}_validation_msg", att_val)
286
+ rescue => e
287
+ RelaxDB.logger.warn "Validation_msg for #{att_name} with #{att_val} raised #{e}"
288
+ @errors[att_name] = "validation_msg_exception:invalid:#{att_val}"
289
+ end
290
+ elsif @errors[att_name].nil?
291
+ # Only set a validation message if a validator hasn't already set one
292
+ @errors[att_name] = "invalid:#{att_val}"
293
+ end
294
+ end
295
+ success
296
+ end
297
+
298
+ def new_document?
299
+ @_rev.nil?
300
+ end
301
+ alias_method :new_record?, :new_document?
302
+ alias_method :unsaved?, :new_document?
303
+
304
+ def to_param
305
+ self._id
306
+ end
307
+ alias_method :id, :to_param
308
+
309
+ def set_timestamps
310
+ now = Time.now
311
+ if new_document? && respond_to?(:created_at)
312
+ # Don't override it if it's already been set
313
+ @created_at = now if @created_at.nil?
314
+ end
315
+
316
+ @updated_at = now if respond_to?(:updated_at)
317
+ end
318
+
319
+ def create_or_get_proxy(klass, relationship, opts=nil)
320
+ proxy_sym = "@proxy_#{relationship}".to_sym
321
+ proxy = instance_variable_get(proxy_sym)
322
+ unless proxy
323
+ proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
324
+ instance_variable_set(proxy_sym, proxy)
325
+ end
326
+ proxy
327
+ end
328
+
329
+ # Returns true if CouchDB considers other to be the same as self
330
+ def ==(other)
331
+ other && _id == other._id
332
+ end
333
+
334
+ # If you're using this method, read the specs and make sure you understand
335
+ # how it can be used and how it shouldn't be used
336
+ def self.references_many(relationship, opts={})
337
+ # Treat the representation as a standard property
338
+ properties << relationship
339
+
340
+ # Keep track of the relationship so peers can be disassociated on destroy
341
+ @references_many_rels ||= []
342
+ @references_many_rels << relationship
343
+
344
+ id_arr_sym = "@#{relationship}".to_sym
345
+
346
+ if RelaxDB.create_views?
347
+ target_class = opts[:class]
348
+ relationship_as_viewed_by_target = opts[:known_as].to_s
349
+ ViewCreator.references_many(self.name, relationship, target_class, relationship_as_viewed_by_target).save
350
+ end
351
+
352
+ define_method(relationship) do
353
+ instance_variable_set(id_arr_sym, []) unless instance_variable_defined? id_arr_sym
354
+ create_or_get_proxy(ReferencesManyProxy, relationship, opts)
355
+ end
356
+
357
+ define_method("#{relationship}_ids") do
358
+ instance_variable_set(id_arr_sym, []) unless instance_variable_defined? id_arr_sym
359
+ instance_variable_get(id_arr_sym)
360
+ end
361
+
362
+ define_method("#{relationship}=") do |val|
363
+ # Don't invoke this method unless you know what you're doing
364
+ instance_variable_set(id_arr_sym, val)
365
+ end
366
+ end
367
+
368
+ def self.references_many_rels
369
+ @references_many_rels ||= []
370
+ end
371
+
372
+ def self.has_many(relationship, opts={})
373
+ @has_many_rels ||= []
374
+ @has_many_rels << relationship
375
+
376
+ if RelaxDB.create_views?
377
+ target_class = opts[:class] || relationship.to_s.singularize.camel_case
378
+ relationship_as_viewed_by_target = (opts[:known_as] || self.name.snake_case).to_s
379
+ ViewCreator.has_n(self.name, relationship, target_class, relationship_as_viewed_by_target).save
380
+ end
381
+
382
+ define_method(relationship) do
383
+ create_or_get_proxy(HasManyProxy, relationship, opts)
384
+ end
385
+
386
+ define_method("#{relationship}=") do |children|
387
+ create_or_get_proxy(HasManyProxy, relationship, opts).children = children
388
+ write_derived_props(relationship) if @set_derived_props
389
+ children
390
+ end
391
+ end
392
+
393
+ def self.has_many_rels
394
+ # Don't force clients to check its instantiated
395
+ @has_many_rels ||= []
396
+ end
397
+
398
+ def self.has_one(relationship)
399
+ @has_one_rels ||= []
400
+ @has_one_rels << relationship
401
+
402
+ if RelaxDB.create_views?
403
+ target_class = relationship.to_s.camel_case
404
+ relationship_as_viewed_by_target = self.name.snake_case
405
+ ViewCreator.has_n(self.name, relationship, target_class, relationship_as_viewed_by_target).save
406
+ end
407
+
408
+ define_method(relationship) do
409
+ create_or_get_proxy(HasOneProxy, relationship).target
410
+ end
411
+
412
+ define_method("#{relationship}=") do |new_target|
413
+ create_or_get_proxy(HasOneProxy, relationship).target = new_target
414
+ write_derived_props(relationship) if @set_derived_props
415
+ new_target
416
+ end
417
+ end
418
+
419
+ def self.has_one_rels
420
+ @has_one_rels ||= []
421
+ end
422
+
423
+ def self.belongs_to(relationship, opts={})
424
+ belongs_to_rels[relationship] = opts
425
+
426
+ define_method(relationship) do
427
+ create_or_get_proxy(BelongsToProxy, relationship).target
428
+ end
429
+
430
+ define_method("#{relationship}=") do |new_target|
431
+ create_or_get_proxy(BelongsToProxy, relationship).target = new_target
432
+ write_derived_props(relationship) if @set_derived_props
433
+ end
434
+
435
+ # Allows all writers to be invoked from the hash passed to initialize
436
+ define_method("#{relationship}_id=") do |id|
437
+ instance_variable_set("@#{relationship}_id".to_sym, id)
438
+ write_derived_props(relationship) if @set_derived_props
439
+ id
440
+ end
441
+
442
+ define_method("#{relationship}_id") do
443
+ instance_variable_get("@#{relationship}_id")
444
+ end
445
+
446
+ create_validator(relationship, opts[:validator]) if opts[:validator]
447
+
448
+ # Untested below
449
+ create_validation_msg(relationship, opts[:validation_msg]) if opts[:validation_msg]
450
+ end
451
+
452
+ class << self
453
+ alias_method :references, :belongs_to
454
+ end
455
+
456
+ self.belongs_to_rels = {}
457
+
458
+ def self.all_relationships
459
+ belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
460
+ end
461
+
462
+ def self.all params = {}
463
+ AllDelegator.new self.name, params
464
+ end
465
+
466
+ # destroy! nullifies all relationships with peers and children before deleting
467
+ # itself in CouchDB
468
+ # The nullification and deletion are not performed in a transaction
469
+ #
470
+ # TODO: Current implemention may be inappropriate - causing CouchDB to try to JSON
471
+ # encode undefined. Ensure nil is serialized? See has_many_spec#should nullify its child relationships
472
+ def destroy!
473
+ self.class.references_many_rels.each do |rel|
474
+ send(rel).clear
475
+ end
476
+
477
+ self.class.has_many_rels.each do |rel|
478
+ send(rel).clear
479
+ end
480
+
481
+ self.class.has_one_rels.each do |rel|
482
+ send("#{rel}=".to_sym, nil)
483
+ end
484
+
485
+ # Implicitly prevent the object from being resaved by failing to update its revision
486
+ RelaxDB.db.delete("#{_id}?rev=#{_rev}")
487
+ self
488
+ end
489
+
490
+ #
491
+ # Callbacks - define these in a module and mix'em'in ?
492
+ #
493
+ def self.before_save(callback)
494
+ before_save_callbacks << callback
495
+ end
496
+
497
+ def self.before_save_callbacks
498
+ @before_save ||= []
499
+ end
500
+
501
+ def before_save
502
+ self.class.before_save_callbacks.each do |callback|
503
+ resp = callback.is_a?(Proc) ? callback.call(self) : send(callback)
504
+ if resp == false
505
+ errors[:before_save] = :failed
506
+ return false
507
+ end
508
+ end
509
+ end
510
+
511
+ def self.after_save(callback)
512
+ after_save_callbacks << callback
513
+ end
514
+
515
+ def self.after_save_callbacks
516
+ @after_save_callbacks ||= []
517
+ end
518
+
519
+ def after_save
520
+ self.class.after_save_callbacks.each do |callback|
521
+ callback.is_a?(Proc) ? callback.call(self) : send(callback)
522
+ end
523
+ end
524
+
525
+ #
526
+ # Creates the corresponding view and stores it in CouchDB
527
+ # Adds by_ and paginate_by_ methods to the class
528
+ #
529
+ def self.view_by *atts
530
+ opts = atts.last.is_a?(Hash) ? atts.pop : {}
531
+ __view_by_list__ << atts
532
+
533
+ if RelaxDB.create_views?
534
+ ViewCreator.by_att_list([self.name], *atts).save
535
+ end
536
+
537
+ by_name = "by_#{atts.join "_and_"}"
538
+ meta_class.instance_eval do
539
+ define_method by_name do |*params|
540
+ view_name = "#{self.name}_#{by_name}"
541
+ if params.empty?
542
+ res = RelaxDB.rf_view view_name, opts
543
+ elsif params[0].is_a? Hash
544
+ res = RelaxDB.rf_view view_name, opts.merge(params[0])
545
+ else
546
+ res = RelaxDB.rf_view(view_name, :key => params[0]).first
547
+ end
548
+ end
549
+ end
550
+
551
+ paginate_by_name = "paginate_by_#{atts.join "_and_"}"
552
+ meta_class.instance_eval do
553
+ define_method paginate_by_name do |params|
554
+ view_name = "#{self.name}_#{by_name}"
555
+ params[:attributes] = atts
556
+ params = opts.merge params
557
+ RelaxDB.paginate_view view_name, params
558
+ end
559
+ end
560
+ end
561
+
562
+ # Create a view allowing all instances of a particular class to be retreived
563
+ def self.create_all_by_class_view
564
+ if RelaxDB.create_views?
565
+ view = ViewCreator.all
566
+ view.save unless view.exists?
567
+ end
568
+ end
569
+
570
+ def self.inherited subclass
571
+ chain = subclass.up_chain
572
+ while k = chain.pop
573
+ k.create_views chain
574
+ end
575
+ end
576
+
577
+ def self.up_chain
578
+ k = self
579
+ kls = [k]
580
+ kls << k while ((k = k.superclass) != RelaxDB::Document)
581
+ kls
582
+ end
583
+
584
+ def self.create_views chain
585
+ # Capture the inheritance hierarchy of this class
586
+ @hierarchy ||= [self]
587
+ @hierarchy += chain
588
+ @hierarchy.uniq!
589
+
590
+ if RelaxDB.create_views?
591
+ ViewCreator.all(@hierarchy).save
592
+ __view_by_list__.each do |atts|
593
+ ViewCreator.by_att_list(@hierarchy, *atts).save
594
+ end
595
+ end
596
+ end
597
+
598
+ end
599
+
600
+ end