paulcarey-relaxdb 0.2.6 → 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -1,9 +1,23 @@
1
1
  h3. What's New?
2
2
 
3
- * Denormalisation via derived properties. Examples in spec/derived_properties_spec.rb.
3
+ * Potentially breaking change. Skipping validations is now done by adding attribute symbols to an object's list rather than passing them to @save@. For example @my_obj.validation_skip_list << :foo@. This offers per object granularity over validations when working with bulk_save.
4
+
5
+ * Potentially breaking change. @load@ now returns an array if passed an array of size one. Previously it would have returned a single object.
6
+
7
+ * Update conflict hook and property
8
+
9
+ * Semantic consistency for bulk_save and bulk_save! wrt to save and save!
10
+
11
+ * Multiple exception handling improvements
12
+
13
+ * @save_all@ that issues a bulk_save for an object and its has_one and has_many children
14
+
15
+ * assignment of @has_many@ relationships
4
16
 
5
17
  * Validations may be skipped by passing the attribute symbol(s) to @save@ or @save!@.
6
18
 
19
+ * Denormalisation via derived properties. Examples in spec/derived_properties_spec.rb.
20
+
7
21
  * Semantic changes for @ has_many#<< @. The parent object is now assigned to the child object *prior* to validation. This potentially breaking change was made to allow child objects to derive properties from a parent object.
8
22
 
9
23
  * Semantic consistency for load, load!, save and save!. The bang versions raise an exception when their more relaxed siblings would simply return nil.
@@ -18,7 +32,7 @@ h3. What's New?
18
32
  ** For example, @ Numbers.all.sorted_by(:val) { |q| q.keys([1,2,3,5]) } @
19
33
  * Works with CouchDB 0.9 trunk as of 2009/01/02. Note that pagination won't work correctly on trunk until issue "COUCHDB-135":http://issues.apache.org/jira/browse/COUCHDB-135 is fixed.
20
34
 
21
- *Note*: 0.2.1 requires CouchDB 0.9 trunk. 0.2.0 works with CouchDB 0.8 onwards.
35
+ *Note*: Current versions require CouchDB 0.9 trunk. If you're working with CouchDB 0.8 or 0.8.1, please build from commit @ a8a2d496462 @.
22
36
 
23
37
  h2. Overview
24
38
 
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require 'spec/rake/spectask'
4
4
 
5
5
  PLUGIN = "relaxdb"
6
6
  NAME = "relaxdb"
7
- GEM_VERSION = "0.2.6"
7
+ GEM_VERSION = "0.2.7"
8
8
  AUTHOR = "Paul Carey"
9
9
  EMAIL = "paul.p.carey@gmail.com"
10
10
  HOMEPAGE = "http://github.com/paulcarey/relaxdb/"
@@ -7,6 +7,9 @@ module RelaxDB
7
7
  # Used to store validation messages
8
8
  attr_accessor :errors
9
9
 
10
+ # Attribute symbols added to this list won't be validated on save
11
+ attr_accessor :validation_skip_list
12
+
10
13
  # Define properties and property methods
11
14
 
12
15
  def self.property(prop, opts={})
@@ -62,13 +65,13 @@ module RelaxDB
62
65
  end
63
66
  end
64
67
 
65
- def self.create_validation_msg(prop, validation_msg)
68
+ def self.create_validation_msg(att, validation_msg)
66
69
  if validation_msg.is_a?(Proc)
67
70
  validation_msg.arity == 1 ?
68
- define_method("#{prop}_validation_msg") { |prop_val| validation_msg.call(prop_val) } :
69
- define_method("#{prop}_validation_msg") { |prop_val| validation_msg.call(prop_val, self) }
71
+ define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val) } :
72
+ define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val, self) }
70
73
  else
71
- define_method("#{prop}_validation_msg") { validation_msg }
74
+ define_method("#{att}_validation_msg") { validation_msg }
72
75
  end
73
76
  end
74
77
 
@@ -84,12 +87,22 @@ module RelaxDB
84
87
  @derived_prop_writers ||= {}
85
88
  end
86
89
 
90
+ #
91
+ # The rationale for rescuing the send below is that the lambda for a derived
92
+ # property shouldn't need to concern itself with checking the validity of
93
+ # the underlying property. Nor, IMO, should clients be exposed to the
94
+ # possibility of a writer raising an exception.
95
+ #
87
96
  def write_derived_props(source)
88
97
  writers = self.class.derived_prop_writers[source]
89
98
  if writers
90
99
  writers.each do |prop, writer|
91
100
  current_val = send(prop)
92
- send("#{prop}=", writer.call(current_val, self))
101
+ begin
102
+ send("#{prop}=", writer.call(current_val, self))
103
+ rescue => e
104
+ RelaxDB.logger.warn "Deriving #{prop} from #{source} raised #{e}"
105
+ end
93
106
  end
