norr-couchrest 0.30.4 → 0.33.01

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/README.md +58 -10
  2. data/history.txt +45 -1
  3. data/lib/couchrest.rb +9 -49
  4. data/lib/couchrest/core/adapters/restclient.rb +35 -0
  5. data/lib/couchrest/core/database.rb +18 -10
  6. data/lib/couchrest/core/document.rb +0 -4
  7. data/lib/couchrest/core/http_abstraction.rb +48 -0
  8. data/lib/couchrest/core/rest_api.rb +49 -0
  9. data/lib/couchrest/middlewares/logger.rb +263 -0
  10. data/lib/couchrest/mixins/attachments.rb +2 -2
  11. data/lib/couchrest/mixins/class_proxy.rb +5 -1
  12. data/lib/couchrest/mixins/collection.rb +23 -6
  13. data/lib/couchrest/mixins/design_doc.rb +5 -0
  14. data/lib/couchrest/mixins/document_queries.rb +33 -4
  15. data/lib/couchrest/mixins/properties.rb +30 -4
  16. data/lib/couchrest/mixins/views.rb +5 -13
  17. data/lib/couchrest/monkeypatches.rb +59 -59
  18. data/lib/couchrest/more/casted_model.rb +3 -2
  19. data/lib/couchrest/more/extended_document.rb +18 -1
  20. data/lib/couchrest/validation/validation_errors.rb +7 -0
  21. data/spec/couchrest/core/couchrest_spec.rb +2 -2
  22. data/spec/couchrest/core/database_spec.rb +19 -5
  23. data/spec/couchrest/core/design_spec.rb +1 -1
  24. data/spec/couchrest/core/document_spec.rb +1 -1
  25. data/spec/couchrest/core/server_spec.rb +1 -1
  26. data/spec/couchrest/helpers/pager_spec.rb +1 -1
  27. data/spec/couchrest/helpers/streamer_spec.rb +1 -1
  28. data/spec/couchrest/more/casted_extended_doc_spec.rb +1 -1
  29. data/spec/couchrest/more/casted_model_spec.rb +1 -1
  30. data/spec/couchrest/more/extended_doc_attachment_spec.rb +1 -1
  31. data/spec/couchrest/more/extended_doc_spec.rb +27 -2
  32. data/spec/couchrest/more/extended_doc_subclass_spec.rb +1 -1
  33. data/spec/couchrest/more/extended_doc_view_spec.rb +21 -9
  34. data/spec/couchrest/more/property_spec.rb +50 -1
  35. data/spec/spec_helper.rb +1 -1
  36. metadata +9 -2
