will-couchrest 0.32.1

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.
Files changed (98) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +165 -0
  3. data/Rakefile +74 -0
  4. data/THANKS.md +18 -0
  5. data/examples/model/example.rb +144 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/history.txt +65 -0
  17. data/lib/couchrest.rb +199 -0
  18. data/lib/couchrest/commands/generate.rb +71 -0
  19. data/lib/couchrest/commands/push.rb +103 -0
  20. data/lib/couchrest/core/adapters/restclient.rb +35 -0
  21. data/lib/couchrest/core/database.rb +317 -0
  22. data/lib/couchrest/core/design.rb +79 -0
  23. data/lib/couchrest/core/document.rb +83 -0
  24. data/lib/couchrest/core/http_abstraction.rb +48 -0
  25. data/lib/couchrest/core/response.rb +16 -0
  26. data/lib/couchrest/core/server.rb +88 -0
  27. data/lib/couchrest/core/view.rb +4 -0
  28. data/lib/couchrest/helper/pager.rb +103 -0
  29. data/lib/couchrest/helper/streamer.rb +44 -0
  30. data/lib/couchrest/helper/upgrade.rb +51 -0
  31. data/lib/couchrest/mixins.rb +4 -0
  32. data/lib/couchrest/mixins/attachments.rb +31 -0
  33. data/lib/couchrest/mixins/callbacks.rb +483 -0
  34. data/lib/couchrest/mixins/class_proxy.rb +116 -0
  35. data/lib/couchrest/mixins/collection.rb +225 -0
  36. data/lib/couchrest/mixins/design_doc.rb +103 -0
  37. data/lib/couchrest/mixins/document_queries.rb +82 -0
  38. data/lib/couchrest/mixins/extended_attachments.rb +74 -0
  39. data/lib/couchrest/mixins/extended_document_mixins.rb +8 -0
  40. data/lib/couchrest/mixins/properties.rb +158 -0
  41. data/lib/couchrest/mixins/validation.rb +257 -0
  42. data/lib/couchrest/mixins/views.rb +173 -0
  43. data/lib/couchrest/monkeypatches.rb +113 -0
  44. data/lib/couchrest/more/casted_model.rb +29 -0
  45. data/lib/couchrest/more/extended_document.rb +246 -0
  46. data/lib/couchrest/more/property.rb +40 -0
  47. data/lib/couchrest/support/blank.rb +42 -0
  48. data/lib/couchrest/support/class.rb +176 -0
  49. data/lib/couchrest/support/rails.rb +35 -0
  50. data/lib/couchrest/validation/auto_validate.rb +161 -0
  51. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  52. data/lib/couchrest/validation/validation_errors.rb +125 -0
  53. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  54. data/lib/couchrest/validation/validators/confirmation_validator.rb +99 -0
  55. data/lib/couchrest/validation/validators/format_validator.rb +117 -0
  56. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  57. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  58. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  59. data/lib/couchrest/validation/validators/length_validator.rb +134 -0
  60. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  61. data/lib/couchrest/validation/validators/numeric_validator.rb +104 -0
  62. data/lib/couchrest/validation/validators/required_field_validator.rb +109 -0
  63. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  64. data/spec/couchrest/core/database_spec.rb +700 -0
  65. data/spec/couchrest/core/design_spec.rb +138 -0
  66. data/spec/couchrest/core/document_spec.rb +267 -0
  67. data/spec/couchrest/core/server_spec.rb +35 -0
  68. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  69. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  70. data/spec/couchrest/more/casted_extended_doc_spec.rb +75 -0
  71. data/spec/couchrest/more/casted_model_spec.rb +177 -0
  72. data/spec/couchrest/more/extended_doc_attachment_spec.rb +135 -0
  73. data/spec/couchrest/more/extended_doc_spec.rb +588 -0
  74. data/spec/couchrest/more/extended_doc_subclass_spec.rb +98 -0
  75. data/spec/couchrest/more/extended_doc_view_spec.rb +426 -0
  76. data/spec/couchrest/more/property_spec.rb +169 -0
  77. data/spec/fixtures/attachments/README +3 -0
  78. data/spec/fixtures/attachments/couchdb.png +0 -0
  79. data/spec/fixtures/attachments/test.html +11 -0
  80. data/spec/fixtures/more/article.rb +34 -0
  81. data/spec/fixtures/more/card.rb +22 -0
  82. data/spec/fixtures/more/cat.rb +18 -0
  83. data/spec/fixtures/more/course.rb +14 -0
  84. data/spec/fixtures/more/event.rb +6 -0
  85. data/spec/fixtures/more/invoice.rb +17 -0
  86. data/spec/fixtures/more/person.rb +8 -0
  87. data/spec/fixtures/more/question.rb +6 -0
  88. data/spec/fixtures/more/service.rb +12 -0
  89. data/spec/fixtures/views/lib.js +3 -0
  90. data/spec/fixtures/views/test_view/lib.js +3 -0
  91. data/spec/fixtures/views/test_view/only-map.js +4 -0
  92. data/spec/fixtures/views/test_view/test-map.js +3 -0
  93. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  94. data/spec/spec.opts +6 -0
  95. data/spec/spec_helper.rb +37 -0
  96. data/utils/remap.rb +27 -0
  97. data/utils/subset.rb +30 -0
  98. metadata +198 -0
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+
3
+ module CouchRest
4
+ module Commands
5
+ module Generate
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ design_names = options[:trailing_args]
10
+
11
+ FileUtils.mkdir_p(directory)
12
+ filename = File.join(directory, "lib.js")
13
+ self.write(filename, <<-FUNC)
14
+ // Put global functions here.
15
+ // Include in your views with
16
+ //
17
+ // //include-lib
18
+ FUNC
19
+
20
+ design_names.each do |design_name|
21
+ subdirectory = File.join(directory, design_name)
22
+ FileUtils.mkdir_p(subdirectory)
23
+ filename = File.join(subdirectory, "sample-map.js")
24
+ self.write(filename, <<-FUNC)
25
+ function(doc) {
26
+ // Keys is first letter of _id
27
+ emit(doc._id[0], doc);
28
+ }
29
+ FUNC
30
+
31
+ filename = File.join(subdirectory, "sample-reduce.js")
32
+ self.write(filename, <<-FUNC)
33
+ function(keys, values) {
34
+ // Count the number of keys starting with this letter
35
+ return values.length;
36
+ }
37
+ FUNC
38
+
39
+ filename = File.join(subdirectory, "lib.js")
40
+ self.write(filename, <<-FUNC)
41
+ // Put functions specific to '#{design_name}' here.
42
+ // Include in your views with
43
+ //
44
+ // //include-lib
45
+ FUNC
46
+ end
47
+ end
48
+
49
+ def self.help
50
+ helpstring = <<-GEN
51
+
52
+ Usage: couchview generate directory design1 design2 design3 ...
53
+
54
+ Couchview will create directories and example views for the design documents you specify.
55
+
56
+ GEN
57
+ helpstring.gsub(/^ /, '')
58
+ end
59
+
60
+ def self.write(filename, contents)
61
+ puts "Writing #{filename}"
62
+ File.open(filename, "w") do |f|
63
+ # Remove leading spaces
64
+ contents.gsub!(/^ ( )?/, '')
65
+ f.write contents
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,103 @@
1
+ module CouchRest
2
+
3
+ module Commands
4
+
5
+ module Push
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ database = options[:trailing_args].first
10
+
11
+ fm = CouchRest::FileManager.new(database)
12
+ fm.loud = options[:loud]
13
+
14
+ if options[:loud]
15
+ puts "Pushing views from directory #{directory} to database #{fm.db}"
16
+ end
17
+
18
+ fm.push_views(directory)
19
+ end
20
+
21
+ def self.help
22
+ helpstring = <<-GEN
23
+
24
+ == Pushing views with Couchview ==
25
+
26
+ Usage: couchview push directory dbname
27
+
28
+ Couchview expects a specific filesystem layout for your CouchDB views (see
29
+ example below). It also supports advanced features like inlining of library
30
+ code (so you can keep DRY) as well as avoiding unnecessary document
31
+ modification.
32
+
33
+ Couchview also solves a problem with CouchDB's view API, which only provides
34
+ access to the final reduce side of any views which have both a map and a
35
+ reduce function defined. The intermediate map results are often useful for
36
+ development and production. CouchDB is smart enough to reuse map indexes for
37
+ functions duplicated across views within the same design document.
38
+
39
+ For views with a reduce function defined, Couchview creates both a reduce view
40
+ and a map-only view, so that you can browse and query the map side as well as
41
+ the reduction, with no performance penalty.
42
+
43
+ == Example ==
44
+
45
+ couchview push foo-project/bar-views baz-database
46
+
47
+ This will push the views defined in foo-project/bar-views into a database
48
+ called baz-database. Couchview expects the views to be defined in files with
49
+ names like:
50
+
51
+ foo-project/bar-views/my-design/viewname-map.js
52
+ foo-project/bar-views/my-design/viewname-reduce.js
53
+ foo-project/bar-views/my-design/noreduce-map.js
54
+
55
+ Pushed to => http://127.0.0.1:5984/baz-database/_design/my-design
56
+
57
+ And the design document:
58
+ {
59
+ "views" : {
60
+ "viewname-map" : {
61
+ "map" : "### contents of view-name-map.js ###"
62
+ },
63
+ "viewname-reduce" : {
64
+ "map" : "### contents of view-name-map.js ###",
65
+ "reduce" : "### contents of view-name-reduce.js ###"
66
+ },
67
+ "noreduce-map" : {
68
+ "map" : "### contents of noreduce-map.js ###"
69
+ }
70
+ }
71
+ }
72
+
73
+ Couchview will create a design document for each subdirectory of the views
74
+ directory specified on the command line.
75
+
76
+ == Library Inlining ==
77
+
78
+ Couchview can optionally inline library code into your views so you only have
79
+ to maintain it in one place. It looks for any files named lib.* in your
80
+ design-doc directory (for doc specific libs) and in the parent views directory
81
+ (for project global libs). These libraries are only inserted into views which
82
+ include the text
83
+
84
+ // !include lib
85
+
86
+ or
87
+
88
+ # !include lib
89
+
90
+ Couchview is a result of scratching my own itch. I'd be happy to make it more
91
+ general, so please contact me at jchris@grabb.it if you'd like to see anything
92
+ added or changed.
93
+
94
+ GEN
95
+ helpstring.gsub(/^ /, '')
96
+ end
97
+
98
+ end
99
+
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,35 @@
1
+ module RestClientAdapter
2
+
3
+ module API
4
+ def proxy=(url)
5
+ RestClient.proxy = url
6
+ end
7
+
8
+ def proxy
9
+ RestClient.proxy
10
+ end
11
+
12
+ def get(uri, headers={})
13
+ RestClient.get(uri, headers)
14
+ end
15
+
16
+ def post(uri, payload, headers={})
17
+ RestClient.post(uri, payload, headers)
18
+ end
19
+
20
+ def put(uri, payload, headers={})
21
+ RestClient.put(uri, payload, headers)
22
+ end
23
+
24
+ def delete(uri, headers={})
25
+ RestClient.delete(uri, headers)
26
+ end
27
+
28
+ def copy(uri, headers)
29
+ RestClient::Request.execute( :method => :copy,
30
+ :url => uri,
31
+ :headers => headers)
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,317 @@
1
+ require 'cgi'
2
+ require "base64"
3
+
4
+ module CouchRest
5
+ class Database
6
+ attr_reader :server, :host, :name, :root, :uri
7
+ attr_accessor :bulk_save_cache_limit
8
+
9
+ # Create a CouchRest::Database adapter for the supplied CouchRest::Server
10
+ # and database name.
11
+ #
12
+ # ==== Parameters
13
+ # server<CouchRest::Server>:: database host
14
+ # name<String>:: database name
15
+ #
16
+ def initialize(server, name)
17
+ @name = name
18
+ @server = server
19
+ @host = server.uri
20
+ @uri = "/#{name.gsub('/','%2F')}"
21
+ @root = host + uri
22
+ @streamer = Streamer.new(self)
23
+ @bulk_save_cache = []
24
+ @bulk_save_cache_limit = 500 # must be smaller than the uuid count
25
+ end
26
+
27
+ # returns the database's uri
28
+ def to_s
29
+ @root
30
+ end
31
+
32
+ # GET the database info from CouchDB
33
+ def info
34
+ CouchRest.get @root
35
+ end
36
+
37
+ # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
38
+ def documents(params = {})
39
+ keys = params.delete(:keys)
40
+ url = CouchRest.paramify_url "#{@root}/_all_docs", params
41
+ if keys
42
+ CouchRest.post(url, {:keys => keys})
43
+ else
44
+ CouchRest.get url
45
+ end
46
+ end
47
+
48
+ # load a set of documents by passing an array of ids
49
+ def get_bulk(ids)
50
+ documents(:keys => ids, :include_docs => true)
51
+ end
52
+ alias :bulk_load :get_bulk
53
+
54
+ # POST a temporary view function to CouchDB for querying. This is not
55
+ # recommended, as you don't get any performance benefit from CouchDB's
56
+ # materialized views. Can be quite slow on large databases.
57
+ def slow_view(funcs, params = {})
58
+ keys = params.delete(:keys)
59
+ funcs = funcs.merge({:keys => keys}) if keys
60
+ url = CouchRest.paramify_url "#{@root}/_temp_view", params
61
+ JSON.parse(HttpAbstraction.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
62
+ end
63
+
64
+ # backwards compatibility is a plus
65
+ alias :temp_view :slow_view
66
+
67
+ # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
68
+ # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
69
+ def view(name, params = {}, &block)
70
+ keys = params.delete(:keys)
71
+ name = name.split('/') # I think this will always be length == 2, but maybe not...
72
+ dname = name.shift
73
+ vname = name.join('/')
74
+ url = CouchRest.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
75
+ if keys
76
+ CouchRest.post(url, {:keys => keys})
77
+ else
78
+ if block_given?
79
+ @streamer.view("_design/#{dname}/_view/#{vname}", params, &block)
80
+ else
81
+ CouchRest.get url
82
+ end
83
+ end
84
+ end
85
+
86
+ # GET a document from CouchDB, by id. Returns a Ruby Hash.
87
+ def get(id, params = {})
88
+ slug = escape_docid(id)
89
+ url = CouchRest.paramify_url("#{@root}/#{slug}", params)
90
+ result = CouchRest.get(url)
91
+ return result unless result.is_a?(Hash)
92
+ doc = if /^_design/ =~ result["_id"]
93
+ Design.new(result)
94
+ else
95
+ Document.new(result)
96
+ end
97
+ doc.database = self
98
+ doc
99
+ end
100
+
101
+ # GET an attachment directly from CouchDB
102
+ def fetch_attachment(doc, name)
103
+ uri = url_for_attachment(doc, name)
104
+ HttpAbstraction.get uri
105
+ end
106
+
107
+ # PUT an attachment directly to CouchDB
108
+ def put_attachment(doc, name, file, options = {})
109
+ docid = escape_docid(doc['_id'])
110
+ name = CGI.escape(name)
111
+ uri = url_for_attachment(doc, name)
112
+ JSON.parse(HttpAbstraction.put(uri, file, options))
113
+ end
114
+
115
+ # DELETE an attachment directly from CouchDB
116
+ def delete_attachment doc, name
117
+ uri = url_for_attachment(doc, name)
118
+ # this needs a rev
119
+ JSON.parse(HttpAbstraction.delete(uri))
120
+ end
121
+
122
+ # Save a document to CouchDB. This will use the <tt>_id</tt> field from
123
+ # the document as the id for PUT, or request a new UUID from CouchDB, if
124
+ # no <tt>_id</tt> is present on the document. IDs are attached to
125
+ # documents on the client side because POST has the curious property of
126
+ # being automatically retried by proxies in the event of network
127
+ # segmentation and lost responses.
128
+ #
129
+ # If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
130
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
131
+ def save_doc(doc, bulk = false)
132
+ if doc['_attachments']
133
+ doc['_attachments'] = encode_attachments(doc['_attachments'])
134
+ end
135
+ if bulk
136
+ @bulk_save_cache << doc
137
+ return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
138
+ return {"ok" => true} # Compatibility with Document#save
139
+ elsif !bulk && @bulk_save_cache.length > 0
140
+ bulk_save
141
+ end
142
+ result = if doc['_id']
143
+ slug = escape_docid(doc['_id'])
144
+ begin
145
+ CouchRest.put "#{@root}/#{slug}", doc
146
+ rescue HttpAbstraction::ResourceNotFound
147
+ p "resource not found when saving even tho an id was passed"
148
+ slug = doc['_id'] = @server.next_uuid
149
+ CouchRest.put "#{@root}/#{slug}", doc
150
+ end
151
+ else
152
+ begin
153
+ slug = doc['_id'] = @server.next_uuid
154
+ CouchRest.put "#{@root}/#{slug}", doc
155
+ rescue #old version of couchdb
156
+ CouchRest.post @root, doc
157
+ end
158
+ end
159
+ if result['ok']
160
+ doc['_id'] = result['id']
161
+ doc['_rev'] = result['rev']
162
+ doc.database = self if doc.respond_to?(:database=)
163
+ end
164
+ result
165
+ end
166
+
167
+ ### DEPRECATION NOTICE
168
+ def save(doc, bulk=false)
169
+ puts "CouchRest::Database's save method is being deprecated, please use save_doc instead"
170
+ save_doc(doc, bulk)
171
+ end
172
+
173
+
174
+ # POST an array of documents to CouchDB. If any of the documents are
175
+ # missing ids, supply one from the uuid cache.
176
+ #
177
+ # If called with no arguments, bulk saves the cache of documents to be bulk saved.
178
+ def bulk_save(docs = nil, use_uuids = true)
179
+ if docs.nil?
180
+ docs = @bulk_save_cache
181
+ @bulk_save_cache = []
182
+ end
183
+ if (use_uuids)
184
+ ids, noids = docs.partition{|d|d['_id']}
185
+ uuid_count = [noids.length, @server.uuid_batch_count].max
186
+ noids.each do |doc|
187
+ nextid = @server.next_uuid(uuid_count) rescue nil
188
+ doc['_id'] = nextid if nextid
189
+ end
190
+ end
191
+ CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
192
+ end
193
+ alias :bulk_delete :bulk_save
194
+
195
+ # DELETE the document from CouchDB that has the given <tt>_id</tt> and
196
+ # <tt>_rev</tt>.
197
+ #
198
+ # If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
199
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
200
+ def delete_doc(doc, bulk = false)
201
+ raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
202
+ if bulk
203
+ @bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true }
204
+ return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
205
+ return { "ok" => true } # Mimic the non-deferred version
206
+ end
207
+ slug = escape_docid(doc['_id'])
208
+ CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
209
+ end
210
+
211
+ ### DEPRECATION NOTICE
212
+ def delete(doc, bulk=false)
213
+ puts "CouchRest::Database's delete method is being deprecated, please use delete_doc instead"
214
+ delete_doc(doc, bulk)
215
+ end
216
+
217
+ # COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
218
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
219
+ # hash with a '_rev' key
220
+ def copy_doc(doc, dest)
221
+ raise ArgumentError, "_id is required for copying" unless doc['_id']
222
+ slug = escape_docid(doc['_id'])
223
+ destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
224
+ "#{dest['_id']}?rev=#{dest['_rev']}"
225
+ else
226
+ dest
227
+ end
228
+ CouchRest.copy "#{@root}/#{slug}", destination
229
+ end
230
+
231
+ ### DEPRECATION NOTICE
232
+ def copy(doc, dest)
233
+ puts "CouchRest::Database's copy method is being deprecated, please use copy_doc instead"
234
+ copy_doc(doc, dest)
235
+ end
236
+
237
+ # Compact the database, removing old document revisions and optimizing space use.
238
+ def compact!
239
+ CouchRest.post "#{@root}/_compact"
240
+ end
241
+
242
+ # Create the database
243
+ def create!
244
+ bool = server.create_db(@name) rescue false
245
+ bool && true
246
+ end
247
+
248
+ # Delete and re create the database
249
+ def recreate!
250
+ delete!
251
+ create!
252
+ rescue HttpAbstraction::ResourceNotFound
253
+ ensure
254
+ create!
255
+ end
256
+
257
+ # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
258
+ def replicate_from other_db
259
+ raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
260
+ CouchRest.post "#{@host}/_replicate", :source => other_db.root, :target => name
261
+ end
262
+
263
+ # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
264
+ def replicate_to other_db
265
+ raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
266
+ CouchRest.post "#{@host}/_replicate", :target => other_db.root, :source => name
267
+ end
268
+
269
+ # DELETE the database itself. This is not undoable and could be rather
270
+ # catastrophic. Use with care!
271
+ def delete!
272
+ clear_extended_doc_fresh_cache
273
+ CouchRest.delete @root
274
+ end
275
+
276
+ private
277
+
278
+ def clear_extended_doc_fresh_cache
279
+ ::CouchRest::ExtendedDocument.subclasses.each{|klass| klass.design_doc_fresh = false if klass.respond_to?(:design_doc_fresh=) }
280
+ end
281
+
282
+ def uri_for_attachment(doc, name)
283
+ if doc.is_a?(String)
284
+ puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
285
+ docid = doc
286
+ rev = nil
287
+ else
288
+ docid = doc['_id']
289
+ rev = doc['_rev']
290
+ end
291
+ docid = escape_docid(docid)
292
+ name = CGI.escape(name)
293
+ rev = "?rev=#{doc['_rev']}" if rev
294
+ "/#{docid}/#{name}#{rev}"
295
+ end
296
+
297
+ def url_for_attachment(doc, name)
298
+ @root + uri_for_attachment(doc, name)
299
+ end
300
+
301
+ def escape_docid id
302
+ /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
303
+ end
304
+
305
+ def encode_attachments(attachments)
306
+ attachments.each do |k,v|
307
+ next if v['stub']
308
+ v['data'] = base64(v['data'])
309
+ end
310
+ attachments
311
+ end
312
+
313
+ def base64(data)
314
+ Base64.encode64(data).gsub(/\s/,'')
315
+ end
316
+ end
317
+ end