94
107
  end
95
108
  end
@@ -110,6 +123,7 @@ module RelaxDB
110
123
  self._id = UuidGenerator.uuid
111
124
 
112
125
  @errors = Errors.new
126
+ @validation_skip_list = []
113
127
 
114
128
  # Set default properties if this object has not known CouchDB
115
129
  unless hash["_rev"]
@@ -201,31 +215,76 @@ module RelaxDB
201
215
  data.to_json
202
216
  end
203
217
 
204
- # Order changed as of 30/10/2008 to be consistent with ActiveRecord
205
218
  # Not yet sure of final implemention for hooks - may lean more towards DM than AR
206
- def save(*validation_skip_list)
207
- return false unless validates?(*validation_skip_list)
208
- return false unless before_save
209
-
210
- set_created_at if new_document?
211
-
212
- resp = RelaxDB.db.put(_id, to_json)
213
- self._rev = JSON.parse(resp.body)["rev"]
214
-
219
+ def save
220
+ if pre_save && save_to_couch
221
+ after_save
222
+ self
223
+ else
224
+ false
225
+ end
226
+ end
227
+
228
+ def save_to_couch
229
+ begin
230
+ resp = RelaxDB.db.put(_id, to_json)
231
+ self._rev = JSON.parse(resp.body)["rev"]
232
+ rescue HTTP_412
233
+ on_update_conflict
234
+ @update_conflict = true
235
+ return false
236
+ end
237
+ end
238
+
239
+ def on_update_conflict
240
+ # override with any behaviour you want to happen when
241
+ # CouchDB returns DocumentConflict on an attempt to save
242
+ end
243
+
244
+ def pre_save
245
+ return false unless validates?
246
+ return false unless before_save
247
+ set_created_at if new_document?
248
+ true
249
+ end
250
+
251
+ def post_save
215
252
  after_save
253
+ end
254
+
255
+ def save!
256
+ if save
257
+ self
258
+ elsif update_conflict?
259
+ raise UpdateConflict, self
260
+ else
261
+ raise ValidationFailure, self.errors.to_json
262
+ end
263
+ end
264
+
265
+ def save_all
266
+ RelaxDB.bulk_save(self, *all_children)
267
+ end
216
268
 
217
- self
218
- end
269
+ def save_all!
270
+ RelaxDB.bulk_save!(self, *all_children)
271
+ end
272
+
273
+ def all_children
274
+ ho = self.class.has_one_rels.map { |r| send(r) }
275
+ hm = self.class.has_many_rels.inject([]) { |m,r| m += send(r).children }
276
+ ho + hm
277
+ end
219
278
 
220
- def save!(*validation_skip_list)
221
- save(*validation_skip_list) || raise(DocumentNotSaved.new(self.errors.to_json))
279
+ def update_conflict?
280
+ @update_conflict
222
281
  end
223
282
 
224
- def validates?(*skip_list)
225
- props = properties - skip_list
283
+ def validates?
284
+ props = properties - validation_skip_list
226
285
  prop_vals = props.map { |prop| instance_variable_get("@#{prop}") }
227
286
 
228
- rels = self.class.belongs_to_rels.keys - skip_list
287
+ rels = self.class.belongs_to_rels.keys - validation_skip_list
229
288
  rel_vals = rels.map { |rel| instance_variable_get("@#{rel}_id") }
230
289
 
231
290
  att_names = props + rels
@@ -246,7 +305,7 @@ module RelaxDB
246
305
  begin
247
306
  success = send("validate_#{att_name}", att_val)
248
307
  rescue => e
249
- RelaxDB.logger.warn("Validating #{att_name} with #{att_val} raised #{e}")
308
+ RelaxDB.logger.warn "Validating #{att_name} with #{att_val} raised #{e}"
250
309
  succes = false
251
310
  end
252
311
 
@@ -255,8 +314,8 @@ module RelaxDB
255
314
  begin
256
315
  @errors[att_name] = send("#{att_name}_validation_msg", att_val)
257
316
  rescue => e
258
- RelaxDB.logger.warn("Validation_msg for #{att_name} with #{att_val} raised #{e}")
259
- @errors[att_name] = "validation_msg_exception:invalid:#{att_name_val}"
317
+ RelaxDB.logger.warn "Validation_msg for #{att_name} with #{att_val} raised #{e}"
318
+ @errors[att_name] = "validation_msg_exception:invalid:#{att_val}"
260
319
  end
261
320
  else
262
321
  @errors[att_name] = "invalid:#{att_val}"
@@ -322,7 +381,6 @@ module RelaxDB
322
381
  end
323
382
 
324
383
  def self.references_many_rels
325
- # Don't force clients to check its instantiated
326
384
  @references_many_rels ||= []