@@ -0,0 +1,263 @@
1
+ ####################################
2
+ # USAGE
3
+ #
4
+ # in your rack.rb file
5
+ # require this file and then:
6
+ #
7
+ # couch = CouchRest.new
8
+ # LOG_DB = couch.database!('couchrest-logger')
9
+ # use CouchRest::Logger, LOG_DB
10
+ #
11
+ # Note:
12
+ # to require just this middleware, if you have the gem installed do:
13
+ # require 'couchrest/middlewares/logger'
14
+ #
15
+ # For log processing examples, see examples at the bottom of this file
16
+
17
+ module CouchRest
18
+ class Logger
19
+
20
+ def self.log
21
+ Thread.current["couchrest.logger"] ||= {:queries => []}
22
+ end
23
+
24
+ def initialize(app, db=nil)
25
+ @app = app
26
+ @db = db
27
+ end
28
+
29
+ def self.record(log_info)
30
+ log[:queries] << log_info
31
+ end
32
+
33
+ def log
34
+ Thread.current["couchrest.logger"] ||= {:queries => []}
35
+ end
36
+
37
+ def reset_log
38
+ Thread.current["couchrest.logger"] = nil
39
+ end
40
+
41
+ def call(env)
42
+ reset_log
43
+ log['started_at'] = Time.now
44
+ log['env'] = env
45
+ log['url'] = 'http://' + env['HTTP_HOST'] + env['REQUEST_URI']
46
+ response = @app.call(env)
47
+ log['ended_at'] = Time.now
48
+ log['duration'] = log['ended_at'] - log['started_at']
49
+ # let's report the log in a different thread so we don't slow down the app
50
+ @db ? Thread.new(@db, log){|db, rlog| db.save_doc(rlog);} : p(log.inspect)
51
+ response
52
+ end
53
+ end
54
+ end
55
+
56
+ # inject our logger into CouchRest HTTP abstraction layer
57
+ module HttpAbstraction
58
+
59
+ def self.get(uri, headers=nil)
60
+ start_query = Time.now
61
+ log = {:method => :get, :uri => uri, :headers => headers}
62
+ response = super(uri, headers=nil)
63
+ end_query = Time.now
64
+ log[:duration] = (end_query - start_query)
65
+ CouchRest::Logger.record(log)
66
+ response
67
+ end
68
+
69
+ def self.post(uri, payload, headers=nil)
70
+ start_query = Time.now
71
+ log = {:method => :post, :uri => uri, :payload => (payload ? (JSON.load(payload) rescue 'parsing error') : nil), :headers => headers}
72
+ response = super(uri, payload, headers=nil)
73
+ end_query = Time.now
74
+ log[:duration] = (end_query - start_query)
75
+ CouchRest::Logger.record(log)
76
+ response
77
+ end
78
+
79
+ def self.put(uri, payload, headers=nil)
80
+ start_query = Time.now
81
+ log = {:method => :put, :uri => uri, :payload => (payload ? (JSON.load(payload) rescue 'parsing error') : nil), :headers => headers}
82
+ response = super(uri, payload, headers=nil)
83
+ end_query = Time.now
84
+ log[:duration] = (end_query - start_query)
85
+ CouchRest::Logger.record(log)
86
+ response
87
+ end
88
+
89
+ def self.delete(uri, headers=nil)
90
+ start_query = Time.now
91
+ log = {:method => :delete, :uri => uri, :headers => headers}
92
+ response = super(uri, headers=nil)
93
+ end_query = Time.now
94
+ log[:duration] = (end_query - start_query)
95
+ CouchRest::Logger.record(log)
96
+ response
97
+ end
98
+
99
+ end
100
+
101
+
102
+ # Advanced usage example
103
+ #
104
+ #
105
+ # # DB VIEWS
106
+ # by_url = {
107
+ # :map =>
108
+ # "function(doc) {
109
+ # if(doc['url']){ emit(doc['url'], 1) };
110
+ # }",
111
+ # :reduce =>
112
+ # 'function (key, values, rereduce) {
113
+ # return(sum(values));
114
+ # };'
115
+ # }
116
+ # req_duration = {
117
+ # :map =>
118
+ # "function(doc) {
119
+ # if(doc['duration']){ emit(doc['url'], doc['duration']) };
120
+ # }",
121
+ # :reduce =>
122
+ # 'function (key, values, rereduce) {
123
+ # return(sum(values)/values.length);
124
+ # };'
125
+ # }
126
+ #
127
+ # query_duration = {
128
+ # :map =>
129
+ # "function(doc) {
130
+ # if(doc['queries']){
131
+ # doc.queries.forEach(function(query){
132
+ # if(query['duration'] && query['method']){
133
+ # emit(query['method'], query['duration'])
134
+ # }
135
+ # });
136
+ # };
137
+ # }" ,
138
+ # :reduce =>
139
+ # 'function (key, values, rereduce) {
140
+ # return(sum(values)/values.length);
141
+ # };'
142
+ # }
143
+ #
144
+ # action_queries = {
145
+ # :map =>
146
+ # "function(doc) {
147
+ # if(doc['queries']){
148
+ # emit(doc['url'], doc['queries'].length)
149
+ # };
150
+ # }",
151
+ # :reduce =>
152
+ # 'function (key, values, rereduce) {
153
+ # return(sum(values)/values.length);
154
+ # };'
155
+ # }
156
+ #
157
+ # action_time_spent_in_db = {
158
+ # :map =>
159
+ # "function(doc) {
160
+ # if(doc['queries']){
161
+ # var totalDuration = 0;
162
+ # doc.queries.forEach(function(query){
163
+ # totalDuration += query['duration']
164
+ # })
165
+ # emit(doc['url'], totalDuration)
166
+ # };
167
+ # }",
168
+ # :reduce =>
169
+ # 'function (key, values, rereduce) {
170
+ # return(sum(values)/values.length);
171
+ # };'
172
+ # }
173
+ #
174
+ # show_queries = %Q~function(doc, req) {
175
+ # var body = ""
176
+ # body += "<h1>" + doc['url'] + "</h1>"
177
+ # body += "<h2>Request duration in seconds: " + doc['duration'] + "</h2>"
178
+ # body += "<h3>" + doc['queries'].length + " queries</h3><ul>"
179
+ # if (doc.queries){
180
+ # doc.queries.forEach(function(query){
181
+ # body += "<li>"+ query['uri'] +"</li>"
182
+ # });
183
+ # };
184
+ # body += "</ul>"
185
+ # if(doc){ return { body: body} }
186
+ # }~
187
+ #
188
+ #
189
+ # couch = CouchRest.new
190
+ # LOG_DB = couch.database!('couchrest-logger')
191
+ # design_doc = LOG_DB.get("_design/stats") rescue nil
192
+ # LOG_DB.delete_doc design_doc rescue nil
193
+ # LOG_DB.save_doc({
194
+ # "_id" => "_design/stats",
195
+ # :views => {
196
+ # :by_url => by_url,
197
+ # :request_duration => req_duration,
198
+ # :query_duration => query_duration,
199
+ # :action_queries => action_queries,
200
+ # :action_time_spent_in_db => action_time_spent_in_db
201
+ # },
202
+ # :shows => {
203
+ # :queries => show_queries
204
+ # }
205
+ # })
206
+ #
207
+ # module CouchRest
208
+ # class Logger
209
+ #
210
+ # def self.roundup(value)
211
+ # begin
212
+ # value = Float(value)
213
+ # (value * 100).round.to_f / 100
214
+ # rescue
215
+ # value
216
+ # end
217
+ # end
218
+ #
219
+ # # Usage example:
220
+ # # CouchRest::Logger.average_request_duration(LOG_DB)['rows'].first['value']
221
+ # def self.average_request_duration(db)
222
+ # raw = db.view('stats/request_duration', :reduce => true)
223
+ # (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
224
+ # end
225
+ #
226
+ # def self.average_query_duration(db)
227
+ # raw = db.view('stats/query_duration', :reduce => true)
228
+ # (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
229
+ # end
230
+ #
231
+ # def self.average_get_query_duration(db)
232
+ # raw = db.view('stats/query_duration', :key => 'get', :reduce => true)
233
+ # (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
234
+ # end
235
+ #
236
+ # def self.average_post_query_duration(db)
237
+ # raw = db.view('stats/query_duration', :key => 'post', :reduce => true)
238
+ # (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
239
+ # end
240
+ #
241
+ # def self.average_queries_per_action(db)
242
+ # raw = db.view('stats/action_queries', :reduce => true)
243
+ # (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
244
+ # end
245
+ #
246
+ # def self.average_db_time_per_action(db)
247
+ # raw = db.view('stats/action_time_spent_in_db', :reduce => true)
248
+ # (raw.has_key?('rows') && !raw['rows'].empty?) ? roundup(raw['rows'].first['value']) : 'not available yet'
249
+ # end
250
+ #
251
+ # def self.stats(db)
252
+ # Thread.new(db){|db|
253
+ # puts "=== STATS ===\n"
254
+ # puts "average request duration: #{average_request_duration(db)}\n"
255
+ # puts "average query duration: #{average_query_duration(db)}\n"
256
+ # puts "average queries per action : #{average_queries_per_action(db)}\n"
257
+ # puts "average time spent in DB (per action): #{average_db_time_per_action(db)}\n"
258
+ # puts "===============\n"
259
+ # }
260
+ # end
261
+ #
262
+ # end
263
+ # end
@@ -19,9 +19,9 @@ module CouchRest
19
19
  end
