couchrest 0.28 → 0.30

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/README.md CHANGED
@@ -93,3 +93,13 @@ you can define some casting rules.
93
93
 
94
94
  If you want to cast an array of instances from a specific Class, use the trick shown above ["ClassName"]
95
95
 
96
+ ### Pagination
97
+
98
+ Pagination is available in any ExtendedDocument classes. Here are some usage examples:
99
+
100
+ articles = Article.by_date :key => Date.today
101
+ articles.paginate(:page => 1, :per_page => 3)
102
+
103
+
104
+ Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
105
+ :per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true)
data/Rakefile CHANGED
@@ -24,7 +24,7 @@ spec = Gem::Specification.new do |s|
24
24
  s.description = "CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments."
25
25
  s.has_rdoc = true
26
26
  s.authors = ["J. Chris Anderson", "Matt Aimonetti"]
27
- s.files = %w( LICENSE README.md Rakefile THANKS.md ) +
27
+ s.files = %w( LICENSE README.md Rakefile THANKS.md history.txt) +
28
28
  Dir["{examples,lib,spec,utils}/**/*"] -
29
29
  Dir["spec/tmp"]
30
30
  s.extra_rdoc_files = %w( README.md LICENSE THANKS.md )
data/history.txt ADDED
@@ -0,0 +1,19 @@
1
+ == 0.30
2
+
3
+ * Major enhancements
4
+
5
+ * Added support for pagination (John Wood)
6
+ * Improved performance when initializing documents with timestamps (Matt Aimonetti)
7
+
8
+ * Minor enhancements
9
+
10
+ * Extended the API to retrieve an attachment URI (Matt Aimonetti)
11
+ * Bug fix: default value should be able to be set as false (Alexander Uvarov)
12
+ * Bug fix: validates_is_numeric should be able to properly validate a Float instance (Rob Kaufman)
13
+ * Bug fix: fixed the Timeout implementation (Seth Falcon)
14
+
15
+
16
+ ---
17
+
18
+ Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest.
19
+ You can see the full commit history on GitHub: http://github.com/mattetti/couchrest/commits/master/
data/lib/couchrest.rb CHANGED
@@ -28,7 +28,7 @@ require 'couchrest/monkeypatches'
28
28
 
29
29
  # = CouchDB, close to the metal
30
30
  module CouchRest
31
- VERSION = '0.28' unless self.const_defined?("VERSION")
31
+ VERSION = '0.30' unless self.const_defined?("VERSION")
32
32
 
33
33
  autoload :Server, 'couchrest/core/server'
34
34
  autoload :Database, 'couchrest/core/database'
@@ -17,7 +17,8 @@ module CouchRest
17
17
  @name = name
18
18
  @server = server
19
19
  @host = server.uri
20
- @uri = @root = "#{host}/#{name.gsub('/','%2F')}"
20
+ @uri = "/#{name.gsub('/','%2F')}"
21
+ @root = host + uri
21
22
  @streamer = Streamer.new(self)
22
23
  @bulk_save_cache = []
23
24
  @bulk_save_cache_limit = 500 # must be smaller than the uuid count
@@ -25,18 +26,18 @@ module CouchRest
25
26
 
26
27
  # returns the database's uri
27
28
  def to_s
28
- @uri
29
+ @root
29
30
  end
30
31
 
31
32
  # GET the database info from CouchDB
32
33
  def info
33
- CouchRest.get @uri
34
+ CouchRest.get @root
34
35
  end
35
36
 
36
37
  # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
37
38
  def documents(params = {})
38
39
  keys = params.delete(:keys)
39
- url = CouchRest.paramify_url "#{@uri}/_all_docs", params
40
+ url = CouchRest.paramify_url "#{@root}/_all_docs", params
40
41
  if keys
41
42
  CouchRest.post(url, {:keys => keys})
42
43
  else
@@ -56,7 +57,7 @@ module CouchRest
56
57
  def slow_view(funcs, params = {})
57
58
  keys = params.delete(:keys)
58
59
  funcs = funcs.merge({:keys => keys}) if keys
