couch_potato 0.2.12 → 0.2.13

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Changes
2
2
 
3
+ ### 0.2.13
4
+
5
+ * support adding errors in before_validation callbacks (mattmatt)
6
+ * support for inheritance (mattmatt)
7
+ * support for save without validations (mattmatt)
8
+ * improved (de)serialization now supports deserializing nested objects (railsbros, specs by hagenburger)
9
+ * RSpec matchers for testing map/reduce functions (langalex)
10
+
3
11
  ### 0.2.10
4
12
  * fixed bug with hardcoded timezone
5
13
 
data/README.md CHANGED
@@ -22,13 +22,11 @@ Lastly Couch Potato aims to provide a seamless integration with Ruby on Rails, e
22
22
 
23
23
  Couch Potato is hosted as a gem on github which you can install like this:
24
24
 
25
- sudo gem source --add http://gems.github.com # if you haven't already
26
- sudo gem install langalex-couch_potato
25
+ sudo gem install couch_potato --source http://gemcutter.org
27
26
 
28
27
  #### Using with your ruby application:
29
28
 
30
29
  require 'rubygems'
31
- gem 'langalex-couch_potato'
32
30
  require 'couch_potato'
33
31
 
34
32
  Alternatively you can download or clone the source repository and then require lib/couch_potato.rb.
@@ -45,7 +43,8 @@ The server URL will default to http://localhost:5984/ unless specified:
45
43
 
46
44
  Add to your config/environment.rb:
47
45
 
48
- config.gem 'langalex-couch_potato', :lib => 'couch_potato', :source => 'http://gems.github.com'
46
+ config.gem 'couch_potato', :source => 'http://gemcutter.org'
47
+ config.frameworks -= [:active_record] # if you switch completely
49
48
 
50
49
  Then create a config/couchdb.yml:
51
50
 
@@ -282,6 +281,36 @@ To make testing easier and faster database logic has been put into its own class
282
281
 
283
282
  By creating you own instances of CouchPotato::Database and passing them a fake CouchRest database instance you can completely disconnect your unit tests/spec from the database.
284
283
 