20
20
 
21
21
  # deletes an attachment directly from couchdb
22
- def delete_attachment(name)
22
+ def delete_attachment(name, force=false)
23
23
  raise ArgumentError, "doc.database required to delete_attachment" unless database
24
- result = database.delete_attachment(self, name)
24
+ result = database.delete_attachment(self, name, force)
25
25
  self['_rev'] = result['rev']
26
26
  result['ok']
27
27
  end
@@ -59,6 +59,10 @@ module CouchRest
59
59
  @klass.all({:database => @database}.merge(opts), &block)
60
60
  end
61
61
 
62
+ def count(opts = {}, &block)
63
+ @klass.all({:database => @database, :raw => true, :limit => 0}.merge(opts), &block)['total_rows']
64
+ end
65
+
62
66
  def first(opts = {})
63
67
  @klass.first({:database => @database}.merge(opts))
64
68
  end
@@ -100,7 +104,7 @@ module CouchRest
100
104
  end
101
105
 
102
106
  def refresh_design_doc
103
- @klass.refresh_design_doc
107
+ @klass.refresh_design_doc_on(@database)
104
108
  end
105
109
 
106
110
  def save_design_doc
@@ -1,5 +1,14 @@
1
1
  module CouchRest
2
2
  module Mixins
3
+ module PaginatedResults
4
+ def amount_pages
5
+ @amount_pages ||= 0
6
+ end
7
+ def amount_pages=(value)
8
+ @amount_pages = value
9
+ end
10
+ end
11
+
3
12
  module Collection