59
- url = CouchRest.paramify_url "#{@uri}/_temp_view", params
60
+ url = CouchRest.paramify_url "#{@root}/_temp_view", params
60
61
  JSON.parse(RestClient.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
61
62
  end
62
63
 
@@ -70,7 +71,7 @@ module CouchRest
70
71
  name = name.split('/') # I think this will always be length == 2, but maybe not...
71
72
  dname = name.shift
72
73
  vname = name.join('/')
73
- url = CouchRest.paramify_url "#{@uri}/_design/#{dname}/_view/#{vname}", params
74
+ url = CouchRest.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
74
75
  if keys
75
76
  CouchRest.post(url, {:keys => keys})
76
77
  else
@@ -85,7 +86,7 @@ module CouchRest
85
86
  # GET a document from CouchDB, by id. Returns a Ruby Hash.
86
87
  def get(id, params = {})
87
88
  slug = escape_docid(id)
88
- url = CouchRest.paramify_url("#{@uri}/#{slug}", params)
89
+ url = CouchRest.paramify_url("#{@root}/#{slug}", params)
89
90
  result = CouchRest.get(url)
90
91
  return result unless result.is_a?(Hash)
91
92
  doc = if /^_design/ =~ result["_id"]
@@ -101,7 +102,7 @@ module CouchRest
101
102
  def fetch_attachment(doc, name)
102
103
  # slug = escape_docid(docid)
103
104
  # name = CGI.escape(name)
104
- uri = uri_for_attachment(doc, name)
105
+ uri = url_for_attachment(doc, name)
105
106
  RestClient.get uri
106
107
  # "#{@uri}/#{slug}/#{name}"
107
108
  end
@@ -110,13 +111,13 @@ module CouchRest
110
111
  def put_attachment(doc, name, file, options = {})
111
112
  docid = escape_docid(doc['_id'])
112
113
  name = CGI.escape(name)
113
- uri = uri_for_attachment(doc, name)
114
+ uri = url_for_attachment(doc, name)
114
115
  JSON.parse(RestClient.put(uri, file, options))
115
116
  end
116
117
 
117
118
  # DELETE an attachment directly from CouchDB
118
119
  def delete_attachment doc, name
119
- uri = uri_for_attachment(doc, name)
120
+ uri = url_for_attachment(doc, name)
120
121
  # this needs a rev
121
122
  JSON.parse(RestClient.delete(uri))
122
123
  end
@@ -144,18 +145,18 @@ module CouchRest
144
145
  result = if doc['_id']
145
146
  slug = escape_docid(doc['_id'])
146
147
  begin
147
- CouchRest.put "#{@uri}/#{slug}", doc
148
+ CouchRest.put "#{@root}/#{slug}", doc
148
149
  rescue RestClient::ResourceNotFound
149
150
  p "resource not found when saving even tho an id was passed"
150
151
  slug = doc['_id'] = @server.next_uuid
151
- CouchRest.put "#{@uri}/#{slug}", doc
152
+ CouchRest.put "#{@root}/#{slug}", doc
152
153
  end
153
154
  else
154
155
  begin
155
156
  slug = doc['_id'] = @server.next_uuid
156
- CouchRest.put "#{@uri}/#{slug}", doc
157
+ CouchRest.put "#{@root}/#{slug}", doc
157
158
  rescue #old version of couchdb
158
- CouchRest.post @uri, doc
159
+ CouchRest.post @root, doc
159
160
  end
160
161
  end
161
162
  if result['ok']
@@ -190,7 +191,7 @@ module CouchRest
190
191
  doc['_id'] = nextid if nextid
191
192
  end
192
193
  end
193
- CouchRest.post "#{@uri}/_bulk_docs", {:docs => docs}
194
+ CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
194
195
  end
195
196
  alias :bulk_delete :bulk_save
196
197
 
@@ -207,7 +208,7 @@ module CouchRest
207
208
  return { "ok" => true } # Mimic the non-deferred version
208
209
  end
209
210
  slug = escape_docid(doc['_id'])
210
- CouchRest.delete "#{@uri}/#{slug}?rev=#{doc['_rev']}"
211
+ CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
211
212
  end
212
213
 
213
214
  ### DEPRECATION NOTICE
@@ -227,7 +228,7 @@ module CouchRest
227
228
  else
228
229
  dest
229
230
  end
230
- CouchRest.copy "#{@uri}/#{slug}", destination
231
+ CouchRest.copy "#{@root}/#{slug}", destination
231
232
  end
232
233
 
233
234
  ### DEPRECATION NOTICE
@@ -238,7 +239,7 @@ module CouchRest
238
239
 
239
240
  # Compact the database, removing old document revisions and optimizing space use.
240
241
  def compact!
241
- CouchRest.post "#{@uri}/_compact"
242
+ CouchRest.post "#{@root}/_compact"
242
243
  end
243
244
 
244
245
  # Create the database
@@ -272,7 +273,7 @@ module CouchRest
272
273
  # catastrophic. Use with care!
273
274
  def delete!
274
275
  clear_extended_doc_fresh_cache
275
- CouchRest.delete @uri
276
+ CouchRest.delete @root
276
277
  end
277
278
 
278
279
  private
@@ -280,7 +281,7 @@ module CouchRest
280
281
  def clear_extended_doc_fresh_cache
281
282
  ::CouchRest::ExtendedDocument.subclasses.each{|klass| klass.design_doc_fresh = false if klass.respond_to?(:design_doc_fresh=) }
282
283
  end
283
-
284
+
284
285
  def uri_for_attachment(doc, name)
285
286
  if doc.is_a?(String)
286
287
  puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
@@ -293,7 +294,11 @@ module CouchRest
293
294
  docid = escape_docid(docid)
294
295
  name = CGI.escape(name)
295
296
  rev = "?rev=#{doc['_rev']}" if rev
296
- "#{@root}/#{docid}/#{name}#{rev}"
297
+ "/#{docid}/#{name}#{rev}"
298
+ end
299
+
300
+ def url_for_attachment(doc, name)
301
+ @root + uri_for_attachment(doc, name)
297
302
  end
298
303
 
299
304
  def escape_docid id
@@ -68,7 +68,7 @@ module CouchRest
68
68
  # Returns the CouchDB uri for the document
69
69
  def uri(append_rev = false)
70
70
  return nil if new_document?
71
- couch_uri = "http://#{database.uri}/#{CGI.escape(id)}"
71
+ couch_uri = "http://#{database.root}/#{CGI.escape(id)}"
72
72
  if append_rev == true
73
73
  couch_uri << "?rev=#{rev}"
74
74
  elsif append_rev.kind_of?(Integer)
@@ -0,0 +1,220 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module Collection
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Creates a new class method, find_all_<collection_name>, that will
12
+ # execute the view specified with the design_doc and view_name
13
+ # parameters, along with the specified view_options. This method will
14
+ # return the results of the view as an Array of objects which are
15
+ # instances of the class.
16
+ #
17
+ # This method is handy for objects that do not use the view_by method
18
+ # to declare their views.
19
+ def provides_collection(collection_name, design_doc, view_name, view_options)
20
+ class_eval <<-END, __FILE__, __LINE__ + 1
21
+ def self.find_all_#{collection_name}(options = {})
22
+ view_options = #{view_options.inspect} || {}
23
+ CollectionProxy.new(@database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
24
+ end
25
+ END
26
+ end
27
+
28
+ # Fetch a group of objects from CouchDB. Options can include:
29
+ # :page - Specifies the page to load (starting at 1)
30
+ # :per_page - Specifies the number of objects to load per page
31
+ #
32
+ # Defaults are used if these options are not specified.
33
+ def paginate(options)
34
+ proxy = create_collection_proxy(options)
35
+ proxy.paginate(options)
36
+ end
37
+
38
+ # Iterate over the objects in a collection, fetching them from CouchDB
39
+ # in groups. Options can include:
40
+ # :page - Specifies the page to load
41
+ # :per_page - Specifies the number of objects to load per page
42
+ #
43
+ # Defaults are used if these options are not specified.
44
+ def paginated_each(options, &block)
45
+ proxy = create_collection_proxy(options)
46
+ proxy.paginated_each(options, &block)
47
+ end
48
+
49
+ # Create a CollectionProxy for the specified view and options.
50
+ # CollectionProxy behaves just like an Array, but offers support for
51
+ # pagination.
52
+ def collection_proxy_for(design_doc, view_name, view_options = {})
53
+ options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
54
+ create_collection_proxy(options)
55
+ end
56
+
57
+ private
58
+
59
+ def create_collection_proxy(options)
60
+ design_doc, view_name, view_options = parse_view_options(options)
61
+ CollectionProxy.new(@database, design_doc, view_name, view_options, self)
62
+ end
63
+
64
+ def parse_view_options(options)
65
+ design_doc = options.delete(:design_doc)
66
+ raise ArgumentError, 'design_doc is required' if design_doc.nil?
67
+
68
+ view_name = options.delete(:view_name)
69
+ raise ArgumentError, 'view_name is required' if view_name.nil?
70
+
71
+ default_view_options = (design_doc.class == Design &&
72
+ design_doc['views'][view_name.to_s] &&
73
+ design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
74
+ view_options = default_view_options.merge(options)
75
+
76
+ [design_doc, view_name, view_options]
77
+ end
78
+ end
79
+
80
+ class CollectionProxy
81
+ alias_method :proxy_respond_to?, :respond_to?
82
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
83
+
84
+ DEFAULT_PAGE = 1
85
+ DEFAULT_PER_PAGE = 30
86
+
87
+ # Create a new CollectionProxy to represent the specified view. If a
88
+ # container class is specified, the proxy will create an object of the
89
+ # given type for each row that comes back from the view. If no
90
+ # container class is specified, the raw results are returned.
91
+ #
92
+ # The CollectionProxy provides support for paginating over a collection
93
+ # via the paginate, and paginated_each methods.
94
+ def initialize(database, design_doc, view_name, view_options = {}, container_class = nil)
95
+ raise ArgumentError, "database is a required parameter" if database.nil?
96
+
97
+ @database = database
98
+ @container_class = container_class
99
+
100
+ strip_pagination_options(view_options)
101
+ @view_options = view_options
102
+
103
+ if design_doc.class == Design
104
+ @view_name = "#{design_doc.name}/#{view_name}"
105
+ else
106
+ @view_name = "#{design_doc}/#{view_name}"
107
+ end
108
+ end
109
+
110
+ # See Collection.paginate
111
+ def paginate(options = {})
112
+ page, per_page = parse_options(options)
113
+ results = @database.view(@view_name, pagination_options(page, per_page))
114
+ remember_where_we_left_off(results, page)
115
+ convert_to_container_array(results)
116
+ end
117
+
118
+ # See Collection.paginated_each
119
+ def paginated_each(options = {}, &block)
120
+ page, per_page = parse_options(options)
121
+
122
+ begin
123
+ collection = paginate({:page => page, :per_page => per_page})
124
+ collection.each(&block)
125
+ page += 1
126
+ end until collection.size < per_page
127
+ end
128
+
129
+ def respond_to?(*args)
130
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
131
+ end
132
+
133
+ # Explicitly proxy === because the instance method removal above
134
+ # doesn't catch it.
135
+ def ===(other)
136
+ load_target
137
+ other === @target
138
+ end
139
+
140
+ private
141
+
142
+ def method_missing(method, *args)
143
+ if load_target
144
+ if block_given?
145
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
146
+ else
147
+ @target.send(method, *args)
148
+ end
149
+ end
150
+ end
151
+
152
+ def load_target
153
+ unless loaded?
154
+ results = @database.view(@view_name, @view_options)
155
+ @target = convert_to_container_array(results)
156
+ end
157
+ @loaded = true
158
+ @target
159
+ end
160
+
161
+ def loaded?
162
+ @loaded
163
+ end
164
+
165
+ def reload
166
+ reset
167
+ load_target
168
+ self unless @target.nil?
169
+ end
170
+
171
+ def reset
172
+ @loaded = false
173
+ @target = nil
174
+ end
175
+
176
+ def inspect
177
+ load_target
178
+ @target.inspect
179
+ end
180
+
181
+ def convert_to_container_array(results)
182
+ if @container_class.nil?
183
+ results
184
+ else
185
+ results['rows'].collect { |row| @container_class.new(row['doc']) } unless results['rows'].nil?
186
+ end
187
+ end
188
+
189
+ def pagination_options(page, per_page)
190
+ view_options = @view_options.clone
191
+ if @last_key && @last_docid && @last_page == page - 1
192
+ view_options.delete(:key)
193
+ options = { :startkey => @last_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
194
+ else
195
+ options = { :limit => per_page, :skip => per_page * (page - 1) }
196
+ end
197
+ view_options.merge(options)
198
+ end
199
+
200
+ def parse_options(options)
201
+ page = options.delete(:page) || DEFAULT_PAGE
202
+ per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
203
+ [page.to_i, per_page.to_i]
204
+ end
205
+
206
+ def strip_pagination_options(options)
207
+ parse_options(options)
208
+ end
209
+
210
+ def remember_where_we_left_off(results, page)
211
+ last_row = results['rows'].last
212
+ @last_key = last_row['key']
213
+ @last_docid = last_row['id']
214
+ @last_page = page
215
+ end
216
+ end
217
+
218
+ end
219
+ end
220
+ end
@@ -44,6 +44,12 @@ module CouchRest
44
44
  "#{database.root}/#{self.id}/#{attachment_name}"
45
45
  end
46
46
 
47
+ # returns URI to fetch the attachment from
48
+ def attachment_uri(attachment_name)
49
+ return unless has_attachment?(attachment_name)
50
+ "#{database.uri}/#{self.id}/#{attachment_name}"
51
+ end
52
+
47
53
  private
48
54
 
49
55
  def encode_attachment(data)
@@ -5,3 +5,4 @@ require File.join(File.dirname(__FILE__), 'design_doc')
5
5
  require File.join(File.dirname(__FILE__), 'validation')
6
6
  require File.join(File.dirname(__FILE__), 'extended_attachments')
7
7
  require File.join(File.dirname(__FILE__), 'class_proxy')
8
+ require File.join(File.dirname(__FILE__), 'collection')
@@ -1,6 +1,25 @@
1
1
  require 'time'
2
2
  require File.join(File.dirname(__FILE__), '..', 'more', 'property')
3
3
 
4
+ class Time
5
+ # returns a local time value much faster than Time.parse
6
+ def self.mktime_with_offset(string)
7
+ string =~ /(\d{4})\/(\d{2})\/(\d{2}) (\d{2}):(\d{2}):(\d{2}) ([\+\-])(\d{2})/
8
+ # $1 = year
9
+ # $2 = month
10
+ # $3 = day
11
+ # $4 = hours
12
+ # $5 = minutes
13
+ # $6 = seconds
14
+ # $7 = time zone direction
15
+ # $8 = tz difference
16
+ # utc time with wrong TZ info:
17
+ time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
18
+ tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
19
+ time + tz_difference + zone_offset(time.zone)
20
+ end
21
+ end
22
+
4
23
  module CouchRest
5
24
  module Mixins
6
25
  module Properties
@@ -24,7 +43,7 @@ module CouchRest
24
43
  self.class.properties.each do |property|
25
44
  key = property.name.to_s
26
45
  # let's make sure we have a default
27
- if property.default
46
+ unless property.default.nil?
28
47
  if property.default.class == Proc
29
48
  self[key] = property.default.call
30
49
  else
@@ -37,10 +56,11 @@ module CouchRest
37
56
  def cast_keys
38
57
  return unless self.class.properties
39
58
  self.class.properties.each do |property|
59
+
40
60
  next unless property.casted
41
61
  key = self.has_key?(property.name) ? property.name : property.name.to_sym
42
62
  # Don't cast the property unless it has a value
43
- next unless self[key]
63
+ next unless self[key]
44
64
  target = property.type
45
65
  if target.is_a?(Array)
46
66
  klass = ::CouchRest.constantize(target[0])
@@ -48,19 +68,21 @@ module CouchRest
48
68
  # Auto parse Time objects
49
69
  obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value)
50
70
  obj.casted_by = self if obj.respond_to?(:casted_by)
51
- obj
71
+ obj
52
72
  end
53
73
  else
54
74
  # Auto parse Time objects
55
- self[property.name] = if ((property.init_method == 'new') && target == 'Time')
56
- self[key].is_a?(String) ? Time.parse(self[key].dup) : self[key]
75
+ 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]
57
78
  else
58
79
  # Let people use :send as a Time parse arg
59
80
  klass = ::CouchRest.constantize(target)
60
- klass.send(property.init_method, self[key].dup)
61
- end
81
+ klass.send(property.init_method, self[key].dup)
82
+ end
62
83
  self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by)
63
- end
84
+ end
85
+
64
86
  end
65
87
  end
66
88
 
@@ -122,4 +144,4 @@ module CouchRest
122
144
 
123
145
  end
124
146
  end
125
- end
147
+ end
@@ -141,8 +141,12 @@ module CouchRest
141
141
  fetch_view(db, name, opts, &block)
142
142
  else
143
143
  begin
144
- view = fetch_view db, name, opts.merge({:include_docs => true}), &block
145
- view['rows'].collect{|r|new(r['doc'])} if view['rows']
144
+ if block.nil?
145
+ collection_proxy_for(design_doc, name, opts.merge({:include_docs => true}))
146
+ else
147
+ view = fetch_view db, name, opts.merge({:include_docs => true}), &block
148
+ view['rows'].collect{|r|new(r['doc'])} if view['rows']
149
+ end
146
150
  rescue
147
151
  # fallback for old versions of couchdb that don't
148
152
  # have include_docs support
@@ -1,5 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), 'support', 'class')
2
2
  require File.join(File.dirname(__FILE__), 'support', 'blank')
