couchrest 0.28 → 0.30

Sign up to get free protection for your applications and to get access to all the features.
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.