paulcarey-relaxdb 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.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