couch_potato 0.2.12 → 0.2.13

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