327
385
  end
328
386
 
@@ -334,8 +392,10 @@ module RelaxDB
334
392
  create_or_get_proxy(HasManyProxy, relationship, opts)
335
393
  end
336
394
 
337
- define_method("#{relationship}=") do
338
- raise "You may not currently assign to a has_many relationship - may be implemented"
395
+ define_method("#{relationship}=") do |children|
396
+ create_or_get_proxy(HasManyProxy, relationship, opts).children = children
397
+ write_derived_props(relationship)
398
+ children
339
399
  end
340
400
  end
341
401
 
@@ -354,6 +414,8 @@ module RelaxDB
354
414
 
355
415
  define_method("#{relationship}=") do |new_target|
356
416
  create_or_get_proxy(HasOneProxy, relationship).target = new_target
417
+ write_derived_props(relationship)
418
+ new_target
357
419
  end
358
420
  end
359
421
 
@@ -378,6 +440,7 @@ module RelaxDB
378
440
  define_method("#{relationship}_id=") do |id|
379
441
  instance_variable_set("@#{relationship}_id".to_sym, id)
380
442
  write_derived_props(relationship)
443
+ id
381
444
  end
382
445
 
383
446
  # Allows belongs_to relationships to be used by the paginator
@@ -386,10 +449,16 @@ module RelaxDB
386
449
  end
387
450
 
388
451
  create_validator(relationship, opts[:validator]) if opts[:validator]
452
+
453
+ # Untested below
454
+ create_validation_msg(relationship, opts[:validation_msg]) if opts[:validation_msg]
455
+ end
456
+
457
+ class << self
458
+ alias_method :references, :belongs_to
389
459
  end
390
460
 
391
461
  def self.belongs_to_rels
392
- # Don't force clients to check that it's instantiated
393
462
  @belongs_to_rels ||= {}
394
463
  end
395
464
 
@@ -404,6 +473,9 @@ module RelaxDB
404
473
  # destroy! nullifies all relationships with peers and children before deleting
405
474
  # itself in CouchDB
406
475
  # The nullification and deletion are not performed in a transaction
476
+ #
477
+ # TODO: Current implemention may be inappropriate - causing CouchDB to try to JSON
478
+ # encode undefined. Ensure nil is serialized? See has_many_spec#should nullify its child relationships
407
479
  def destroy!
408
480
  self.class.references_many_rels.each do |rel|
409
481
  send(rel).clear
@@ -3,6 +3,8 @@ module RelaxDB
3
3
  class HasManyProxy
4
4
 
5
5
  include Enumerable
6
+
7
+ attr_reader :children
6
8
 
7
9
  def initialize(client, relationship, opts)
8
10
  @client = client
@@ -57,6 +59,14 @@ module RelaxDB
57
59
  def [](*args)
58
60
  @children[*args]
59
61
  end
62
+
63
+ def first
64
+ @children[0]
65
+ end
66
+
67
+ def last
68
+ @children[size-1]
69
+ end
60
70
 
61
71
  def each(&blk)
62
72
  @children.each(&blk)
@@ -73,6 +83,13 @@ module RelaxDB
73
83
  map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
74
84
  @children = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
75
85
  end
86
+
87
+ def children=(children)
88
+ children.each do |obj|
89
+ obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
90
+ end
91
+ @children = children
92
+ end
76
93
 
77
94
  def inspect
78
95
  @children.inspect
@@ -2,6 +2,8 @@ module RelaxDB
2
2
 
3
3
  class NotFound < StandardError; end
4
4
  class DocumentNotSaved < StandardError; end
5
+ class UpdateConflict < DocumentNotSaved; end
6
+ class ValidationFailure < DocumentNotSaved; end
5
7
 
6
8
  @@db = nil
7
9
 
@@ -21,11 +23,15 @@ module RelaxDB
21
23
 
22
24
  # Creates the named database if it doesn't already exist
23
25
  def use_db(name)
24
- db.use_db(name)
26
+ db.use_db name
27
+ end
28
+
29
+ def db_exists?(name)
30
+ db.db_exists? name
25
31
  end
26
32
 
27
33
  def delete_db(name)
28
- db.delete_db(name)
34
+ db.delete_db name
29
35
  end
30
36
 
31
37
  def list_dbs
@@ -36,41 +42,58 @@ module RelaxDB
36
42
  db.replicate_db source, target
37
43
  end
38
44
 
39
- def bulk_save(*objs)
45
+ def bulk_save!(*objs)
46
+ pre_save_success = objs.inject(true) { |s, o| s &= o.pre_save }
47
+ raise ValidationFailure, objs unless pre_save_success
48
+
40
49
  docs = {}
41
50
  objs.each { |o| docs[o._id] = o }
51
+
52
+ begin
53
+ resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
54
+ data = JSON.parse(resp.body)
42
55
 