3
+ require 'timeout'
3
4
 
4
5
  # This file must be loaded after the JSON gem and any other library that beats up the Time class.
5
6
  class Time
@@ -38,7 +39,7 @@ if RUBY_VERSION.to_f < 1.9
38
39
  if IO.select([@io], nil, nil, @read_timeout)
39
40
  retry
40
41
  else
41
- raise Timeout::TimeoutError
42
+ raise Timeout::Error
42
43
  end
43
44
  end
44
45
  else
@@ -109,4 +110,4 @@ module RestClient
109
110
  # end
110
111
  # end
111
112
 
112
- end
113
+ end
@@ -13,10 +13,11 @@ module CouchRest
13
13
  include CouchRest::Mixins::DesignDoc
14
14
  include CouchRest::Mixins::ExtendedAttachments
15
15
  include CouchRest::Mixins::ClassProxy
16
+ include CouchRest::Mixins::Collection
16
17
 
17
- def self.subclasses
18
- ObjectSpace.enum_for(:each_object, class << self; self; end).to_a.delete_if{|k| k == self}
19
- end
18
+ def self.subclasses
19
+ @subclasses ||= []
20
+ end
20
21
 
21
22
  def self.inherited(subklass)
22
23
  subklass.send(:include, CouchRest::Mixins::Properties)
