couch_potato 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/CHANGES.md +15 -0
  2. data/MIT-LICENSE.txt +19 -0
  3. data/README.md +295 -0
  4. data/VERSION.yml +4 -0
  5. data/init.rb +3 -0
  6. data/lib/core_ext/date.rb +10 -0
  7. data/lib/core_ext/object.rb +5 -0
  8. data/lib/core_ext/string.rb +19 -0
  9. data/lib/core_ext/symbol.rb +15 -0
  10. data/lib/core_ext/time.rb +11 -0
  11. data/lib/couch_potato.rb +40 -0
  12. data/lib/couch_potato/database.rb +106 -0
  13. data/lib/couch_potato/persistence.rb +98 -0
  14. data/lib/couch_potato/persistence/attachments.rb +31 -0
  15. data/lib/couch_potato/persistence/callbacks.rb +60 -0
  16. data/lib/couch_potato/persistence/dirty_attributes.rb +49 -0
  17. data/lib/couch_potato/persistence/ghost_attributes.rb +22 -0
  18. data/lib/couch_potato/persistence/json.rb +46 -0
  19. data/lib/couch_potato/persistence/magic_timestamps.rb +13 -0
  20. data/lib/couch_potato/persistence/properties.rb +52 -0
  21. data/lib/couch_potato/persistence/simple_property.rb +83 -0
  22. data/lib/couch_potato/persistence/validation.rb +18 -0
  23. data/lib/couch_potato/view/base_view_spec.rb +24 -0
  24. data/lib/couch_potato/view/custom_view_spec.rb +27 -0
  25. data/lib/couch_potato/view/custom_views.rb +44 -0
  26. data/lib/couch_potato/view/model_view_spec.rb +63 -0
  27. data/lib/couch_potato/view/properties_view_spec.rb +39 -0
  28. data/lib/couch_potato/view/raw_view_spec.rb +25 -0
  29. data/lib/couch_potato/view/view_query.rb +44 -0
  30. data/rails/init.rb +7 -0
  31. data/spec/attachments_spec.rb +23 -0
  32. data/spec/callbacks_spec.rb +271 -0
  33. data/spec/create_spec.rb +22 -0
  34. data/spec/custom_view_spec.rb +149 -0
  35. data/spec/default_property_spec.rb +34 -0
  36. data/spec/destroy_spec.rb +29 -0
  37. data/spec/fixtures/address.rb +9 -0
  38. data/spec/fixtures/person.rb +6 -0
  39. data/spec/property_spec.rb +101 -0
  40. data/spec/spec.opts +4 -0
  41. data/spec/spec_helper.rb +29 -0
  42. data/spec/unit/attributes_spec.rb +48 -0
  43. data/spec/unit/callbacks_spec.rb +33 -0
  44. data/spec/unit/couch_potato_spec.rb +20 -0
  45. data/spec/unit/create_spec.rb +58 -0
  46. data/spec/unit/customs_views_spec.rb +15 -0
  47. data/spec/unit/database_spec.rb +50 -0
  48. data/spec/unit/dirty_attributes_spec.rb +131 -0
  49. data/spec/unit/string_spec.rb +13 -0
  50. data/spec/unit/view_query_spec.rb +9 -0
  51. data/spec/update_spec.rb +40 -0
  52. metadata +153 -0