43
- resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
44
- data = JSON.parse(resp.body)
45
-
46
- data["new_revs"].each do |new_rev|
47
- docs[ new_rev["id"] ]._rev = new_rev["rev"]
56
+ data["new_revs"].each do |new_rev|
57
+ obj = docs[ new_rev["id"] ]
58
+ obj._rev = new_rev["rev"]
59
+ obj.post_save
60
+ end
61
+ rescue HTTP_412
62
+ raise UpdateConflict, objs
48
63
  end
49
64
 
50
- data["ok"]
65
+ objs
66
+ end
67
+
68
+ def bulk_save(*objs)
69
+ begin
70
+ bulk_save!(*objs)
71
+ rescue ValidationFailure, UpdateConflict
72
+ false
73
+ end
51
74
  end
52
75
 
53
- def load(*ids)
54
- if ids.size == 1
76
+ def load(ids)
77
+ if ids.is_a? Array
78
+ resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
79
+ data = JSON.parse(resp.body)
80
+ data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
81
+ else
55
82
  begin
56
- resp = db.get(ids[0])
83
+ resp = db.get(ids)
57
84
  data = JSON.parse(resp.body)
58
85
  create_object(data)
59
86
  rescue HTTP_404
60
87
  nil
61
88
  end
62
- else
63
- resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
64
- data = JSON.parse(resp.body)
65
- data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
66
89
  end
67
90
  end
68
91
 
69
- def load!(*ids)
70
- res = load(*ids)
92
+ def load!(ids)
93
+ res = load(ids)
71
94
 
72
- raise NotFound if res == nil
73
- raise NotFound if res.respond_to?(:include?) && res.include?(nil)
95
+ raise NotFound, ids if res == nil
96
+ raise NotFound, ids if res.respond_to?(:include?) && res.include?(nil)
74
97
 
75
98
  res
76
99
  end
@@ -2,6 +2,7 @@ module RelaxDB
2
2
 
3
3
  class HTTP_404 < StandardError; end
4
4
  class HTTP_409 < StandardError; end
5
+ class HTTP_412 < StandardError; end
5
6
 
6
7
  class Server
7
8
 
@@ -62,13 +63,15 @@ module RelaxDB
62
63
  end
63
64
 
64
65
  class CouchDB
66
+
67
+ attr_reader :logger
65
68
 
66
69
  # Used for test instrumentation only i.e. to assert that
67
- # an expected number of GET requests have been issued
68
- attr_accessor :get_count
70
+ # an expected number of requests have been issued
71
+ attr_accessor :get_count, :put_count
69
72
 
70
73
  def initialize(config)
71
- @get_count = 0
74
+ @get_count, @put_count = 0, 0
72
75
  @server = RelaxDB::Server.new(config[:host], config[:port])
73
76
  @logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('couchdb.log'))
74
77
  end
@@ -78,6 +81,10 @@ module RelaxDB
78
81
  @db = name
79
82
  end
80
83
 
84
+ def db_exists?(name)
85
+ @server.get("/#{name}") rescue false
86
+ end
87
+
81
88
  def delete_db(name)
82
89
  @logger.info("Deleting database #{name}")
83
90
  @server.delete("/#{name}")
@@ -99,7 +106,7 @@ module RelaxDB
99
106
  @server.delete("/#{@db}/#{path}")
100
107
  end
101
108
 
102
- # *ignored allows methods to invoke get or post indifferently
109
+ # *ignored allows methods to invoke get or post indifferently via send
103
110
  def get(path=nil, *ignored)
104
111
  @get_count += 1
105
112
  @logger.info("GET /#{@db}/#{unesc(path)}")
@@ -112,6 +119,7 @@ module RelaxDB
112
119
  end
113
120
 
114
121
  def put(path=nil, json=nil)
122
+ @put_count += 1
115
123
  @logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
116
124
  @server.put("/#{@db}/#{path}", json)
117
125
  end
@@ -129,10 +137,10 @@ module RelaxDB
129
137
  @db
130
138
  end
131
139
 
132
- def logger
133
- @logger
140
+ def name=(name)
141
+ @db = name
134
142
  end
135
-
143
+
136
144
  private
137
145
 
138
146
  def create_db_if_non_existant(name)
@@ -1,9 +1,6 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
  require File.dirname(__FILE__) + '/spec_models.rb'
3
3
 
4
- # These tests would ideally instrument server.rb, asserting that no
5
- # HTTP requests are made when retrieving the derived values
6
-
7
4
  class DpInvite < RelaxDB::Document
8
5
  property :event_name, :derived => [:event, lambda { |en, i| i.event.name }]
9
6
  belongs_to :event
@@ -46,12 +43,11 @@ describe RelaxDB::Document, "derived properties" do
46
43
  i.event_name.should == "shindig"
