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 +10 -0
- data/Rakefile +1 -1
- data/history.txt +19 -0
- data/lib/couchrest.rb +1 -1
- data/lib/couchrest/core/database.rb +26 -21
- data/lib/couchrest/core/document.rb +1 -1
- data/lib/couchrest/mixins/collection.rb +220 -0
- data/lib/couchrest/mixins/extended_attachments.rb +6 -0
- data/lib/couchrest/mixins/extended_document_mixins.rb +1 -0
- data/lib/couchrest/mixins/properties.rb +31 -9
- data/lib/couchrest/mixins/views.rb +6 -2
- data/lib/couchrest/monkeypatches.rb +3 -2
- data/lib/couchrest/more/extended_document.rb +5 -3
- data/lib/couchrest/validation/validators/numeric_validator.rb +1 -1
- data/spec/couchrest/core/database_spec.rb +2 -1
- data/spec/couchrest/more/extended_doc_attachment_spec.rb +5 -0
- data/spec/couchrest/more/extended_doc_spec.rb +5 -0
- data/spec/couchrest/more/extended_doc_view_spec.rb +74 -1
- data/spec/couchrest/more/property_spec.rb +10 -0
- data/spec/fixtures/more/card.rb +2 -0
- metadata +4 -2
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.
|
|
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
|
|
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
|
-
@
|
|
29
|
+
@root
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# GET the database info from CouchDB
|
|
32
33
|
def info
|
|
33
|
-
CouchRest.get @
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
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("#{@
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
157
|
+
CouchRest.put "#{@root}/#{slug}", doc
|
|
157
158
|
rescue #old version of couchdb
|
|
158
|
-
CouchRest.post @
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
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 "#{@
|
|
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 @
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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::
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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?(
|
|
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.
|
|
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
|
data/spec/fixtures/more/card.rb
CHANGED
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.
|
|
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.
|
|
166
|
+
rubygems_version: 1.3.4
|
|
165
167
|
signing_key:
|
|
166
168
|
specification_version: 3
|
|
167
169
|
summary: Lean and RESTful interface to CouchDB.
|