4
13
 
5
14
  def self.included(base)
@@ -83,6 +92,8 @@ module CouchRest
83
92
 
84
93
  DEFAULT_PAGE = 1
85
94
  DEFAULT_PER_PAGE = 30
95
+
96
+ attr_accessor :amount_pages
86
97
 
87
98
  # Create a new CollectionProxy to represent the specified view. If a
88
99
  # container class is specified, the proxy will create an object of the
@@ -110,9 +121,13 @@ module CouchRest
110
121
  # See Collection.paginate
111
122
  def paginate(options = {})
112
123
  page, per_page = parse_options(options)
113
- results = @database.view(@view_name, pagination_options(page, per_page))
124
+ results = @database.view(@view_name, pagination_options(page, per_page))
125
+ @amount_pages ||= (results['total_rows'].to_f / per_page.to_f).ceil
114
126
  remember_where_we_left_off(results, page)
115
- convert_to_container_array(results)
127
+ results = convert_to_container_array(results)
128
+ results.extend(PaginatedResults)
129
+ results.amount_pages = @amount_pages
130
+ results
116
131
  end
117
132
 
118
133
  # See Collection.paginated_each
@@ -178,7 +193,7 @@ module CouchRest
178
193
  @target.inspect
179
194
  end
180
195
 
181
- def convert_to_container_array(results)
196
+ def convert_to_container_array(results)
182
197
  if @container_class.nil?
183
198
  results
184
199
  else
@@ -209,12 +224,14 @@ module CouchRest
209
224
 
210
225
  def remember_where_we_left_off(results, page)
211
226
  last_row = results['rows'].last
212
- @last_key = last_row['key']
213
- @last_docid = last_row['id']
227
+ if last_row
228
+ @last_key = last_row['key']
229
+ @last_docid = last_row['id']
230
+ end
214
231
  @last_page = page
215
232
  end
216
233
  end
217
234
 
218
235
  end
219
236
  end
220
- end
237
+ end
@@ -45,6 +45,11 @@ module CouchRest
45
45
  save_design_doc
46
46
  end
47
47
 
48
+ def refresh_design_doc_on(db)
49
+ reset_design_doc
50
+ save_design_doc_on(db)
51
+ end
52
+
48
53
  # Save the design doc onto the default database, and update the
49
54
  # design_doc attribute
50
55
  def save_design_doc
@@ -12,16 +12,14 @@ module CouchRest
12
12
  # name of the current class. Take the standard set of
13
13
  # CouchRest::Database#view options.
14
14
  def all(opts = {}, &block)
15
- view(:all, {:reduce => false}.merge(opts), &block)
15
+ view(:all, opts, &block)
16
16
  end
17
17
 
18
18
  # Returns the number of documents that have the "couchrest-type" field