47
44
  end
48
45
 
49
- it "will fail when the source_id is updated for a unsaved event" do
50
- # Almost certainly not desired - merely codifying current behaviour
51
- e = DpEvent.new(:name => "shindig")
52
- lambda { DpInvite.new(:event_id => e._id) }.should raise_error
46
+ it "will not raise an exception when the source is nil" do
47
+ # See the rationale in Document.write_derived_props
48
+ DpInvite.new(:event => nil).save!
53
49
  end
54
-
50
+
55
51
  it "should only be updated for registered properties" do
56
52
  invite = Class.new(RelaxDB::Document) do
57
53
  property :event_name, :derived => [:foo, lambda { |en, i| i.event.name }]
@@ -36,6 +36,10 @@ describe RelaxDB::Document do
36
36
  Post.new(:foo => "").save
37
37
  end
38
38
 
39
+ it "should create a document with a non conflicing state" do
40
+ Atom.new.should_not be_update_conflict
41
+ end
42
+
39
43
  end
40
44
 
41
45
  describe "#initialize" do
@@ -92,6 +96,14 @@ describe RelaxDB::Document do
92
96
  p = Post.new(:created_at => back_then).save
93
97
  p.created_at.should be_close(back_then, 1)
94
98
  end
99
+
100
+ it "should set document conflict state on conflicting save" do
101
+ a1 = Atom.new
102
+ a2 = a1.dup
103
+ a1.save!
104
+ a2.save
105
+ a2.should be_update_conflict
106
+ end
95
107
 
96
108
  end
97
109
 
@@ -102,15 +114,57 @@ describe RelaxDB::Document do
102
114
  RelaxDB.load(a._id).should == a
103
115
  end
104
116
 
105
- it "should throw an exception when an object is not saved" do
117
+ it "should raise ValidationFailure on validation failure" do
106
118
  r = Class.new(RelaxDB::Document) do
107
119
  property :thumbs_up, :validator => lambda { false }
108
120
  end
109
121
  lambda do
110
122
  r.new.save!
111
- end.should raise_error(RelaxDB::DocumentNotSaved)
123
+ end.should raise_error(RelaxDB::ValidationFailure)
112
124
  end
113
125
 
126
+ it "should raise UpdateConflict on an update conflict" do
127
+ a1 = Atom.new
128
+ a2 = a1.dup
129
+ a1.save!
130
+ lambda { a2.save! }.should raise_error(RelaxDB::UpdateConflict)
131
+ end
132
+
133
+ end
134
+
135
+ describe "#save_all" do
136
+
137
+ before(:each) do
138
+ # Create the underlying views
139
+ User.new(:items => [], :invites_received => [], :invites_sent => [])
140
+ end
141
+
142
+ it "should issue only a single PUT request" do
143
+ RelaxDB.db.put_count = 0
144
+ RelaxDB.db.get_count = 0
145
+
146
+ i1, i2 = Item.new(:name => "i1"), Item.new(:name => "i2")
147
+ u = User.new(:items => [i1, i2])
148
+ u.save_all!
149
+
150
+ RelaxDB.db.put_count.should == 0
151
+ RelaxDB.db.get_count.should == 3
152
+ end
153
+
154
+ end
155
+
156
+ describe "#all_children" do
157
+
158
+ it "should return an array containing all children" do
159
+ r = Rating.new
160
+ p = Photo.new(:rating => r)
161
+ t = Tag.new
162
+ t1, t2 = Tagging.new(:photo => p, :tag => t), Tagging.new(:photo => p, :tag => t)
163
+ p.taggings = [t1, t2]
164
+ p.all_children.size.should == 3
165
+ [r, t1, t2].each { |c| p.all_children.should include(c) }
166
+ end
167
+
114
168
  end
115
169
 
116
170
  describe "user defined property reader" do
@@ -171,7 +225,7 @@ describe RelaxDB::Document do
171
225
 
172
226
  it "should prevent the object from being resaved" do
173
227
  p = Atom.new.save.destroy!
174
- lambda { p.save }.should raise_error
228
+ lambda { p.save! }.should raise_error
175
229
  end
176
230
 
177
231
  it "will result in undefined behaviour when invoked on unsaved objects" do
@@ -434,7 +488,9 @@ describe RelaxDB::Document do
434
488
  r = Class.new(RelaxDB::Document) do
435
489
  property :thumbs_up, :validator => lambda { raise }
436
490
  end
437
- r.new.save!(:thumbs_up)
491
+ x = r.new
492
+ x.validation_skip_list << :thumbs_up
493
+ x.save!
438
494
  end
439
495
 
440
496
  end
@@ -16,7 +16,7 @@ describe RelaxDB::HasManyProxy do
16
16
 
17
17
  it "should be considered enumerable" do
18
18
  u = User.new.save
