mattetti-couchrest 0.33 → 0.34

Sign up to get free protection for your applications and to get access to all the features.
@@ -38,3 +38,22 @@ module CouchRest
38
38
 
39
39
  end
40
40
  end
41
+
42
+ class CastedArray < Array
43
+ attr_accessor :casted_by
44
+
45
+ def << obj
46
+ obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
47
+ super(obj)
48
+ end
49
+
50
+ def push(obj)
51
+ obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
52
+ super(obj)
53
+ end
54
+
55
+ def []= index, obj
56
+ obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
57
+ super(index, obj)
58
+ end
59
+ end
@@ -1,5 +1,5 @@
1
- # Copyright (c) 2004-2008 David Heinemeier Hansson
2
- #
1
+ # Copyright (c) 2006-2009 David Heinemeier Hansson
2
+ #
3
3
  # Permission is hereby granted, free of charge, to any person obtaining
4
4
  # a copy of this software and associated documentation files (the
5
5
  # "Software"), to deal in the Software without restriction, including
@@ -7,10 +7,10 @@
7
7
  # distribute, sublicense, and/or sell copies of the Software, and to
8
8
  # permit persons to whom the Software is furnished to do so, subject to
9
9
  # the following conditions:
10
- #
10
+ #
11
11
  # The above copyright notice and this permission notice shall be
12
12
  # included in all copies or substantial portions of the Software.
13
- #
13
+ #
14
14
  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
15
  # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
16
  # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
@@ -18,79 +18,59 @@
18
18
  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
-
22
- # Allows attributes to be shared within an inheritance hierarchy, but where
23
- # each descendant gets a copy of their parents' attributes, instead of just a
24
- # pointer to the same. This means that the child can add elements to, for
25
- # example, an array without those additions being shared with either their
26
- # parent, siblings, or children, which is unlike the regular class-level
27
- # attributes that are shared across the entire hierarchy.
21
+ #
22
+ # Extracted From
23
+ # http://github.com/rails/rails/commit/971e2438d98326c994ec6d3ef8e37b7e868ed6e2
24
+
25
+ # Extends the class object with class and instance accessors for class attributes,
26
+ # just like the native attr* accessors for instance attributes.
27
+ #
28
+ # class Person
29
+ # cattr_accessor :hair_colors
30
+ # end
31
+ #
32
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
28
33
  class Class
29
- # Defines class-level and instance-level attribute reader.
30
- #
31
- # @param *syms<Array> Array of attributes to define reader for.
32
- # @return <Array[#to_s]> List of attributes that were made into cattr_readers
33
- #
34
- # @api public
35
- #
36
- # @todo Is this inconsistent in that it does not allow you to prevent
37
- # an instance_reader via :instance_reader => false
38
34
  def cattr_reader(*syms)
39
35
  syms.flatten.each do |sym|
40
36
  next if sym.is_a?(Hash)
41
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
42
- unless defined? @@#{sym}
43
- @@#{sym} = nil
44
- end
45
-
46
- def self.#{sym}
47
- @@#{sym}
48
- end
49
-
50
- def #{sym}
51
- @@#{sym}
52
- end
53
- RUBY
37
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
38
+ unless defined? @@#{sym} # unless defined? @@hair_colors
39
+ @@#{sym} = nil # @@hair_colors = nil
40
+ end # end
41
+ #
42
+ def self.#{sym} # def self.hair_colors
43
+ @@#{sym} # @@hair_colors
44
+ end # end
45
+ #
46
+ def #{sym} # def hair_colors
47
+ @@#{sym} # @@hair_colors
48
+ end # end
49
+ EOS
54
50
  end
55
51
  end unless Class.respond_to?(:cattr_reader)
56
52
 
57
- # Defines class-level (and optionally instance-level) attribute writer.
58
- #
59
- # @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
60
- # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
61
- # @return <Array[#to_s]> List of attributes that were made into cattr_writers
62
- #
63
- # @api public
64
53
  def cattr_writer(*syms)
65
- options = syms.last.is_a?(Hash) ? syms.pop : {}
54
+ options = syms.extract_options!
66
55
  syms.flatten.each do |sym|