@@ -25,6 +26,7 @@ module CouchRest
25
26
  subklass.properties = self.properties.dup
26
27
  end
27
28
  EOS
29
+ subclasses << subklass
28
30
  end
29
31
 
30
32
  # Accessors
@@ -40,7 +40,7 @@ module CouchRest
40
40
  value = target.send(field_name)
41
41
  return true if @options[:allow_nil] && value.nil?
42
42
 
43
- value = value.kind_of?(Float) ? value.to_s('F') : value.to_s
43
+ value = (defined?(BigDecimal) && value.kind_of?(BigDecimal)) ? value.to_s('F') : value.to_s
44
44
 
45
45
  error_message = @options[:message]
46
46
  precision = @options[:precision]
@@ -12,7 +12,8 @@ describe CouchRest::Database do
12
12
  it "should escape the name in the URI" do
13
13
  db = @cr.database("foo/bar")
14
14
  db.name.should == "foo/bar"
15
- db.uri.should == "#{COUCHHOST}/foo%2Fbar"
15
+ db.root.should == "#{COUCHHOST}/foo%2Fbar"
16
+ db.uri.should == "/foo%2Fbar"
16
17
  end
17
18
  end
18
19
 
@@ -126,5 +126,10 @@ describe "ExtendedDocument attachments" do
126
126
  it 'should return the attachment URL as specified by CouchDB HttpDocumentApi' do