@@ -0,0 +1,83 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ class SimpleProperty #:nodoc:
4
+ attr_accessor :name, :type
5
+
6
+ def initialize(owner_clazz, name, options = {})
7
+ self.name = name
8
+ self.type = options[:type]
9
+
10
+ define_accessors accessors_module_for(owner_clazz), name, options
11
+ end
12
+
13
+ def build(object, json)
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
20
+ object.send "#{name}=", typecast_value
21
+ end
22
+
23
+ def dirty?(object)
24
+ object.send("#{name}_changed?")
25
+ end
26
+
27
+ def save(object)
28
+
29
+ end
30
+
31
+ def destroy(object)
32
+
33
+ end
34
+
35
+ def serialize(json, object)
36
+ json[name] = object.send name
37
+ end
38
+
39
+ private
40
+
41
+ def accessors_module_for(clazz)
42
+ unless clazz.const_defined?('AccessorMethods')
43
+ accessors_module = clazz.const_set('AccessorMethods', Module.new)
44
+ clazz.send(:include, accessors_module)
45
+ end
46
+ clazz.const_get('AccessorMethods')
47
+ end
48
+
49
+ def define_accessors(base, name, options)
50
+ base.class_eval do
51
+ attr_reader "#{name}_was"
52
+
53
+ define_method "#{name}" do
54
+ value = self.instance_variable_get("@#{name}")
55
+ if value.blank? && options[:default]
56
+ default = clone_attribute(options[:default])
57
+ self.instance_variable_set("@#{name}", default)
58
+ default
59
+ else
60
+ value
61
+ end
62
+ end
63
+
64
+ define_method "#{name}=" do |value|
65
+ self.instance_variable_set("@#{name}", value)
66
+ end
67
+
68
+ define_method "#{name}?" do
69
+ !self.send(name).nil? && !self.send(name).try(:blank?)
70
+ end
71
+
72
+ define_method "#{name}_changed?" do
73
+ !self.instance_variable_get("@#{name}_not_changed") && self.send(name) != self.send("#{name}_was")
74
+ end
75
+
76
+ define_method "#{name}_not_changed" do
77
+ self.instance_variable_set("@#{name}_not_changed", true)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,18 @@
1
+ require 'validatable'
2
+
3
+ module CouchPotato
4
+ module Persistence
5
+ module Validation
6
+ def self.included(base)
7
+ base.send :include, Validatable
8
+ base.class_eval do
9
+ # Override the validate method to first run before_validation callback
10
+ def valid?
11
+ self.run_callbacks :before_validation
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module CouchPotato
2
+ module View
3
+ class BaseViewSpec
4
+ attr_reader :reduce_function, :design_document, :view_name, :view_parameters, :klass, :options
5
+ private :klass, :options
6
+
7
+ def initialize(klass, view_name, options, view_parameters)
8
+ @klass = klass
9
+ @design_document = klass.to_s.underscore
10
+ @view_name = view_name
11
+ @options = options
12
+ @view_parameters = {}
13
+ [:group, :include_docs, :descending, :group_level, :limit].each do |key|
14
+ @view_parameters[key] = options[key] if options.include?(key)
15
+ end
16
+ @view_parameters.merge!(view_parameters)
17
+ end
18
+
19
+ def process_results(results)
20
+ results
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module CouchPotato
2
+ module View
3
+ # a view for custom map/reduce functions that still returns model instances
4
+ #
5
+ # example:
6
+ # view :my_custom_view, :map => "function(doc) { emit(doc._id, null); }", :include_docs => true, :type => :custom, :reduce => nil
7
+ class CustomViewSpec < BaseViewSpec
8
+ def map_function
9
+ options[:map]
10
+ end
11
+
12
+ def reduce_function
13
+ options[:reduce]
14
+ end
15
+
16
+ def view_parameters
17
+ {:include_docs => options[:include_docs] || false}.merge(super)
18
+ end
19
+
20
+ def process_results(results)
21
+ results['rows'].map do |row|
22
+ klass.json_create row['doc'] || row['value'].merge(:_id => row['id'] || row['key'])
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ require File.dirname(__FILE__) + '/base_view_spec'
2
+ require File.dirname(__FILE__) + '/model_view_spec'
3
+ require File.dirname(__FILE__) + '/properties_view_spec'
4
+ require File.dirname(__FILE__) + '/custom_view_spec'
5
+ require File.dirname(__FILE__) + '/raw_view_spec'
6
+
7
+
8
+ module CouchPotato
9
+ module View
10
+ module CustomViews
11
+
12
+ def self.included(base)
13
+ base.extend ClassMethods
14
+ end
15
+
16
+ module ClassMethods
17
+ # Declare a CouchDB view, for examples on how to use see the *ViewSpec classes in CouchPotato::View
18
+ def views
19
+ @views ||= {}
20
+ end
21
+
22
+ def execute_view(view_name, view_parameters)
23
+ view_spec_class(views[view_name][:type]).new(self, view_name, views[view_name], view_parameters)
24
+ end
25
+
26
+ def view(view_name, options)
27
+ view_name = view_name.to_s
28
+ views[view_name] = options
29
+ method_str = "def #{view_name}(view_parameters = {}); execute_view(\"#{view_name}\", view_parameters); end"
30
+ self.instance_eval(method_str)
31
+ end
32
+
33
+ def view_spec_class(type)
34
+ if type && type.is_a?(Class)
35
+ type
36
+ else
37
+ name = type.nil? ? 'Model' : type.to_s.camelize
38
+ CouchPotato::View.const_get("#{name}ViewSpec")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ module CouchPotato
2
+ module View
3
+ # A view to return model instances by searching its properties.
4
+ # If you pass reduce => true will count instead
5
+ #
6
+ # example:
7
+ # view :my_view, :key => :name
8
+ class ModelViewSpec < BaseViewSpec
9
+
10
+ def view_parameters
11
+ _super = super
12
+ if _super[:reduce]
13
+ _super
14
+ else
15
+ {:include_docs => true, :reduce => false}.merge(_super)
16
+ end
17
+ end
18
+
19
+ def map_function
20
+ "function(doc) {
21
+ if(doc.ruby_class && doc.ruby_class == '#{@klass.name}') {
22
+ emit(#{formatted_key(key)}, null);
23
+ }
24
+ }"
25
+ end
26
+
27
+ def reduce_function
28
+ "function(key, values) {
29
+ return values.length;
30
+ }"
31
+ end
32
+
33
+ def process_results(results)
34
+ if count?
35
+ results['rows'].first.try(:[], 'value') || 0
36
+ else
37
+ results['rows'].map do |row|
38
+ klass.json_create row['doc']
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def count?
46
+ view_parameters[:reduce]
47
+ end
48
+
49
+ def key
50
+ options[:key]
51
+ end
52
+
53
+ def formatted_key(key)
54
+ if key.is_a? Array
55
+ '[' + key.map{|attribute| formatted_key(attribute)}.join(', ') + ']'
56
+ else
57
+ "doc['#{key}']"
58
+ end
59
+ end
60
+
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,39 @@
1
+ module CouchPotato
2
+ module View
3
+ # A view to return model instances with only some properties poulated by searching its properties, e.g. for very large documents where you are only interested in some of their data
4
+ #
5
+ # example:
6
+ # view :my_view, :key => :name, :properties => [:name, :author], :type => :properties
7
+ class PropertiesViewSpec < ModelViewSpec
8
+ def map_function
9
+ "function(doc) {
10
+ if(doc.ruby_class && doc.ruby_class == '#{@klass.name}') {
11
+ emit(#{formatted_key(key)}, #{properties_for_map(properties)});
12
+ }
13
+ }"
14
+ end
15
+
16
+ def process_results(results)
17
+ results['rows'].map do |row|
18
+ klass.json_create row['value'].merge(:_id => row['id'])
19
+ end
20
+ end
21
+
22
+ def view_parameters
23
+ {:include_docs => false}.merge(super)
24
+ end
25
+
26
+ private
27
+
28
+ def properties
29
+ options[:properties]
30
+ end
31
+
32
+ def properties_for_map(properties)
33
+ '{' + properties.map{|p| "#{p}: doc.#{p}"}.join(', ') + '}'
34
+ end
35
+
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ module CouchPotato
2
+ module View
3
+ # A view for custom map/reduce functions that returns the raw data fromcouchdb
4
+ #
5
+ # example:
6
+ # view :my_custom_view, :map => "function(doc) { emit(doc._id, null); }", :type => :raw, :reduce => nil
7
+ # optionally you can pass in a results filter which you can use to process the raw couchdb results before returning them
8
+ #
9
+ # example:
10
+ # view :my_custom_view, :map => "function(doc) { emit(doc._id, null); }", :type => :raw, :results_filter => lambda{|results| results['rows].map{|row| row['value']}}
11
+ class RawViewSpec < BaseViewSpec
12
+ def map_function
13
+ options[:map]
14
+ end
15
+
16
+ def process_results(results)
17
+ options[:results_filter] ? options[:results_filter].call(results) : results
18
+ end
19
+
20
+ def reduce_function
21
+ options[:reduce]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ module CouchPotato
2
+ module View
3
+ # Used to query views (and create them if they don't exist). Usually you won't have to use this class directly. Instead it is used internally by the CouchPotato::Database.view method.
4
+ class ViewQuery
5
+ def initialize(couchrest_database, design_document_name, view_name, map_function, reduce_function = nil)
6
+ @database = couchrest_database
7
+ @design_document_name = design_document_name
8
+ @view_name = view_name
9
+ @map_function = map_function
10
+ @reduce_function = reduce_function
11
+ end
12
+
13
+ def query_view!(parameters = {})
14
+ begin
15
+ query_view parameters
16
+ rescue RestClient::ResourceNotFound# => e
17
+ create_view
18
+ retry
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def create_view
25
+ design_doc = @database.get "_design/#{@design_document_name}" rescue nil
26
+ design_doc ||= {'views' => {}, "_id" => "_design/#{@design_document_name}"}
27
+ design_doc['views'][@view_name.to_s] = {
28
+ 'map' => @map_function,
29
+ 'reduce' => @reduce_function
30
+ }
31
+ @database.save_doc(design_doc)
32
+ end
33
+
34
+ def query_view(parameters)
35
+ @database.view view_url, parameters
36
+ end
37
+
38
+ def view_url
39
+ "#{@design_document_name}/#{@view_name}"
40
+ end
41
+
42
+ end
43
+ end
44
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,7 @@
1
+ # this is for rails only
2
+
3
+ require File.dirname(__FILE__) + '/../lib/couch_potato'
4
+
5
+ CouchPotato::Config.database_name = YAML::load(File.read(Rails.root.to_s + '/config/couchdb.yml'))[RAILS_ENV]
6
+
7
+ RAILS_DEFAULT_LOGGER.info "** couch_potato: initialized from #{__FILE__}"
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe CouchPotato, 'attachments' do
4
+ it "should persist an attachment" do
5
+ comment = Comment.new :title => 'nil'
6
+ comment._attachments = {'body' => {'data' => 'a useful comment', 'content_type' => 'text/plain'}}
7
+ CouchPotato.database.save! comment
8
+ CouchPotato.couchrest_database.fetch_attachment(comment.to_hash, 'body').to_s.should == 'a useful comment'
9
+ end
10
+
11
+ it "should give me information about the attachments of a document" do
12
+ comment = Comment.new :title => 'nil'
13
+ comment._attachments = {'body' => {'data' => 'a useful comment', 'content_type' => 'text/plain'}}
14
+ CouchPotato.database.save! comment
15
+ comment_reloaded = CouchPotato.database.load comment.id
16
+ comment_reloaded._attachments.should == {"body" => {"content_type" => "text/plain", "stub" => true, "length" => 16}}
17
+ end
18
+
19
+ it "should have an empty array for a new object" do
20
+ Comment.new._attachments.should == {}
21
+ end
22
+
23
+ end
@@ -0,0 +1,271 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class CallbackRecorder
4
+ include CouchPotato::Persistence
5
+
6
+ property :required_property
7
+
8
+ validates_presence_of :required_property
9
+
10
+ [:before_validation, :before_validation_on_create,
11
+ :before_validation_on_save, :before_validation_on_update,
12
+ :before_save, :before_create, :before_create,
13
+ :after_save, :after_create, :after_create,
14
+ :before_update, :after_update,
15
+ :before_destroy, :after_destroy
16
+ ].each do |callback|
17
+ define_method callback do
18
+ callbacks << callback
19
+ end
20
+ self.send callback, callback
21
+ end
22
+
23
+ view :all, :key => :required_property
24
+
25
+ attr_accessor :lambda_works
26
+ before_create lambda {|model| model.lambda_works = true }
27
+
28
+ def callbacks
29
+ @callbacks ||= []
30
+ end
31
+
32
+ private
33
+
34
+ def method_callback_with_argument(db)
35
+ db.view CallbackRecorder.all
36
+ end
37
+
38
+ end
39
+
40
+ describe "multiple callbacks at once" do
41
+
42
+ class Monkey
43
+ include CouchPotato::Persistence
44
+ attr_accessor :eaten_banana, :eaten_apple
45
+
46
+ before_create :eat_apple, :eat_banana
47
+
48
+ private
49
+
50
+ def eat_banana
51
+ self.eaten_banana = true
52
+ end
53
+
54
+ def eat_apple
55
+ self.eaten_apple = true
56
+ end
57
+ end
58
+ it "should run all callback methods given to the callback method call" do
59
+ monkey = Monkey.new
60
+ CouchPotato.database.save_document! monkey
61
+ monkey.eaten_banana.should be_true
62
+ monkey.eaten_apple.should be_true
63
+ end
64
+ end
65
+
66
+ describe 'create callbacks' do
67
+
68
+ before(:each) do
69
+ @recorder = CallbackRecorder.new
70
+ couchrest_database = stub 'couchrest_database', :save_doc => {'id' => '1', 'rev' => '2'}, :view => {'rows' => []}, :info => nil
71
+ @db = CouchPotato::Database.new(couchrest_database)
72
+ end
73
+
74
+ describe "successful create" do
75
+ before(:each) do
76
+ @recorder.required_property = 1
77
+ end
78
+
79
+ it "should call before_validation" do
80
+ @recorder.valid?
81
+ @recorder.callbacks.should include(:before_validation)
82
+ end
83
+
84
+ it "should call before_validation_on_create" do
85
+ @db.save_document! @recorder
86
+ @recorder.callbacks.should include(:before_validation_on_create)
87
+ end
88
+
89
+ it "should call before_validation_on_save" do
90
+ @db.save_document! @recorder
91
+ @recorder.callbacks.should include(:before_validation_on_save)
92
+ end
93
+
94
+ it "should call before_save" do
95
+ @db.save_document! @recorder
96
+ @recorder.callbacks.should include(:before_save)
97
+ end
98
+
99
+ it "should call after_save" do
100
+ @db.save_document! @recorder
101
+ @recorder.callbacks.should include(:after_save)
102
+ end
103
+
104
+ it "should call before_create" do
105
+ @db.save_document! @recorder
106
+ @recorder.callbacks.should include(:before_create)
107
+ end
108
+
109
+ it "should call after_create" do
110
+ @db.save_document! @recorder
111
+ @recorder.callbacks.should include(:after_create)
112
+ end
113
+
114
+ end
115
+
116
+ describe "failed create" do
117
+
118
+ it "should call before_validation" do
119
+ @recorder.valid?
120
+ @recorder.callbacks.should include(:before_validation)
121
+ end
122
+
123
+ it "should call before_validation_on_create" do
124
+ @db.save_document @recorder
125
+ @recorder.callbacks.should include(:before_validation_on_create)
126
+ end
127
+
128
+ it "should call before_validation_on_save" do
129
+ @db.save_document @recorder
130
+ @recorder.callbacks.should include(:before_validation_on_save)
131
+ end
132
+
133
+ it "should not call before_save" do
134
+ @db.save_document @recorder
135
+ @recorder.callbacks.should_not include(:before_save)
136
+ end
137
+
138
+ it "should not call after_save" do
139
+ @db.save_document @recorder
140
+ @recorder.callbacks.should_not include(:after_save)
141
+ end
142
+
143
+ it "should not call before_create" do
144
+ @db.save_document @recorder
145
+ @recorder.callbacks.should_not include(:before_create)
146
+ end
147
+
148
+ it "should not call after_create" do
149
+ @db.save_document @recorder
150
+ @recorder.callbacks.should_not include(:after_create)
151
+ end
152
+ end
153
+ end
154
+
155
+ describe "update callbacks" do
156
+
157
+ before(:each) do
158
+ @recorder = CallbackRecorder.new :required_property => 1
159
+
160
+ couchrest_database = stub 'couchrest_database', :save_doc => {'id' => '1', 'rev' => '2'}, :view => {'rows' => []}, :info => nil
161
+ @db = CouchPotato::Database.new(couchrest_database)
162
+ @db.save_document! @recorder
163
+
164
+ @recorder.required_property = 2
165
+ @recorder.callbacks.clear
166
+ end
167
+
168
+ describe "successful update" do
169
+
170
+ before(:each) do
171
+ @db.save_document! @recorder
172
+ end
173
+
174
+ it "should call before_validation" do
175
+ @recorder.callbacks.should include(:before_validation)
176
+ end
177
+
178
+ it "should call before_validation_on_update" do
179
+ @recorder.callbacks.should include(:before_validation_on_update)
180
+ end
181
+
182
+ it "should call before_validation_on_save" do
183
+ @recorder.callbacks.should include(:before_validation_on_save)
184
+ end
185
+
186
+ it "should call before_save" do
187
+ @recorder.callbacks.should include(:before_save)
188
+ end
189
+
190
+ it "should call after_save" do
191
+ @recorder.callbacks.should include(:after_save)
192
+ end
193
+
194
+ it "should call before_update" do
195
+ @recorder.callbacks.should include(:before_update)
196
+ end
197
+
198
+ it "should call after_update" do
199
+ @recorder.callbacks.should include(:after_update)
200
+ end
201
+
202
+ end
203
+
204
+ describe "failed update" do
205
+
206
+ before(:each) do
207
+ @recorder.required_property = nil
208
+ @db.save_document @recorder
209
+ end
210
+
211
+ it "should call before_validation" do
212
+ @recorder.callbacks.should include(:before_validation)
213
+ end
214
+
215
+ it "should call before_validation_on_update" do
216
+ @recorder.callbacks.should include(:before_validation_on_update)
217
+ end
218
+
219
+ it "should call before_validation_on_save" do
220
+ @recorder.callbacks.should include(:before_validation_on_save)
221
+ end
222
+
223
+ it "should not call before_save" do
224
+ @recorder.callbacks.should_not include(:before_save)
225
+ end
226
+
227
+ it "should not call after_save" do
228
+ @recorder.callbacks.should_not include(:after_save)
229
+ end
230
+
231
+ it "should not call before_update" do
232
+ @recorder.callbacks.should_not include(:before_update)
233
+ end
234
+
235
+ it "should not call after_update" do
236
+ @recorder.callbacks.should_not include(:after_update)
237
+ end
238
+
239
+ end
240
+
241
+ end
242
+
243
+ describe "destroy callbacks" do
244
+
245
+ before(:each) do
246
+ @recorder = CallbackRecorder.new :required_property => 1
247
+ couchrest_database = stub 'couchrest_database', :save_doc => {'id' => '1', 'rev' => '2'}, :delete_doc => nil, :view => {'rows' => []}, :info => nil
248
+ @db = CouchPotato::Database.new(couchrest_database)
249
+ @db.save_document! @recorder
250
+
251
+ @recorder.callbacks.clear
252
+ end
253
+
254
+ it "should call before_destroy" do
255
+ @db.destroy_document @recorder
256
+ @recorder.callbacks.should include(:before_destroy)
257
+ end
258
+
259
+ it "should call after_destroy" do
260
+ @db.destroy_document @recorder
261
+ @recorder.callbacks.should include(:after_destroy)
262
+ end
263
+ end
264
+
265
+ describe "lambda callbacks" do
266
+ it "should run the lambda" do
267
+ recorder = CallbackRecorder.new
268
+ recorder.run_callbacks :before_create
269
+ recorder.lambda_works.should be_true
270
+ end
271
+ end