67
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
68
- unless defined? @@#{sym}
69
- @@#{sym} = nil
70
- end
71
-
72
- def self.#{sym}=(obj)
73
- @@#{sym} = obj
74
- end
75
- RUBY
76
-
77
- unless options[:instance_writer] == false
78
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
79
- def #{sym}=(obj)
80
- @@#{sym} = obj
81
- end
82
- RUBY
83
- end
56
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
57
+ unless defined? @@#{sym} # unless defined? @@hair_colors
58
+ @@#{sym} = nil # @@hair_colors = nil
59
+ end # end
60
+ #
61
+ def self.#{sym}=(obj) # def self.hair_colors=(obj)
62
+ @@#{sym} = obj # @@hair_colors = obj
63
+ end # end
64
+ #
65
+ #{" #
66
+ def #{sym}=(obj) # def hair_colors=(obj)
67
+ @@#{sym} = obj # @@hair_colors = obj
68
+ end # end
69
+ " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
70
+ EOS
84
71
  end
85
72
  end unless Class.respond_to?(:cattr_writer)
86
73
 
87
- # Defines class-level (and optionally instance-level) attribute accessor.
88
- #
89
- # @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
90
- # @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
91
- # @return <Array[#to_s]> List of attributes that were made into accessors
92
- #
93
- # @api public
94
74
  def cattr_accessor(*syms)
95
75
  cattr_reader(*syms)
96
76
  cattr_writer(*syms)
@@ -156,6 +136,8 @@ class Class
156
136
  def #{ivar}=(obj) self.class.#{ivar} = obj end
157
137
  RUBY
158
138
  end
139
+
140
+ self.send("#{ivar}=", yield) if block_given?
159
141
  end
160
142
  end unless Class.respond_to?(:extlib_inheritable_writer)
161
143
 
@@ -168,9 +150,41 @@ class Class
168
150
  # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
169
151
  #
170
152
  # @api public
171
- def extlib_inheritable_accessor(*syms)
153
+ def extlib_inheritable_accessor(*syms, &block)
172
154
  extlib_inheritable_reader(*syms)
173
- extlib_inheritable_writer(*syms)
155
+ extlib_inheritable_writer(*syms, &block)
174
156
  end unless Class.respond_to?(:extlib_inheritable_accessor)
175
157
  end
176
158
 
159
+ class Array
160
+ # Extracts options from a set of arguments. Removes and returns the last
161
+ # element in the array if it's a hash, otherwise returns a blank hash.
162
+ #
163
+ # def options(*args)
164
+ # args.extract_options!
165
+ # end
166
+ #
167
+ # options(1, 2) # => {}
168
+ # options(1, 2, :a => :b) # => {:a=>:b}
169
+ def extract_options!
170
+ last.is_a?(::Hash) ? pop : {}
171
+ end unless Array.new.respond_to?(:extract_options!)
172
+
173
+ # Wraps the object in an Array unless it's an Array. Converts the
174
+ # object to an Array using #to_ary if it implements that.
175
+ def self.wrap(object)
176
+ case object
177
+ when nil
178
+ []
179
+ when self
180
+ object
181
+ else
182
+ if object.respond_to?(:to_ary)
183
+ object.to_ary
184
+ else
185
+ [object]
186
+ end
187
+ end
188
+ end unless Array.respond_to?(:wrap)
189
+ end
190
+
@@ -1,8 +1,4 @@
1
1
  # This file contains various hacks for Rails compatibility.
2
- # To use, just require in environment.rb, like so:
3
- #
4
- # require 'couchrest/support/rails'
5
-
6
2
  class Hash
7
3
  # Hack so that CouchRest::Document, which descends from Hash,
8
4
  # doesn't appear to Rails routing as a Hash of options
@@ -12,8 +8,10 @@ class Hash
12
8
  end
13
9
  end
14
10
 
15
-
16
11
  CouchRest::Document.class_eval do
12
+ # Need this when passing doc to a resourceful route
13
+ alias_method :to_param, :id
14
+
17
15
  # Hack so that CouchRest::Document, which descends from Hash,
18
16
  # doesn't appear to Rails routing as a Hash of options
19
17
  def is_a?(o)