19
- u.items.should be_a_kind_of( Enumerable)
19
+ u.items.should be_a_kind_of(Enumerable)
20
20
  end
21
21
 
22
22
  it "should actually be enumerable" do
@@ -41,7 +41,7 @@ describe RelaxDB::HasManyProxy do
41
41
  m = RelaxDB.load m._id
42
42
  m.multi_word_children[0].should == c
43
43
  end
44
-
44
+
45
45
  describe "#<<" do
46
46
 
47
47
  it "should link the added item to the parent" do
@@ -74,10 +74,39 @@ describe RelaxDB::HasManyProxy do
74
74
  end
75
75
 
76
76
  describe "#=" do
77
-
78
- it "should fail" do
79
- # This may be implemented in future
80
- lambda { User.new.items = [] }.should raise_error
77
+
78
+ before(:each) do
79
+ # Create the underlying views
80
+ User.new(:items => [], :invites_received => [], :invites_sent => [])
81
+ end
82
+
83
+ it "should not attempt to save the child objects when the relationship is established" do
84
+ RelaxDB.db.put_count = 0
85
+ i1, i2 = Item.new(:name => "i1"), Item.new(:name => "i2")
86
+ User.new(:items => [i1, i2])
87
+ RelaxDB.db.put_count.should == 0
88
+ end
89
+
90
+ it "should preserve given relationships across save/load boundary" do
91
+ i1, i2 = Item.new(:name => "i1"), Item.new(:name => "i2")
92
+ u = User.new(:items => [i1, i2])
93
+ u.save_all!
94
+ u = RelaxDB.load u._id
95
+ u.items.map { |i| i.name }.sort.join.should == "i1i2"
96
+ end
97
+
98
+ it "should invoke the derived properties writer" do
99
+ P = Class.new(RelaxDB::Document) do
100
+ property :foo, :derived => [:zongs, lambda {|f, o| o.zongs.first.z / 2 }]
101
+ has_many :zongs, :class => "Zong"
102
+ end
103
+ Zong = Class.new(RelaxDB::Document) do
104
+ property :z
105
+ belongs_to :p
106
+ end
107
+ oz = Zong.new(:z => 10)
108
+ op = P.new(:zongs => [oz])
109
+ op.foo.should == 5
81
110
  end
82
111
 
83
112
  end
data/spec/relaxdb_spec.rb CHANGED
@@ -28,20 +28,69 @@ describe RelaxDB do
28
28
  end
29
29
 
30
30
  end
31
-
31
+
32
+ # bulk_save and bulk_save! should match Document#save and Document#save! semantics
32
33
  describe ".bulk_save" do
33
34
 
34
35
  it "should be invokable multiple times" do
35
- t1 = Tag.new(:name => "t1")
36
- t2 = Tag.new(:name => "t2")
36
+ t1, t2 = Tag.new, Tag.new
37
37
  RelaxDB.bulk_save(t1, t2)
38
38
  RelaxDB.bulk_save(t1, t2)
39
39
  end
40
40
 
41
+ it "should return the objects it was passed" do
42
+ t1, t2 = Tag.new, Tag.new
43
+ ta, tb = RelaxDB.bulk_save(t1, t2)
44
+ ta.should == t1
45
+ tb.should == t2
46
+ end
47
+
41
48
  it "should succeed when passed no args" do
42
49
  RelaxDB.bulk_save
43
50
  end
44
51
 
52
+ it "should return false on failure" do
53
+ c = Class.new(RelaxDB::Document) do
54
+ property :foo, :validator => lambda { false }
55
+ end
56
+ x = c.new
57
+ RelaxDB.bulk_save(x).should be_false
58
+ end
59
+
60
+ it "should not attempt to save if a pre-save stage fails" do
61
+ c = Class.new(RelaxDB::Document) do
62
+ property :foo, :validator => lambda { false }
63
+ end
64
+ x = c.new
65
+ RelaxDB.bulk_save(x)
66
+ x.should be_new_document
67
+ end
68
+
69
+ it "should invoke the after-save stage after a successful save" do
70
+ c = Class.new(RelaxDB::Document) do
71
+ attr_accessor :foo
72
+ after_save lambda { |c| c.foo = :bar }
73
+ end
74
+ x = c.new
75
+ RelaxDB.bulk_save(x).first.foo.should == :bar
76
+ end
77
+
78
+ end
79
+
80
+ describe ".bulk_save!" do
81
+
82
+ it "should raise an exception if a obj fails validation" do
83
+ c = Class.new(RelaxDB::Document) do
84
+ property :foo, :validator => lambda { false }
85
+ end
86
+ lambda { RelaxDB.bulk_save!(c.new) }.should raise_error(RelaxDB::ValidationFailure)
87
+ end
88
+
89
+ it "should raise an exception if a document update conflict occurs on save" do
90
+ Atom.new(:_id => "a1").save!
91
+ lambda { RelaxDB.bulk_save! Atom.new(:_id => "a1") }.should raise_error(RelaxDB::UpdateConflict)
92
+ end
93
+
45
94
  end