127
127
  @obj.attachment_url(@attachment_name).should == "#{Basic.database}/#{@obj.id}/#{@attachment_name}"
128
128
  end
129
+
130
+ it 'should return the attachment URI' do
131
+ @obj.attachment_uri(@attachment_name).should == "#{Basic.database.uri}/#{@obj.id}/#{@attachment_name}"
132
+ end
133
+
129
134
  end
130
135
  end
@@ -11,6 +11,7 @@ describe "ExtendedDocument" do
11
11
  property :set_by_proc, :default => Proc.new{Time.now}, :cast_as => 'Time'
12
12
  property :tags, :default => []
13
13
  property :read_only_with_default, :default => 'generic', :read_only => true
14
+ property :default_false, :default => false
14
15
  property :name
15
16
  timestamps!
16
17
  end
@@ -142,6 +143,10 @@ describe "ExtendedDocument" do
142
143
  it "should have the default value set at initalization" do
143
144
  @obj.preset.should == {:right => 10, :top_align => false}
144
145
  end
146
+
147
+ it "should have the default false value explicitly assigned" do
148
+ @obj.default_false.should == false
149
+ end
145
150
 
146
151
  it "should automatically call a proc default at initialization" do
147
152
  @obj.set_by_proc.should be_an_instance_of(Time)