19
19
  # equal to the name of the current class. Takes the standard set of
20
20
  # CouchRest::Database#view options
21
21
  def count(opts = {}, &block)
22
- result = all({:reduce => true}.merge(opts), &block)['rows']
23
- return 0 if result.empty?
24
- result.first['value']
22
+ all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
25
23
  end
26
24
 
27
25
  # Load the first document that have the "couchrest-type" field equal to
@@ -41,7 +39,38 @@ module CouchRest
41
39
  end
42
40
 
43
41
  # Load a document from the database by id
42
+ # No exceptions will be raised if the document isn't found
43
+ #
44
+ # ==== Returns
45
+ # Object:: if the document was found
46
+ # or
47
+ # Nil::
48
+ #
49
+ # === Parameters
50
+ # id<String, Integer>:: Document ID
51
+ # db<Database>:: optional option to pass a custom database to use
44
52
  def get(id, db = database)
53
+ begin
54
+ doc = db.get id
55
+ rescue
56
+ nil
57
+ else
58
+ new(doc)
59
+ end
60
+ end
61
+
62
+ # Load a document from the database by id
63
+ # An exception will be raised if the document isn't found
64
+ #
65
+ # ==== Returns
66
+ # Object:: if the document was found
67
+ # or
68
+ # Exception
69
+ #
70
+ # === Parameters
71
+ # id<String, Integer>:: Document ID
72
+ # db<Database>:: optional option to pass a custom database to use
73
+ def get!(id, db = database)
45
74
  doc = db.get id
46
75
  new(doc)
47
76
  end
@@ -56,7 +56,6 @@ module CouchRest
56
56
  def cast_keys
57
57
  return unless self.class.properties
58
58
  self.class.properties.each do |property|
59
-
60
59
  next unless property.casted
61
60
  key = self.has_key?(property.name) ? property.name : property.name.to_sym
62
61
  # Don't cast the property unless it has a value
@@ -73,9 +72,15 @@ module CouchRest
73
72
  else
74
73
  # Auto parse Time objects
75
74
  self[property.name] = if ((property.init_method == 'new') && target == 'Time')
76
- # Using custom time parsing method because Ruby's default method is toooo slow
77
- #self[key].is_a?(String) ? Time.mktime_with_offset(self[key].dup) : self[key]
78
- self[key].is_a?(String) ? Time.parse(self[key].dup) : self[key]
75
+ # Using custom time parsing method because Ruby's default method is toooo slow
76
+ self[key].is_a?(String) ? Time.mktime_with_offset(self[key].dup) : self[key]
77
+ #self[key].is_a?(String) ? Time.parse(self[key].dup) : self[key]
78
+ # Float instances don't get initialized with #new
79
+ elsif ((property.init_method == 'new') && target == 'Float')
80
+ cast_float(self[key])
81
+ # 'boolean' type is simply used to generate a property? accessor method
82
+ elsif ((property.init_method == 'new') && target == 'boolean')
83
+ self[key]
79
84
  else
80
85
  # Let people use :send as a Time parse arg
81
86
  klass = ::CouchRest.constantize(target)
@@ -85,6 +90,15 @@ module CouchRest
85
90
  end
86
91
 
87
92
  end
93
+
94
+ def cast_float(value)
95
+ begin
96
+ Float(value)
97
+ rescue
98
+ value
99
+ end
100
+ end
101
+
88
102
  end
89
103
 
90
104
  module ClassMethods
@@ -118,6 +132,18 @@ module CouchRest
118
132
  end
119
133
  EOS
120
134
 
135
+ if property.type == 'boolean'
136
+ class_eval <<-EOS, __FILE__, __LINE__
137
+ def #{property.name}?
138
+ if self['#{property.name}'].nil? || self['#{property.name}'] == false || self['#{property.name}'].to_s.downcase == 'false'
139
+ false
140
+ else
141
+ true
142
+ end
143
+ end
144
+ EOS
145
+ end
146
+
121
147
  if property.alias
122
148
  class_eval <<-EOS, __FILE__, __LINE__
123
149
  alias #{property.alias.to_sym} #{property.name.to_sym}