@@ -23,6 +21,15 @@ CouchRest::Document.class_eval do
23
21
  alias_method :kind_of?, :is_a?
24
22
  end
25
23
 
24
+ CouchRest::CastedModel.class_eval do
25
+ # The to_param method is needed for rails to generate resourceful routes.
26
+ # In your controller, remember that it's actually the id of the document.
27
+ def id
28
+ return nil if base_doc.nil?
29
+ base_doc.id
30
+ end
31
+ alias_method :to_param, :id
32
+ end
26
33
 
27
34
  require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors')
28
35
 
@@ -253,7 +253,7 @@ describe CouchRest::Database do
253
253
  describe "PUT attachment from file" do
254
254
  before(:each) do
255
255
  filename = FIXTURE_PATH + '/attachments/couchdb.png'
256
- @file = File.open(filename)
256
+ @file = File.open(filename, "rb")
257
257
  end
258
258
  after(:each) do
259
259
  @file.close
@@ -552,7 +552,7 @@ describe CouchRest::Database do
552
552
  newdoc['artist'].should == 'Zappa'
553
553
  end
554
554
  it "should fail without an _id" do
555
- lambda{@db.copy({"not"=>"a real doc"})}.should raise_error(ArgumentError)
555
+ lambda{@db.copy_doc({"not"=>"a real doc"})}.should raise_error(ArgumentError)
556
556
  end
557
557
  end
558
558
  describe "to an existing location" do
@@ -43,16 +43,14 @@ describe "assigning a value to casted attribute after initializing an object" do
43
43
  @car.driver.should be_nil
44
44
  end
45
45
 
46
- # Note that this isn't casting the attribute, it's just assigning it a value
47
- # (see "should not cast attribute")
48
46
  it "should let you assign the value" do
49
47
  @car.driver = @driver
50
48
  @car.driver.name.should == 'Matt'
51
49
  end
52
50
 
53
- it "should not cast attribute" do
51
+ it "should cast attribute" do
54
52
  @car.driver = JSON.parse(JSON.generate(@driver))
55
- @car.driver.should_not be_instance_of(Driver)
53
+ @car.driver.should be_instance_of(Driver)
56
54
  end
57
55
 
58
56
  end
@@ -4,6 +4,8 @@ require File.expand_path('../../../spec_helper', __FILE__)
4
4
  require File.join(FIXTURE_PATH, 'more', 'card')
5
5
  require File.join(FIXTURE_PATH, 'more', 'cat')
6
6
  require File.join(FIXTURE_PATH, 'more', 'person')
7
+ require File.join(FIXTURE_PATH, 'more', 'question')
8
+ require File.join(FIXTURE_PATH, 'more', 'course')
7
9
 
8
10
 
9
11
  class WithCastedModelMixin < Hash
@@ -21,6 +23,26 @@ class DummyModel < CouchRest::ExtendedDocument
21
23
  property :keywords, :cast_as => ["String"]
22
24
  end
23
25
 
26
+ class CastedCallbackDoc < CouchRest::ExtendedDocument
27
+ use_database TEST_SERVER.default_database
28
+ raise "Default DB not set" if TEST_SERVER.default_database.nil?
29
+ property :callback_model, :cast_as => 'WithCastedCallBackModel'
30
+ end
31
+ class WithCastedCallBackModel < Hash
32
+ include CouchRest::CastedModel
33
+ include CouchRest::Validation
34
+ property :name
35
+ property :run_before_validate
36
+ property :run_after_validate
37
+
38
+ before_validate do |object|
39
+ object.run_before_validate = true
40
+ end
41
+ after_validate do |object|
42
+ object.run_after_validate = true
43
+ end
44
+ end
45
+
24
46
  describe CouchRest::CastedModel do
25
47
 
26
48
  describe "A non hash class including CastedModel" do
@@ -106,7 +128,40 @@ describe CouchRest::CastedModel do
106
128
  @obj.keywords.should be_an_instance_of(Array)
107
129
  @obj.keywords.first.should == 'couch'
108
130
  end