@@ -337,5 +337,78 @@ describe "ExtendedDocument views" do
337
337
  Article.design_doc["views"].keys.should include("by_updated_at")
338
338
  end
339
339
  end
340
-
340
+
341
+ describe "with a collection" do
342
+ before(:all) do
343
+ reset_test_db!
344
+ @titles = ["very uniq one", "really interesting", "some fun",
345
+ "really awesome", "crazy bob", "this rocks", "super rad"]
346
+ @titles.each_with_index do |title,i|
347
+ a = Article.new(:title => title, :date => Date.today)
348
+ a.save
349
+ end
350
+ end
351
+ it "should return a proxy that looks like an array of 7 Article objects" do
352
+ articles = Article.by_date :key => Date.today
353
+ articles.class.should == Array
354
+ articles.size.should == 7
355
+ end
356
+ it "should get a subset of articles using paginate" do
357
+ articles = Article.by_date :key => Date.today
358
+ articles.paginate(:page => 1, :per_page => 3).size.should == 3
359
+ articles.paginate(:page => 2, :per_page => 3).size.should == 3
360
+ articles.paginate(:page => 3, :per_page => 3).size.should == 1
361
+ end
362
+ it "should get all articles, a few at a time, using paginated each" do
363
+ articles = Article.by_date :key => Date.today
364
+ articles.paginated_each(:per_page => 3) do |a|
365
+ a.should_not be_nil
366
+ end
367
+ end
368
+ it "should provide a class method to access the collection directly" do
369
+ articles = Article.collection_proxy_for('Article', 'by_date', :descending => true,
370
+ :key => Date.today, :include_docs => true)
371
+ articles.class.should == Array
372
+ articles.size.should == 7
373
+ end
374
+ it "should provide a class method for paginate" do
375
+ articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
376
+ :per_page => 3, :descending => true, :key => Date.today, :include_docs => true)
377
+ articles.size.should == 3
378
+
379
+ articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
380
+ :per_page => 3, :page => 2, :descending => true, :key => Date.today, :include_docs => true)
381
+ articles.size.should == 3
382
+
383
+ articles = Article.paginate(:design_doc => 'Article', :view_name => 'by_date',
384
+ :per_page => 3, :page => 3, :descending => true, :key => Date.today, :include_docs => true)
385
+ articles.size.should == 1
386
+ end
387
+ it "should provide a class method for paginated_each" do
388
+ options = { :design_doc => 'Article', :view_name => 'by_date',
389
+ :per_page => 3, :page => 1, :descending => true, :key => Date.today,
390
+ :include_docs => true }
391
+ Article.paginated_each(options) do |a|
392
+ a.should_not be_nil
393
+ end
394
+ end
395
+ it "should provide a class method to get a collection for a view" do
396
+ class Article
397
+ provides_collection :article_details, 'Article', 'by_date', :descending => true, :include_docs => true
398
+ end
399
+
400
+ articles = Article.find_all_article_details(:key => Date.today)
401
+ articles.class.should == Array
402
+ articles.size.should == 7
403
+ end
404
+ it "should raise an exception if design_doc is not provided" do
405
+ lambda{Article.collection_proxy_for(nil, 'by_date')}.should raise_error
406
+ lambda{Article.paginate(:view_name => 'by_date')}.should raise_error
407
+ end
408
+ it "should raise an exception if view_name is not provided" do
409
+ lambda{Article.collection_proxy_for('Article', nil)}.should raise_error
410
+ lambda{Article.paginate(:design_doc => 'Article')}.should raise_error
411
+ end
412
+ end
413
+
341
414
  end
