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 +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.
|