relaxdb 0.3.5

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.
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