@@ -1,4 +1,5 @@
1
1
  require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')
2
+ require File.join(FIXTURE_PATH, 'more', 'person')
2
3
  require File.join(FIXTURE_PATH, 'more', 'card')
3
4
  require File.join(FIXTURE_PATH, 'more', 'invoice')
4
5
  require File.join(FIXTURE_PATH, 'more', 'service')
@@ -36,6 +37,15 @@ describe "ExtendedDocument properties" do
36
37
  @card.family_name.should == @card.last_name
37
38
  end
38
39
 
40
+ it "should let you use an alias for a casted attribute" do
41
+ @card.cast_alias = Person.new(:name => "Aimonetti")
42
+ @card.cast_alias.name.should == "Aimonetti"
43
+ @card.calias.name.should == "Aimonetti"
44
+ card = Card.new(:first_name => "matt", :cast_alias => {:name => "Aimonetti"})
45
+ card.cast_alias.name.should == "Aimonetti"
46
+ card.calias.name.should == "Aimonetti"
47
+ end
48
+
39
49
  it "should be auto timestamped" do
40
50
  @card.created_at.should be_nil
41
51
  @card.updated_at.should be_nil
@@ -11,6 +11,8 @@ class Card < CouchRest::ExtendedDocument
11
11
  property :first_name