46
95
 
47
96
  describe ".replicate_db" do
@@ -68,7 +117,7 @@ describe RelaxDB do
68
117
 
69
118
  it "should load an arbitrary number of documents" do
70
119
  a1, a2 = Atom.new.save, Atom.new.save
71
- ar1, ar2 = RelaxDB.load a1._id, a2._id
120
+ ar1, ar2 = RelaxDB.load [a1._id, a2._id]
72
121
  ar1.should == a1
73
122
  ar2.should == a2
74
123
  end
@@ -79,7 +128,7 @@ describe RelaxDB do
79
128
 
80
129
  it "should return an array with correctly placed nils when given a list containing non existant doc ids" do
81
130
  a1, a2 = Atom.new.save, Atom.new.save
82
- res = RelaxDB.load nil, a1._id, nil, a2._id, nil
131
+ res = RelaxDB.load [nil, a1._id, nil, a2._id, nil]
83
132
  res.should == [nil, a1, nil, a2, nil]
84
133
  end
85
134
 
@@ -95,7 +144,7 @@ describe RelaxDB do
95
144
 
96
145
  it "should load multiple documents" do
97
146
  a1, a2 = Atom.new.save, Atom.new.save
98
- ar1, ar2 = RelaxDB.load! a1._id, a2._id
147
+ ar1, ar2 = RelaxDB.load! [a1._id, a2._id]
99
148
  ar1.should == a1
100
149
  ar2.should == a2
101
150
  end
@@ -109,7 +158,7 @@ describe RelaxDB do
109
158
  it "should throw an exception if any of a list of doc ids is for a non-existant doc" do
110
159
  a = Atom.new.save
111
160
  lambda do
112
- RelaxDB.load! nil, a._id
161
+ RelaxDB.load! [nil, a._id]
113
162
  end.should raise_error(RelaxDB::NotFound)
114
163
  end
115
164
 
data/spec/spec_models.rb CHANGED
@@ -1,139 +1,150 @@
1
- class Atom < RelaxDB::Document
2
- end
1
+ #
2
+ # RSpec loads this file multiple times, thus breaking invocations like Document.has_one_rels
3
+ # The following clause ensures the tests pass, but perhaps Document should raise a warning
4
+ # if it's loaded more than once...
5
+ #
6
+ unless @spec_models_loaded
7
+
8
+ class Atom < RelaxDB::Document
9
+ end
3
10
 
4
- class Initiative < RelaxDB::Document
5
- property :x
6
- attr_reader :foo
7
- def initialize(svw=true, hash={})
8
- super
9
- @foo = :bar
11
+ class Initiative < RelaxDB::Document
12
+ property :x
13
+ attr_reader :foo
14
+ def initialize(*data)
15
+ super *data
16
+ @foo = :bar
17
+ end
10
18
  end
11
- end
12
19
 
13
- class Primitives < RelaxDB::Document
20
+ class Primitives < RelaxDB::Document
14
21
 
15
- property :str
16
- property :num
17
- property :true_bool
18
- property :false_bool
19
- property :created_at
20
- property :empty
22
+ property :str
23
+ property :num
24
+ property :true_bool
25
+ property :false_bool
26
+ property :created_at
27
+ property :empty
21
28
 
22
- end
29
+ end
23
30
 
24
- class BespokeReader < RelaxDB::Document
25
- property :val
26
- def val; @val + 5; end
27
- end
31
+ class BespokeReader < RelaxDB::Document
32
+ property :val
33
+ def val; @val + 5; end
34
+ end
28
35
 
29
- class BespokeWriter < RelaxDB::Document
30
- property :val
31
- def val=(v); @val = v - 10; end
32
- end
36
+ class BespokeWriter < RelaxDB::Document
37
+ property :val
38
+ def val=(v); @val = v - 10; end
39
+ end
33
40
 
34
- class Letter < RelaxDB::Document
41
+ class Letter < RelaxDB::Document
35
42
 
36
- property :letter
37
- property :number
43
+ property :letter
44
+ property :number
38
45
 
39
- end
46
+ end
40
47
 
41
- class Invite < RelaxDB::Document
48
+ class Invite < RelaxDB::Document
42
49
 
43
- property :message
50
+ property :message
44
51
 
45
- belongs_to :sender
46
- belongs_to :recipient
52
+ belongs_to :sender
53
+ belongs_to :recipient
47
54
 
48
- end
55
+ end
49
56
 
50
- class Item < RelaxDB::Document
57
+ class Item < RelaxDB::Document
51
58
 