284
+ ##### Testing map/reduce functions
285
+
286
+ Couch Potato provides custom RSpec matchers for testing the map and reduce functions of your views. For example you can do this:
287
+
288
+ Class User
289
+ include CouchPotato::Persistence
290
+
291
+ view :by_name, :key => :name
292
+ view :by_age, :key => :age
293
+ end
294
+
295
+ #RSpec
296
+ describe User, 'by_name' do
297
+ it "should map users to their name" do
298
+ User.by_name.should map({:name => 'bill', :age => 23}).to([['bill', null]])
299
+ end
300
+
301
+ it "should reduce the users to the sum of their age" do
302
+ User.by_age.should reduce([], [[23], [22]]).to(45)
303
+ end
304
+
305
+ it "should rereduce" do
306
+ User.by_age.should rereduce([], [[23], [22]]).to(45)
307
+ end
308
+ end
309
+
310
+ This will actually run your map/reduce functions in a JavaScript interpreter, passing the arguments as JSON and converting the results back to Ruby.
311
+
312
+ In order for this to work you must have the `js` executable in your PATH. This is usually part of the _spidermonkey_ package/port. (On MacPorts that's _spidemonkey_, on Linux it could be one of _libjs_, _libmozjs_ or _libspidermonkey_). When you installed CouchDB via your packet manager Spidermonkey should already be there.
313
+
285
314
  ### Helping out
286
315
 
287
316
  Please fix bugs, add more specs, implement new features by forking the github repo at http://github.com/langalex/couch_potato.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :major: 0
3
2
  :minor: 2
4
- :patch: 12
3
+ :patch: 13
4
+ :major: 0
@@ -19,12 +19,13 @@ module CouchPotato
19
19
  spec.process_results results
20
20
  end
21
21
 
22
- def save_document(document)
22
+
23
+ def save_document(document, validate = true)
23
24
  return true unless document.dirty?
24
25
  if document.new?
25
- create_document document
26
+ create_document(document, validate)
26
27
  else
27
- update_document document
28
+ update_document(document, validate)
28
29
  end
29
30
  end
30
31
  alias_method :save, :save_document
@@ -47,9 +48,7 @@ module CouchPotato
47
48
  def load_document(id)
48
49
  raise "Can't load a document without an id (got nil)" if id.nil?
49
50
  begin
50
- json = database.get(id)
51
- klass = json['ruby_class'].split('::').inject(Class){|scope, const| scope.const_get(const)}
52
- instance = klass.json_create json
51
+ instance = database.get(id)
53
52
  instance.database = self
54
53
  instance
55
54
  rescue(RestClient::ResourceNotFound)
@@ -70,11 +69,16 @@ module CouchPotato
70
69
  end
71
70
  end
72
71
 
73
- def create_document(document)
72
+ def create_document(document, validate)
74
73
  document.database = self
75
- document.run_callbacks :before_validation_on_save
76
- document.run_callbacks :before_validation_on_create
77
- return unless document.valid?
74
+
75
+ if validate
76
+ document.errors.clear
77
+ document.run_callbacks :before_validation_on_save
78
+ document.run_callbacks :before_validation_on_create
79
+ return false unless valid_document?(document)
80
+ end
81
+
78
82
  document.run_callbacks :before_save
79
83
  document.run_callbacks :before_create
80
84
  res = database.save_doc clean_hash(document.to_hash)
@@ -85,10 +89,14 @@ module CouchPotato
85
89
  true
86
90
  end
87
91
 
88
- def update_document(document)
89
- document.run_callbacks :before_validation_on_save
90
- document.run_callbacks :before_validation_on_update
91
- return unless document.valid?
92
+ def update_document(document, validate)
93
+ if validate
94
+ document.errors.clear
95
+ document.run_callbacks :before_validation_on_save
96
+ document.run_callbacks :before_validation_on_update
97
+ return false unless valid_document?(document)
98
+ end
99
+
92
100
  document.run_callbacks :before_save
93
101
  document.run_callbacks :before_update
94
102
  res = database.save_doc clean_hash(document.to_hash)
@@ -98,6 +106,15 @@ module CouchPotato
98
106
  true
99
107
  end
100
108
 
109
+ def valid_document?(document)
110
+ errors = document.errors.errors.dup
111
+ document.valid?
112
+ errors.each do |k, v|
113
+ v.each {|message| document.errors.add(k, message)}
114
+ end
115
+ document.errors.empty?
116
+ end
117
+
101
118
  def database
102
119
  @database
103
120
  end
@@ -15,7 +15,7 @@ module CouchPotato
15
15
  (self.class.properties).inject({}) do |props, property|
16
16
  property.serialize(props, self)
17
17
  props
18
- end.merge('ruby_class' => self.class.name).merge(id_and_rev_json)
18
+ end.merge(JSON.create_id => self.class.name).merge(id_and_rev_json)
19
19
  end
20
20
 
21
21
  private
@@ -3,12 +3,41 @@ require File.dirname(__FILE__) + '/simple_property'
3
3
  module CouchPotato
4
4
  module Persistence
5
5
  module Properties
6
+ class PropertyList
7
+ include Enumerable
8
+
9
+ attr_accessor :list
10
+
11
+ def initialize(clazz)
12
+ @clazz = clazz
13
+ @list = []
14
+ end
15
+
16
+ def each
17
+ (list + inherited_properties).each {|property| yield property}
18
+ end
19
+
20
+ def <<(property)
21
+ @list << property
22
+ end
23
+
24
+ def inherited_properties
25
+ superclazz = @clazz.superclass
26
+ properties = []
27
+ while superclazz && superclazz.respond_to?(:properties)
28
+ properties << superclazz.properties.list
29
+ superclazz = superclazz.superclass
30
+ end
31
+ properties.flatten
32
+ end
33
+ end
34
+
6
35
  def self.included(base)
7
36
  base.extend ClassMethods
8
37
  base.class_eval do
9
38
  def self.properties
10
39
  @properties ||= {}
11
- @properties[self.name] ||= []
40
+ @properties[name] ||= PropertyList.new(self)
12
41
  end
13
42
  end
14
43
  end
@@ -12,11 +12,11 @@ module CouchPotato
12
12
 
13
13
  def build(object, json)
14
14
  value = json[name.to_s] || json[name.to_sym]
15
- typecast_value = if type
16
- type.json_create value
17
- else
18
- value
19
- end
15
+ typecast_value = if type && !value.instance_of?(type)
16
+ type.json_create value
17
+ else
18
+ value
19
+ end
20
20
  object.send "#{name}=", typecast_value
21
21
  end
22
22
 
@@ -8,8 +8,14 @@ module CouchPotato
8
8
  base.class_eval do
9
9
  # Override the validate method to first run before_validation callback
10
10
  def valid?
11
- self.run_callbacks :before_validation
11
+ errors.clear
12
+ run_callbacks :before_validation
13
+ before_validation_errors = errors.errors.dup
12
14
  super
15
+ before_validation_errors.each do |k, v|
16
+ v.each {|message| errors.add(k, message)}
17
+ end
18
+ errors.empty?
13
19
  end
14
20
  end
15
21
  end
@@ -0,0 +1,125 @@
1
+ require 'json'
2
+
3
+ module Spec
4
+ module Matchers
5
+ def map(document)
6
+ CouchPotato::RSpec::MapToProxy.new(document)
7
+ end
8
+
9
+ def reduce(docs, keys)
10
+ CouchPotato::RSpec::ReduceToProxy.new(docs, keys)
11
+ end
12
+
13
+ def rereduce(docs, keys)
14
+ CouchPotato::RSpec::ReduceToProxy.new(docs, keys, true)
15
+ end
16
+ end
17
+ end
18
+
19
+ module CouchPotato
20
+ module RSpec
21
+
22
+ module RunJS
23
+ private
24
+
25
+ def run_js(js)
26
+ path = 'couch_potato_js_runner.js'
27
+ File.open(path, 'w') {|f| f << js}
28
+ `js #{path}`
29
+ end
30
+ end
31
+
32
+
33
+ class MapToProxy
34
+ def initialize(input_ruby)
35
+ @input_ruby = input_ruby
36
+ end
37
+
38
+ def to(expected_ruby)
39
+ MapToMatcher.new(expected_ruby, @input_ruby)
40
+ end
41
+ end
42
+
43
+ class MapToMatcher
44
+ include RunJS
45
+
46
+ def initialize(expected_ruby, input_ruby)
47
+ @expected_ruby = expected_ruby
48
+ @input_ruby = input_ruby
49
+ end
50
+
51
+ def matches?(view_spec)
52
+ js = <<-JS
53
+ #{File.read(File.dirname(__FILE__) + '/print_r.js')}
54
+ var doc = #{@input_ruby.to_json};
55
+ var map = #{view_spec.map_function};
56
+ var result = [];
57
+ var emit = function(key, value) {
58
+ result.push([key, value]);
59
+ };
60
+ map(doc);
61
+ print(print_r(result));
62
+ JS
63
+ @actual_ruby = JSON.parse(run_js(js))
64
+ @expected_ruby == @actual_ruby
65
+ end
66
+
67
+ def failure_message_for_should
68
+ "Expected to map to #{@expected_ruby.inspect} but got #{@actual_ruby.inspect}."
69
+ end
70
+
71
+ def failure_message_for_should_not
72
+ "Expected not to map to #{@actual_ruby.inspect} but did."
73
+ end
74
+ end
75
+
76
+ class ReduceToProxy
77
+ def initialize(docs, keys, rereduce = false)
78
+ @docs, @keys, @rereduce = docs, keys, rereduce
79
+ end
80
+
81
+ def to(expected_ruby)
82
+ ReduceToMatcher.new(expected_ruby, @docs, @keys, @rereduce)
83
+ end
84
+ end
85
+
86
+ class ReduceToMatcher
87
+ include RunJS
88
+
89
+ def initialize(expected_ruby, docs, keys, rereduce = false)
90
+ @expected_ruby, @docs, @keys, @rereduce = expected_ruby, docs, keys, rereduce
91
+ end
92
+
93
+ def matches?(view_spec)
94
+ js = <<-JS
95
+ #{File.read(File.dirname(__FILE__) + '/print_r.js')}
96
+
97
+ sum = function(values) {
98
+ var rv = 0;
99
+ for (var i in values) {
100
+ rv += values[i];
101
+ }
102
+ return rv;
103
+ };
104
+
105
+ var docs = #{@docs.to_json};
106
+ var keys = #{@keys.to_json};
107
+ var reduce = #{view_spec.reduce_function};
108
+ print(print_r({result: reduce(docs, keys, #{@rereduce})}));
109
+ JS
110
+ @actual_ruby = JSON.parse(run_js(js))['result']
111
+ @expected_ruby == @actual_ruby
112
+ end
113
+
114
+ def failure_message_for_should
115
+ "Expected to reduce to #{@expected_ruby.inspect} but got #{@actual_ruby.inspect}."
116
+ end
117
+
118
+ def failure_message_for_should_not
119
+ "Expected not to reduce to #{@actual_ruby.inspect} but did."
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,60 @@
1
+ // taken and adapted from http://scriptnode.com/article/javascript-print_r-or-var_dump-equivalent/
2
+
3
+ /**
4
+ * Concatenates the values of a variable into an easily readable string
5
+ * by Matt Hackett [scriptnode.com]
6
+ * @param {Object} x The variable to debug
7
+ * @param {Number} max The maximum number of recursions allowed (keep low, around 5 for HTML elements to prevent errors) [default: 10]
8
+ * @param {String} sep The separator to use between [default: a single space ' ']
9
+ * @param {Number} l The current level deep (amount of recursion). Do not use this parameter: it's for the function's own use
10
+ */
11
+ function print_r(x, max, sep, l) {
12
+
13
+ l = l || 0;
14
+ max = max || 10;
15
+ sep = sep || ' ';
16
+
17
+ if (l > max) {
18
+ throw("Too much recursion");
19
+ };
20
+
21
+ var r = '';
22
+
23
+ if (x === null) {
24
+ r += "null";
25
+ } else if (is_array(x)) {
26
+ r += '[' + x.map(function(i) {
27
+ return print_r(i, max, sep, (l + 1));
28
+ }).join(', ') + ']';
29
+ } else if(is_object(x)) {
30
+ r += '{'
31
+ var pairs = [];
32
+ for (i in x) {
33
+ pairs.push('"' + i + '": ' + print_r(x[i], max, sep, (l + 1)));
34
+ }
35
+ r += pairs.join(', ');
36
+ r += '}';
37
+ } else if(is_string(x)) {
38
+ r += '"' + x + "\"";
39
+ } else {
40
+ r += x;
41
+ };
42
+
43
+ return r;
44
+
45
+ function is_string(a) {
46
+ return typeof a === 'string';
47
+ };
48
+
49
+ function is_array(a) {
50
+ return (a &&
51
+ typeof a === 'object' &&
52
+ a.constructor === Array);
53
+ };
54
+
55
+ function is_object(a) {
56
+ return a && typeof a == 'object'
57
+ };
58
+
59
+ };
60
+
@@ -0,0 +1,6 @@
1
+ module CouchPotato
2
+ module RSpec
3
+ class ReduceToMatcher
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,2 @@
1
+ require File.dirname(__FILE__) + '/matchers/map_to_matcher'
2
+ require File.dirname(__FILE__) + '/matchers/reduce_to_matcher'
@@ -19,7 +19,11 @@ module CouchPotato
19
19
 
20
20
  def process_results(results)
21
21
  results['rows'].map do |row|
22
- klass.json_create row['doc'] || row['value'].merge(:_id => row['id'] || row['key'])
22
+ if row['doc'].instance_of?(klass)
23
+ row['doc']
24
+ else
25
+ klass.json_create row['doc'] || row['value'].merge(:_id => row['id'] || row['key'])
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -15,12 +15,16 @@ module CouchPotato
15
15
 
16
16
  module ClassMethods
17
17
  # Declare a CouchDB view, for examples on how to use see the *ViewSpec classes in CouchPotato::View
18
- def views
19
- @views ||= {}
18
+ def views(view_name = nil)
19
+ if view_name
20
+ _find_view(view_name)
21
+ else
22
+ @views ||= {}
23
+ end
20
24
  end
21
25
 
22
26
  def execute_view(view_name, view_parameters)
23
- view_spec_class(views[view_name][:type]).new(self, view_name, views[view_name], view_parameters)
27
+ view_spec_class(views(view_name)[:type]).new(self, view_name, views(view_name), view_parameters)
24
28
  end
25
29
 
26
30
  def view(view_name, options)
@@ -38,6 +42,11 @@ module CouchPotato
38
42
  CouchPotato::View.const_get("#{name}ViewSpec")
39
43
  end
40
44
  end
45
+
46
+ def _find_view(view)
47
+ return @views[view] if @views && @views[view]
48
+ superclass._find_view(view) if superclass && superclass.respond_to?(:_find_view)
49
+ end
41
50
  end
42
51
  end
43
52
  end
@@ -34,9 +34,7 @@ module CouchPotato
34
34
  if count?
35
35
  results['rows'].first.try(:[], 'value') || 0
36
36
  else
37
- results['rows'].map do |row|
38
- klass.json_create row['doc']
39
- end
37
+ results['rows'].map { |row| row['doc'] }
40
38
  end
41
39
  end
42
40
 
@@ -23,7 +23,7 @@ module CouchPotato
23
23
 
24
24
  def create_view
25
25
  design_doc = @database.get "_design/#{@design_document_name}" rescue nil
26
- design_doc ||= {'views' => {}, "_id" => "_design/#{@design_document_name}"}
26
+ design_doc ||= {'views' => {}, "_id" => "_design/#{@design_document_name}", "language" => "javascript"}
27
27
  design_doc['views'][@view_name.to_s] = {
28
28
  'map' => @map_function,
29
29
  'reduce' => @reduce_function
data/lib/couch_potato.rb CHANGED
@@ -5,6 +5,7 @@ require 'json/add/rails'
5
5
 
6
6
  require 'ostruct'
7
7
 
8
+ JSON.create_id = 'ruby_class'
8
9
 
9
10
  module CouchPotato
10
11
  Config = Struct.new(:database_name).new
@@ -23,7 +24,7 @@ module CouchPotato
23
24
 
24
25
  def self.full_url_to_database
25
26
  raise('No Database configured. Set CouchPotato::Config.database_name') unless CouchPotato::Config.database_name
26
- if CouchPotato::Config.database_name[0,7] == 'http://'
27
+ if CouchPotato::Config.database_name.match(%r{https?://})
27
28
  CouchPotato::Config.database_name
28
29
  else
29
30
  "http://127.0.0.1:5984/#{CouchPotato::Config.database_name}"
@@ -13,7 +13,7 @@ describe CouchPotato, 'attachments' do
13
13
  comment._attachments = {'body' => {'data' => 'a useful comment', 'content_type' => 'text/plain'}}
14
14
  CouchPotato.database.save! comment
15
15
  comment_reloaded = CouchPotato.database.load comment.id
16
- comment_reloaded._attachments.should == {"body" => {"content_type" => "text/plain", "stub" => true, "length" => 16}}
16
+ comment_reloaded._attachments["body"].should include({"content_type" => "text/plain", "stub" => true, "length" => 16})
17
17
  end
18
18
 
19
19
  it "should have an empty array for a new object" do
@@ -268,4 +268,38 @@ describe "lambda callbacks" do
268
268
  recorder.run_callbacks :before_create
269
269
  recorder.lambda_works.should be_true
270
270
  end
271
+ end
272
+
273
+ describe "validation callbacks" do
274
+ class ValidatedUser
275
+ include CouchPotato::Persistence
276
+
277
+ property :name
278
+ before_validation :check_name
279
+ validates_presence_of :name
280
+
281
+ def check_name
282
+ errors.add(:name, 'should be Paul') unless name == "Paul"
283
+ end
284
+ end
285
+
286
+ it "should keep error messages set in custom before_validation filters" do
287
+ user = ValidatedUser.new(:name => "john")
288
+ user.valid?.should == false
289
+ user.errors.on(:name).should == "should be Paul"
290
+ end
291
+
292
+ it "should combine the errors from validations and callbacks" do
293
+ user = ValidatedUser.new(:name => nil)
294
+ user.valid?.should == false
295
+ user.errors.on(:name).should == ["can't be empty", "should be Paul"]
296
+ end
297
+
298
+ it "should clear the errors on subsequent calls to valid?" do
299
+ user = ValidatedUser.new(:name => nil)
300
+ user.valid?.should == false
301
+ user.name = 'Paul'
302
+ user.valid?.should == true
303
+ user.errors.on(:name).should == nil
304
+ end
271
305
  end
data/spec/create_spec.rb CHANGED
@@ -8,7 +8,7 @@ describe "create" do
8
8
  it "should store the class" do
9
9
  @comment = Comment.new :title => 'my_title'
10
10
  CouchPotato.database.save_document! @comment
11
- CouchPotato.couchrest_database.get(@comment.id)['ruby_class'].should == 'Comment'
11
+ CouchPotato.couchrest_database.get(@comment.id).ruby_class.should == 'Comment'
12
12
  end
13
13
  end
14
14
  describe "fails" do
@@ -18,6 +18,9 @@ class Build
18
18
  view :with_view_options, :group => true, :key => :time
19
19
  end
20
20
 
21
+ class CustomBuild < Build
22
+ end
23
+
21
24
  describe 'view' do
22
25
  before(:each) do
23
26
  recreate_db
@@ -108,6 +111,13 @@ describe 'view' do
108
111
  CouchPotato.couchrest_database.save_doc({:state => 'success', :time => '2008-01-01'})
109
112
  CouchPotato.database.view(Build.custom_timeline_returns_docs).map(&:state).should == ['success']
110
113
  end
114
+
115
+ it "should still return instance of class if document included 'ruby_class'" do
116
+ CouchPotato.couchrest_database.save_doc({:state => 'success', :time => '2008-01-01', :ruby_class => "Build"})
117
+ view_data = CouchPotato.database.view(Build.custom_timeline_returns_docs)
118
+ view_data.map(&:class).should == [Build]
119
+ view_data.map(&:state).should == ['success']
120
+ end
111
121
  end
112
122
 
113
123
  describe "additional reduce function given" do
@@ -146,4 +156,11 @@ describe 'view' do
146
156
  end
147
157
  end
148
158
 
159
+ describe "inherited views" do
160
+ it "should support parent views for objects of the subclass" do
161
+ CouchPotato.database.save_document CustomBuild.new(:state => 'success', :time => '2008-01-01')
162
+ CouchPotato.database.view(CustomBuild.timeline).should have(1).item
163
+ CouchPotato.database.view(CustomBuild.timeline).first.should be_kind_of(CustomBuild)
164
+ end
165
+ end
149
166
  end
@@ -1,6 +1,6 @@
1
1
  class Person
2
2
  include CouchPotato::Persistence
3
3
 
4
- property :name
5
- property :ship_address, :type => Address
4
+ property :name, :type => Address
5
+ property :ship_address
6
6
  end
@@ -18,6 +18,9 @@ class Watch
18
18
  end
19
19
  end
20
20
 
21
+ class CuckooClock < Watch
22
+ property :cuckoo
23
+ end
21
24
 
22
25
  describe 'properties' do
23
26
  before(:all) do
@@ -57,6 +60,45 @@ describe 'properties' do
57
60
  c.title.should == {'key' => 'value'}
58
61
  end
59
62
 
63
+ def it_should_persist value
64
+ c = Comment.new :title => value
65
+ CouchPotato.database.save_document! c
66
+ c = CouchPotato.database.load_document c.id
67
+ c.title.should == value
68
+ end
69
+
70
+ it "should persist a child class" do
71
+ it_should_persist Child.new('text' => 'some text')
72
+ end
73
+
74
+ it "should persist a hash with a child class" do
75
+ it_should_persist 'child' => Child.new('text' => 'some text')
76
+ end
77
+
78
+ it "should persist an array with a child class" do
79
+ it_should_persist [Child.new('text' => 'some text')]
80
+ end
81
+
82
+ it "should persist something very complex" do
83
+ something_very_complex = [
84
+ [
85
+ [
86
+ {
87
+ 'what' => [
88
+ {
89
+ 'ever' => Child.new('text' => 'some text')
90
+ }
91
+ ],
92
+ 'number' => 3
93
+ },
94
+ "string"
95
+ ],
96
+ Child.new('text' => 'nothing')
97
+ ]
98
+ ]
99
+ it_should_persist something_very_complex
100
+ end
101
+
60
102
  it "should persist a Time object" do
61
103
  w = Watch.new :time => Time.now
62
104
  CouchPotato.database.save_document! w
@@ -98,4 +140,17 @@ describe 'properties' do
98
140
  Comment.new(:title => '').title?.should be_false
99
141
  end
100
142
  end
143
+
144
+ describe "with subclasses" do
145
+ it "should include properties of superclasses" do
146
+ CuckooClock.properties.map(&:name).should include(:time)
147
+ CuckooClock.properties.map(&:name).should include(:cuckoo)
148
+ end
149
+
150
+ it "should return attributes of superclasses" do
151
+ clock = CuckooClock.new(:time => Time.now, :cuckoo => 'bavarian')
152
+ clock.attributes[:time].should_not == nil
153
+ clock.attributes[:cuckoo].should == 'bavarian'
154
+ end
155
+ end
101
156
  end
data/spec/spec_helper.rb CHANGED
@@ -7,6 +7,13 @@ require 'couch_potato'
7
7
 
8
8
  CouchPotato::Config.database_name = 'couch_potato_test'
9
9
 
10
+ class Child
11
+ include CouchPotato::Persistence
12
+
13
+ property :text
14
+ end
15
+
16
+
10
17
 
11
18
  class Comment
12
19
  include CouchPotato::Persistence
@@ -18,6 +18,32 @@ describe CouchPotato::Database, 'new' do
18
18
  end
19
19
  end
20
20
 
21
+ describe CouchPotato::Database, 'full_url_to_database' do
22
+ before(:all) do
23
+ @database_url = CouchPotato::Config.database_name
24
+ end
25
+
26
+ after(:all) do
27
+ CouchPotato::Config.database_name = @database_url
28
+ end
29
+
30
+ it "should return the full URL when it starts with https" do
31
+ CouchPotato::Config.database_name = "https://example.com/database"
32
+ CouchPotato.full_url_to_database.should == 'https://example.com/database'
33
+ end
34
+
35
+ it "should return the full URL when it starts with http" do
36
+ CouchPotato::Config.database_name = "http://example.com/database"
37
+ CouchPotato.full_url_to_database.should == 'http://example.com/database'
38
+ end
39
+
40
+ it "should use localhost when no protocol was specified" do
41
+ CouchPotato::Config.database_name = "database"
42
+ CouchPotato.full_url_to_database.should == 'http://127.0.0.1:5984/database'
43
+ end
44
+ end
45
+
46
+
21
47
  describe CouchPotato::Database, 'load' do
22
48
  it "should raise an exception if nil given" do
23
49
  db = CouchPotato::Database.new(stub('couchrest db', :info => nil))
@@ -29,22 +55,153 @@ describe CouchPotato::Database, 'load' do
29
55
  it "should set itself on the model" do
30
56
  user = mock 'user'
31
57
  DbTestUser.stub!(:new).and_return(user)
32
- db = CouchPotato::Database.new(stub('couchrest db', :info => nil, :get => {'ruby_class' => 'DbTestUser'}))
58
+ db = CouchPotato::Database.new(stub('couchrest db', :info => nil, :get => DbTestUser.json_create({'ruby_class' => 'DbTestUser'})))
33
59
  user.should_receive(:database=).with(db)
34
60
  db.load '1'
35
61
  end
36
62
 
37
63
  it "should load namespaced models" do
38
- db = CouchPotato::Database.new(stub('couchrest db', :info => nil, :get => {'ruby_class' => 'Parent::Child'}))
64
+ db = CouchPotato::Database.new(stub('couchrest db', :info => nil, :get => Parent::Child.json_create({'ruby_class' => 'Parent::Child'})))
39
65
  db.load('1').class.should == Parent::Child
40
66
  end
41
67
  end
42
68
 
43
69
  describe CouchPotato::Database, 'save_document' do
70
+ before(:each) do
71
+ @db = CouchPotato::Database.new(stub('couchrest db').as_null_object)
72
+ end
73
+
44
74
  it "should set itself on the model for a new object before doing anything else" do
45
- db = CouchPotato::Database.new(stub('couchrest db', :info => nil))
46
- user = stub('user', :new? => true, :valid? => false).as_null_object
47
- user.should_receive(:database=).with(db)
48
- db.save_document user
75
+ @db.stub(:valid_document?).and_return false
76
+ user = stub('user', :new? => true).as_null_object
77
+ user.should_receive(:database=).with(@db)
78
+ @db.save_document user
79
+ end
80
+
81
+ class Category
82
+ include CouchPotato::Persistence
83
+ property :name
84
+ validates_presence_of :name
85
+ end
86
+
87
+ it "should return false when creating a new document and the validations failed" do
88
+ CouchPotato.database.save_document(Category.new).should == false
89
+ end
90
+
91
+ it "should return false when saving an existing document and the validations failed" do
92
+ category = Category.new(:name => "pizza")
93
+ CouchPotato.database.save_document(category).should == true
94
+ category.name = nil
95
+ CouchPotato.database.save_document(category).should == false
96
+ end
97
+
98
+ describe "when creating with validate options" do
99
+ it "should not run the validations when saved with false" do
100
+ category = Category.new
101
+ @db.save_document(category, false)
102
+ category.new?.should == false
103
+ end
104
+
105
+ it "should run the validations when saved with true" do
106
+ category = Category.new
107
+ @db.save_document(category, true)
108
+ category.new?.should == true
109
+ end
110
+
111
+ it "should run the validations when saved with default" do
112
+ category = Category.new
113
+ @db.save_document(category)
114
+ category.new?.should == true
115
+ end
49
116
  end
50
- end
117
+
118
+ describe "when updating with validate options" do
119
+ it "should not run the validations when saved with false" do
120
+ category = Category.new(:name => 'food')
121
+ @db.save_document(category)
122
+ category.new?.should == false
123
+ category.name = nil
124
+ @db.save_document(category, false)
125
+ category.dirty?.should == false
126
+ end
127
+
128
+ it "should run the validations when saved with true" do
129
+ category = Category.new(:name => "food")
130
+ @db.save_document(category)
131
+ category.new?.should == false
132
+ category.name = nil
133
+ @db.save_document(category, true)
134
+ category.dirty?.should == true
135
+ category.valid?.should == false
136
+ end
137
+
138
+ it "should run the validations when saved with default" do
139
+ category = Category.new(:name => "food")
140
+ @db.save_document(category)
141
+ category.new?.should == false
142
+ category.name = nil
143
+ @db.save_document(category)
144
+ category.dirty?.should == true
145
+ end
146
+ end
147
+
148
+ describe "when saving documents with errors set in callbacks" do
149
+ class Vulcan
150
+ include CouchPotato::Persistence
151
+ before_validation_on_create :set_errors
152
+ before_validation_on_update :set_errors
153
+
154
+ property :name
155
+ validates_presence_of :name
156
+
157
+ def set_errors
158
+ errors.add(:validation, "failed")
159
+ end
160
+ end
161
+
162
+ it "should keep errors added in before_validation_on_* callbacks when creating a new object" do
163
+ spock = Vulcan.new(:name => 'spock')
164
+ @db.save_document(spock)
165
+ spock.errors.on(:validation).should == 'failed'
166
+ end
167
+
168
+ it "should keep errors added in before_validation_on_* callbacks when creating a new object" do
169
+ spock = Vulcan.new(:name => 'spock')
170
+ @db.save_document(spock, false)
171
+ spock.new_record?.should == false
172
+ spock.name = "spock's father"
173
+ @db.save_document(spock)
174
+ spock.errors.on(:validation).should == 'failed'
175
+ end
176
+
177
+ it "should keep errors generated from normal validations together with errors set in normal validations" do
178
+ spock = Vulcan.new
179
+ @db.save_document(spock)
180
+ spock.errors.on(:validation).should == 'failed'
181
+ spock.errors.on(:name).should == "can't be empty"
182
+ end
183
+
184
+ it "should clear errors on subsequent, valid saves when creating" do
185
+ spock = Vulcan.new
186
+ @db.save_document(spock)
187
+
188
+ spock.name = 'Spock'
189
+ @db.save_document(spock)
190
+ spock.errors.on(:name).should == nil
191
+ end
192
+
193
+ it "should clear errors on subsequent, valid saves when updating" do
194
+ spock = Vulcan.new(:name => 'spock')
195
+ @db.save_document(spock, false)
196
+
197
+ spock.name = nil
198
+ @db.save_document(spock)
199
+ spock.errors.on(:name).should == "can't be empty"
200
+
201
+ spock.name = 'Spock'
202
+ @db.save_document(spock)
203
+ spock.errors.on(:name).should == nil
204
+ end
205
+
206
+ end
207
+ end
@@ -86,7 +86,7 @@ describe 'dirty attribute tracking' do
86
86
 
87
87
  describe "object loaded from database" do
88
88
  before(:each) do
89
- couchrest_db = stub('database', :get => {'_id' => '1', '_rev' => '2', 'food' => 'sushi', 'ruby_class' => 'Plate'}, :info => nil)
89
+ couchrest_db = stub('database', :get => Plate.json_create({'_id' => '1', '_rev' => '2', 'food' => 'sushi', 'ruby_class' => 'Plate'}), :info => nil)
90
90
  @plate = CouchPotato::Database.new(couchrest_db).load_document '1'
91
91
  end
92
92
 
@@ -104,7 +104,7 @@ describe 'dirty attribute tracking' do
104
104
  end
105
105
 
106
106
  it "should return true if array attribute changed" do
107
- couchrest_db = stub('database', :get => {'_id' => '1', '_rev' => '2', 'food' => ['sushi'], 'ruby_class' => 'Plate'}, :info => nil)
107
+ couchrest_db = stub('database', :get => Plate.json_create({'_id' => '1', '_rev' => '2', 'food' => ['sushi'], 'ruby_class' => 'Plate'}), :info => nil)
108
108
  plate = CouchPotato::Database.new(couchrest_db).load_document '1'
109
109
  plate.food << 'burger'
110
110
  plate.should be_food_changed
@@ -119,7 +119,7 @@ describe 'dirty attribute tracking' do
119
119
 
120
120
  describe "after save" do
121
121
  it "should reset all attributes to not dirty" do
122
- couchrest_db = stub('database', :get => {'_id' => '1', '_rev' => '2', 'food' => 'sushi', 'ruby_class' => 'Plate'}, :info => nil, :save_doc => {})
122
+ couchrest_db = stub('database', :get => Plate.json_create({'_id' => '1', '_rev' => '2', 'food' => 'sushi', 'ruby_class' => 'Plate'}), :info => nil, :save_doc => {})
123
123
  db = CouchPotato::Database.new(couchrest_db)
124
124
  @plate = db.load_document '1'
125
125
  @plate.food = 'burger'
@@ -0,0 +1,14 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ class Drink
4
+ include CouchPotato::Persistence
5
+
6
+ property :alcohol
7
+ end
8
+
9
+ describe "json module" do
10
+ it "should inject JSON.create_id into hash representation of a persistence object" do
11
+ sake = Drink.new(:alcohol => "18%")
12
+ sake.to_hash[JSON.create_id].should eql("Drink")
13
+ end
14
+ end
@@ -0,0 +1,97 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+ require 'couch_potato/rspec/matchers'
3
+ require 'ostruct'
4
+
5
+ describe CouchPotato::RSpec::MapToMatcher do
6
+
7
+ describe "basic map function" do
8
+ before(:each) do
9
+ @view_spec = OpenStruct.new(:map_function => "function(doc) {emit(doc.name, doc.tags.length);}")
10
+ end
11
+
12
+ it "should pass if the given function emits the expected javascript" do
13
+ @view_spec.should map({:name => 'horst', :tags => ['person', 'male']}).to([['horst', 2]])
14
+ end
15
+
16
+ it "should not pass if the given function emits different javascript" do
17
+ @view_spec.should_not map({:name => 'horst', :tags => ['person', 'male']}).to([['horst', 3]])
18
+ end
19
+ end
20
+
21
+ describe "functions emitting multiple times" do
22
+ before(:each) do
23
+ @view_spec = OpenStruct.new(:map_function => "function(doc) {emit(doc.name, doc.tags.length); emit(doc.tags[0], doc.tags[1])};")
24
+ end
25
+
26
+ it "should pass if the given function emits the expected javascript" do
27
+ @view_spec.should map({:name => 'horst', :tags => ['person', 'male']}).to([['horst', 2], ['person', 'male']])
28
+ end
29
+
30
+ it "should return false if the given function emits different javascript" do
31
+ @view_spec.should_not map({:name => 'horst', :tags => ['person', 'male']}).to([['horst', 2], ['male', 'person']])
32
+ end
33
+ end
34
+
35
+ describe "failing specs" do
36
+ before(:each) do
37
+ @view_spec = OpenStruct.new(:map_function => "function(doc) {emit(doc.name, null)}")
38
+ end
39
+
40
+ it "should have a nice error message for failing should" do
41
+ lambda {
42
+ @view_spec.should map({:name => 'bill'}).to([['linus', nil]])
43
+ }.should raise_error('Expected to map to [["linus", nil]] but got [["bill", nil]].')
44
+ end
45
+
46
+ it "should have a nice error message for failing should not" do
47
+ lambda {
48
+ @view_spec.should_not map({:name => 'bill'}).to([['bill', nil]])
49
+ }.should raise_error('Expected not to map to [["bill", nil]] but did.')
50
+ end
51
+ end
52
+ end
53
+
54
+ describe CouchPotato::RSpec::ReduceToMatcher do
55
+ before(:each) do
56
+ @view_spec = OpenStruct.new(:reduce_function => "function(docs, keys, rereduce) {
57
+ if(rereduce) {
58
+ return(sum(keys) * 2);
59
+ } else {
60
+ return(sum(keys));
61
+ };
62
+ }")
63
+ end
64
+
65
+ it "should pass if the given function return the expected javascript" do
66
+ @view_spec.should reduce([], [1, 2, 3]).to(6)
67
+ end
68
+
69
+ it "should not pass if the given function returns different javascript" do
70
+ @view_spec.should_not reduce([], [1, 2, 3]).to(7)
71
+ end
72
+
73
+ describe "rereduce" do
74
+ it "should pass if the given function return the expected javascript" do
75
+ @view_spec.should rereduce([], [1, 2, 3]).to(12)
76
+ end
77
+
78
+ it "should not pass if the given function returns different javascript" do
79
+ @view_spec.should_not rereduce([], [1, 2, 3]).to(13)
80
+ end
81
+ end
82
+
83
+ describe 'failing specs' do
84
+
85
+ it "should have a nice error message for failing should" do
86
+ lambda {
87
+ @view_spec.should reduce([], [1, 2, 3]).to(7)
88
+ }.should raise_error('Expected to reduce to 7 but got 6.')
89
+ end
90
+
91
+ it "should have a nice error message for failing should not" do
92
+ lambda {
93
+ @view_spec.should_not reduce([], [1, 2, 3]).to(6)
94
+ }.should raise_error('Expected not to reduce to 6 but did.')
95
+ end
96
+ end
97
+ end
data/spec/update_spec.rb CHANGED
@@ -35,6 +35,6 @@ describe "create" do
35
35
  it "should update the attributes" do
36
36
  @comment.title = 'new title'
37
37
  CouchPotato.database.save_document! @comment
38
- CouchPotato.couchrest_database.get("#{@comment.id}")['title'].should == 'new title'
38
+ CouchPotato.couchrest_database.get("#{@comment.id}").title.should == 'new title'
39
39
  end
40
40
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couch_potato
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.12
4
+ version: 0.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Lang
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-09-20 00:00:00 +02:00
12
+ date: 2009-10-30 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -73,6 +73,10 @@ files:
73
73
  - lib/couch_potato/persistence/properties.rb
74
74
  - lib/couch_potato/persistence/simple_property.rb
75
75
  - lib/couch_potato/persistence/validation.rb
76
+ - lib/couch_potato/rspec/matchers.rb
77
+ - lib/couch_potato/rspec/matchers/map_to_matcher.rb
78
+ - lib/couch_potato/rspec/matchers/print_r.js
79
+ - lib/couch_potato/rspec/matchers/reduce_to_matcher.rb
76
80
  - lib/couch_potato/view/base_view_spec.rb
77
81
  - lib/couch_potato/view/custom_view_spec.rb
78
82
  - lib/couch_potato/view/custom_views.rb
@@ -99,6 +103,8 @@ files:
99
103
  - spec/unit/customs_views_spec.rb
100
104
  - spec/unit/database_spec.rb
101
105
  - spec/unit/dirty_attributes_spec.rb
106
+ - spec/unit/json_create_id_spec.rb
107
+ - spec/unit/rspec_matchers_spec.rb
102
108
  - spec/unit/string_spec.rb
103
109
  - spec/unit/view_query_spec.rb
104
110
  - spec/update_spec.rb
@@ -148,6 +154,8 @@ test_files:
148
154
  - spec/unit/customs_views_spec.rb
149
155
  - spec/unit/database_spec.rb
150
156
  - spec/unit/dirty_attributes_spec.rb
157
+ - spec/unit/json_create_id_spec.rb
158
+ - spec/unit/rspec_matchers_spec.rb
151
159
  - spec/unit/string_spec.rb
152
160
  - spec/unit/view_query_spec.rb
153
161
  - spec/update_spec.rb