12
12
  property :last_name, :alias => :family_name
13
13
  property :read_only_value, :read_only => true
14
+ property :cast_alias, :cast_as => 'Person', :alias => :calias
15
+
14
16
 
15
17
  timestamps!
16
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couchrest
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.28"
4
+ version: "0.30"
5
5
  platform: ruby
6
6
  authors:
7
7
  - J. Chris Anderson
@@ -48,6 +48,7 @@ files:
48
48
  - README.md
49
49
  - Rakefile
50
50
  - THANKS.md
51
+ - history.txt
51
52
  - examples/model/example.rb
52
53
  - examples/word_count/markov
53
54
  - examples/word_count/views/books/chunked-map.js
@@ -73,6 +74,7 @@ files:
73
74
  - lib/couchrest/mixins/attachments.rb
74
75
  - lib/couchrest/mixins/callbacks.rb
75
76
  - lib/couchrest/mixins/class_proxy.rb
77
+ - lib/couchrest/mixins/collection.rb
76
78
  - lib/couchrest/mixins/design_doc.rb
77
79
  - lib/couchrest/mixins/document_queries.rb
78
80
  - lib/couchrest/mixins/extended_attachments.rb
@@ -161,7 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
163
  requirements: []
162
164
 
163
165
  rubyforge_project:
164
- rubygems_version: 1.3.2
166
+ rubygems_version: 1.3.4
165
167
  signing_key:
166
168
  specification_version: 3
167
169
  summary: Lean and RESTful interface to CouchDB.