131
+ end
132
+
133
+ describe "update attributes without saving" do
134
+ before(:each) do
135
+ @question = Question.new(:q => "What is your quest?", :a => "To seek the Holy Grail")
136
+ end
137
+ it "should work for attribute= methods" do
138
+ @question.q.should == "What is your quest?"
139
+ @question['a'].should == "To seek the Holy Grail"
140
+ @question.update_attributes_without_saving(:q => "What is your favorite color?", 'a' => "Blue")
141
+ @question['q'].should == "What is your favorite color?"
142
+ @question.a.should == "Blue"
143
+ end
144
+
145
+ it "should also work for attributes= alias" do
146
+ @question.respond_to?(:attributes=).should be_true
147
+ @question.attributes = {:q => "What is your favorite color?", 'a' => "Blue"}
148
+ @question['q'].should == "What is your favorite color?"
149
+ @question.a.should == "Blue"
150
+ end
151
+
152
+ it "should flip out if an attribute= method is missing" do
153
+ lambda {
154
+ @q.update_attributes_without_saving('foo' => "something", :a => "No green")
155
+ }.should raise_error(NoMethodError)
156
+ end
109
157
 
158
+ it "should not change any attributes if there is an error" do
159
+ lambda {
160
+ @q.update_attributes_without_saving('foo' => "something", :a => "No green")
161
+ }.should raise_error(NoMethodError)
162
+ @question.q.should == "What is your quest?"
163
+ @question.a.should == "To seek the Holy Grail"
164
+ end
110
165
  end
111
166
 
112
167
  describe "saved document with casted models" do
@@ -154,6 +209,10 @@ describe CouchRest::CastedModel do
154
209
  toy = CatToy.new :name => "Mouse"
155
210
  @cat.toys.push(toy)
156
211
  @cat.save.should be_true
212
+ @cat = Cat.get @cat.id
213
+ @cat.toys.class.should == CastedArray
214
+ @cat.toys.first.class.should == CatToy
215
+ @cat.toys.first.should === toy
157
216
  end
158
217
 
159
218
  it "should fail because name is not present" do
@@ -171,7 +230,177 @@ describe CouchRest::CastedModel do
171
230
  cat.masters.push Person.new
172
231
  cat.should be_valid
173
232
  end
233
+ end
234
+
235
+ describe "calling valid?" do
236
+ before :each do
237
+ @cat = Cat.new
238
+ @toy1 = CatToy.new
239
+ @toy2 = CatToy.new
240
+ @toy3 = CatToy.new
241
+ @cat.favorite_toy = @toy1
242
+ @cat.toys << @toy2
243
+ @cat.toys << @toy3
244
+ end
245
+
246
+ describe "on the top document" do
247
+ it "should put errors on all invalid casted models" do
248
+ @cat.should_not be_valid
249
+ @cat.errors.should_not be_empty
250
+ @toy1.errors.should_not be_empty
251
+ @toy2.errors.should_not be_empty
252
+ @toy3.errors.should_not be_empty
253
+ end
254
+
255
+ it "should not put errors on valid casted models" do
256
+ @toy1.name = "Feather"
257
+ @toy2.name = "Twine"
258
+ @cat.should_not be_valid
259
+ @cat.errors.should_not be_empty
260
+ @toy1.errors.should be_empty
261
+ @toy2.errors.should be_empty
262
+ @toy3.errors.should_not be_empty
263
+ end
264
+ end
265
+
266
+ describe "on a casted model property" do
267
+ it "should only validate itself" do
268
+ @toy1.should_not be_valid
269
+ @toy1.errors.should_not be_empty
270
+ @cat.errors.should be_empty
271
+ @toy2.errors.should be_empty
272
+ @toy3.errors.should be_empty
273
+ end
274
+ end
275
+
276
+ describe "on a casted model inside a casted collection" do
277
+ it "should only validate itself" do
278
+ @toy2.should_not be_valid
279
+ @toy2.errors.should_not be_empty
280
+ @cat.errors.should be_empty
281
+ @toy1.errors.should be_empty
282
+ @toy3.errors.should be_empty
283
+ end
284
+ end
285
+ end
286
+
287
+ describe "calling new? on a casted model" do
288
+ before :each do
289
+ reset_test_db!
290
+ @cat = Cat.new(:name => 'Sockington')
291
+ @favorite_toy = CatToy.new(:name => 'Catnip Ball')
292
+ @cat.favorite_toy = @favorite_toy
293
+ @cat.toys << CatToy.new(:name => 'Fuzzy Stick')
294
+ end
174
295
 
296
+ it "should be true on new" do
297
+ CatToy.new.should be_new
298
+ CatToy.new.new_record?.should be_true
299
+ end
300
+
301
+ it "should be true after assignment" do
302
+ @cat.should be_new
303
+ @cat.favorite_toy.should be_new
304
+ @cat.toys.first.should be_new
305
+ end
306
+
307
+ it "should not be true after create or save" do
308
+ @cat.create
309
+ @cat.save
310
+ @cat.favorite_toy.should_not be_new
311
+ @cat.toys.first.should_not be_new
312
+ end
313
+
314
+ it "should not be true after get from the database" do
315
+ @cat.save
316
+ @cat = Cat.get(@cat.id)
317
+ @cat.favorite_toy.should_not be_new
318
+ @cat.toys.first.should_not be_new
319
+ end
320
+
321
+ it "should still be true after a failed create or save" do
322
+ @cat.name = nil
323
+ @cat.create.should be_false
324
+ @cat.save.should be_false
325
+ @cat.favorite_toy.should be_new
326
+ @cat.toys.first.should be_new
327
+ end
328
+ end
329
+
330
+ describe "calling base_doc from a nested casted model" do
331
+ before :each do
332
+ @course = Course.new(:title => 'Science 101')
333
+ @professor = Person.new(:name => 'Professor Plum')
334
+ @cat = Cat.new(:name => 'Scratchy')
335
+ @toy1 = CatToy.new
336
+ @toy2 = CatToy.new
337
+ @course.professor = @professor
338
+ @professor.pet = @cat
339
+ @cat.favorite_toy = @toy1
340
+ @cat.toys << @toy2
341
+ end
342
+
343
+ it "should reference the top document for" do
344
+ @course.base_doc.should === @course
345
+ @professor.casted_by.should === @course
346
+ @professor.base_doc.should === @course
347
+ @cat.base_doc.should === @course
348
+ @toy1.base_doc.should === @course
349
+ @toy2.base_doc.should === @course
350
+ end
351
+
352
+ it "should call setter on top document" do
353
+ @toy1.base_doc.should_not be_nil
354
+ @toy1.base_doc.title = 'Tom Foolery'
355
+ @course.title.should == 'Tom Foolery'
356
+ end
357
+
358
+ it "should return nil if not yet casted" do
359
+ person = Person.new
360
+ person.base_doc.should == nil
361
+ end
362
+ end
363
+
364
+ describe "calling base_doc.save from a nested casted model" do
365
+ before :each do
366
+ reset_test_db!
367
+ @cat = Cat.new(:name => 'Snowball')
368
+ @toy = CatToy.new
369
+ @cat.favorite_toy = @toy
370
+ end
371
+
372
+ it "should not save parent document when casted model is invalid" do
373
+ @toy.should_not be_valid
374
+ @toy.base_doc.save.should be_false
375
+ lambda{@toy.base_doc.save!}.should raise_error
376
+ end
377
+
378
+ it "should save parent document when nested casted model is valid" do
379
+ @toy.name = "Mr Squeaks"
380
+ @toy.should be_valid
381
+ @toy.base_doc.save.should be_true
382
+ lambda{@toy.base_doc.save!}.should_not raise_error
383
+ end
175
384
  end
176
385
 
386
+ describe "callbacks" do
387
+ before(:each) do
388
+ @doc = CastedCallbackDoc.new
389
+ @model = WithCastedCallBackModel.new
390
+ @doc.callback_model = @model
391
+ end
392
+
393
+ describe "validate" do
394
+ it "should run before_validate before validating" do
395
+ @model.run_before_validate.should be_nil
396
+ @model.should be_valid
397
+ @model.run_before_validate.should be_true
398
+ end
399
+ it "should run after_validate after validating" do
400
+ @model.run_after_validate.should be_nil
401
+ @model.should be_valid
402
+ @model.run_after_validate.should be_true
403
+ end
404
+ end
405
+ end
177
406
  end