52
- property :name
53
- belongs_to :user
59
+ property :name
60
+ belongs_to :user
54
61
 
55
- end
62
+ end
56
63
 
57
- class User < RelaxDB::Document
64
+ class User < RelaxDB::Document
58
65
 
59
- property :name
60
- property :age
66
+ property :name, :default => "u"
67
+ property :age
61
68
 
62
- has_many :items, :class => "Item"
69
+ has_many :items, :class => "Item"
63
70
 
64
- has_many :invites_received, :class => "Invite", :known_as => :recipient
65
- has_many :invites_sent, :class => "Invite", :known_as => :sender
71
+ has_many :invites_received, :class => "Invite", :known_as => :recipient
72
+ has_many :invites_sent, :class => "Invite", :known_as => :sender
66
73
 
67
- end
74
+ end
68
75
 
69
- class Post < RelaxDB::Document
76
+ class Post < RelaxDB::Document
70
77
 
71
- property :subject
72
- property :content
73
- property :created_at
74
- property :viewed_at
78
+ property :subject
79
+ property :content
80
+ property :created_at
81
+ property :viewed_at
75
82
 
76
- end
83
+ end
77
84
 
78
- class Rating < RelaxDB::Document
85
+ class Rating < RelaxDB::Document
79
86
 
80
- property :stars, :default => 5
81
- belongs_to :photo
87
+ property :stars, :default => 5
88
+ belongs_to :photo
82
89
 
83
- end
90
+ end
84
91
 
85
- class Photo < RelaxDB::Document
92
+ class Photo < RelaxDB::Document
86
93
 
87
- property :name
94
+ property :name
88
95
 
89
- has_one :rating
96
+ has_one :rating
90
97
 
91
- references_many :tags, :class => "Tag", :known_as => :photos
98
+ references_many :tags, :class => "Tag", :known_as => :photos
92
99
 
93
- has_many :taggings, :class => "Tagging"
100
+ has_many :taggings, :class => "Tagging"
94
101
 
95
- end
102
+ end
96
103
 
97
- class Tag < RelaxDB::Document
104
+ class Tag < RelaxDB::Document
98
105
 
99
- property :name
100
- references_many :photos, :class => "Photo", :known_as => :tags
106
+ property :name
107
+ references_many :photos, :class => "Photo", :known_as => :tags
101
108
 
102
- has_many :taggings, :class => "Tagging"
109
+ has_many :taggings, :class => "Tagging"
103
110
 
104
- end
111
+ end
105
112
 
106
- class Tagging < RelaxDB::Document
113
+ class Tagging < RelaxDB::Document
107
114
 
108
- belongs_to :photo
109
- belongs_to :tag
110
- property :relevance
115
+ belongs_to :photo
116
+ belongs_to :tag
117
+ property :relevance
111
118
 
112
- end
119
+ end
113
120
 
114
- class MultiWordClass < RelaxDB::Document
115
- has_one :multi_word_child
116
- has_many :multi_word_children, :class => "MultiWordChild"
117
- end
121
+ class MultiWordClass < RelaxDB::Document
122
+ has_one :multi_word_child
123
+ has_many :multi_word_children, :class => "MultiWordChild"
124
+ end
118
125
 
119
- class MultiWordChild < RelaxDB::Document
120
- belongs_to :multi_word_class
121
- end
126
+ class MultiWordChild < RelaxDB::Document
127
+ belongs_to :multi_word_class
128
+ end
122
129
 
123
- class TwitterUser < RelaxDB::Document
130
+ class TwitterUser < RelaxDB::Document
124
131
 
125
- property :name
126
- references_many :followers, :class => "User", :known_as => :leaders
127
- references_many :leaders, :class => "User", :known_as => :followers
132
+ property :name
133
+ references_many :followers, :class => "User", :known_as => :leaders
134
+ references_many :leaders, :class => "User", :known_as => :followers
128
135
 
129
- end
136
+ end
130
137
 
131
- class Dysfunctional < RelaxDB::Document
132
- has_one :failure
133
- has_many :failures, :class => "Failure"
134
- end
138
+ class Dysfunctional < RelaxDB::Document
139
+ has_one :failure
140
+ has_many :failures, :class => "Failure"
141
+ end
142
+
143
+ class Failure < RelaxDB::Document
144
+ property :pathological, :validator => lambda { false }
145
+ belongs_to :dysfunctional
146
+ end
147
+
148
+ @spec_models_loaded = true
135
149
 
136
- class Failure < RelaxDB::Document
137
- property :pathological, :validator => lambda { false }
138
- belongs_to :dysfunctional
139
150
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paulcarey-relaxdb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Carey
@@ -9,7 +9,7 @@ autorequire: relaxdb
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-01-08 00:00:00 -08:00
12
+ date: 2009-01-18 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency