ShyCouch 0.6.0 → 0.7.0

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/Gemfile CHANGED
@@ -3,7 +3,9 @@ source "http://rubygems.org"
3
3
  # Example:
4
4
  # gem "activesupport", ">= 2.3.5"
5
5
 
6
- gem "ShyRubyJS"
6
+ gem "rest-client", ">= 0"
7
+ gem "sourcify", "~> 0.5.0"
8
+ gem "ShyRubyJS", ">= 0.2.0"
7
9
 
8
10
  # Add dependencies to develop your gem here.
9
11
  # Include everything needed to run rake, tests, features, etc.
@@ -11,6 +13,5 @@ group :development do
11
13
  gem "bundler", "~> 1.0.0"
12
14
  gem "jeweler", "~> 1.6.4"
13
15
  gem "rcov", ">= 0"
14
- gem "sourcify", "~> 0.5.0"
15
- gem "ShyRubyJS"
16
+
16
17
  end
data/Rakefile CHANGED
@@ -21,9 +21,10 @@ Jeweler::Tasks.new do |gem|
21
21
  gem.description = %Q{Ruby API for CouchDB, designed to work with the Camping micro-framework.}
22
22
  gem.email = "danbryan@gmail.com"
23
23
  gem.authors = ["Shy Inc.", "Daniel Bryan", "Cerales"]
24
- gem.add_dependency "ShyRubyJS"
24
+ gem.add_dependency "ShyRubyJS", ">= 0.2.0"
25
25
  gem.add_dependency "rest-client", ">=1.6.7"
26
26
  gem.add_dependency "sourcify", ">=0"
27
+ gem.add_dependency "mime-types", ">=0"
27
28
  # dependencies defined in Gemfile
28
29
  end
29
30
  Jeweler::RubygemsDotOrgTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0
1
+ 0.7.0
@@ -17,27 +17,29 @@ require 'ShyRubyJS'
17
17
  require 'ShyCouch/data'
18
18
  require 'ShyCouch/fields'
19
19
 
20
-
21
20
  module ShyCouch
22
21
  class << self
23
- def getDB(settings)
24
- # this is a wrapper for creating CouchDatabase object and testing that it can connect
25
- database = CouchDatabase.new(settings)
26
- puts database.connect unless database.connect["ok"] #TODO - hm
27
- database.create! unless database.exists?
28
- return database
29
- end
22
+ def getDB(settings)
23
+ # this is a wrapper for creating CouchDatabase object and testing that it can connect
24
+ database = CouchDatabase.new(settings)
25
+ puts database.connect unless database.connect["ok"] #TODO - hm
26
+ database.create! unless database.exists?
27
+ return database
28
+ end
30
29
  end
31
30
  attr_accessor :database
32
31
 
33
- class ShyCouchError < StandardError; end
34
-
32
+ class ShyCouchError < StandardError; end
35
33
  class ShyCouchDesignMissing < ShyCouchError; end
34
+ class ShyCouch::DatabaseError < ShyCouchError; end
35
+ class ShyCouch::ViewError < ShyCouchError; end
36
+ class ShyCouch::DesignConflict < ShyCouchError; end
37
+ class ShyCouch::ResourceNotFound < ShyCouchError; end
38
+ class ShyCouch::DocumentValidationError < ShyCouchError; end
36
39
 
37
40
  class CouchDatabase
38
41
  def initialize(settings)
39
- raise StandardError, "invalid settings" if settings == nil
40
- init(settings)
42
+ init(settings)
41
43
  end
42
44
 
43
45
  attr_accessor :server, :name, :host, :port, :design_documents, :designs
@@ -55,216 +57,290 @@ module ShyCouch
55
57
  end
56
58
  def server_responds?; return @server.responds?; end
57
59
 
58
- def design(design)
59
- if design.kind_of?(ShyCouch::Data::Design)
60
- return design_by_name(design.name)
61
- else
62
- return design_by_name(design)
63
- end
64
- end
65
-
66
- def design_by_name(name)
67
- @designs.each { |design| return design if design.name == name.to_s }
68
- return nil
60
+ def init!
61
+ create! unless exists?
69
62
  end
63
+ def design(design)
64
+ if design.kind_of?(ShyCouch::Data::Design)
65
+ return design_by_name(design.name)
66
+ else
67
+ return design_by_name(design)
68
+ end
69
+ end
70
+
71
+ def design_by_name(name)
72
+ @designs.each { |design| return design if design.name == name.to_s }
73
+ return nil
74
+ end
70
75
 
71
- def exists?
72
- @server.has_database?(@name)
73
- end
76
+ def exists?
77
+ @server.has_database?(@name)
78
+ end
74
79
  def create!
75
80
  @server.create_db(@name)
76
81
  end
77
- def get_document_by_id(id)
78
- @server.get_document_by_id(@name, id)
79
- end
82
+ def get_document_by_id(id)
83
+ @server.get_document_by_id(@name, id)
84
+ end
80
85
 
81
- def pull_design(designName)
82
- doc = @server.get_document_by_id(@name, designName)
83
- return ShyCouch::Data::Design.new(designName).merge! doc
84
- end
85
- def all_docs
86
- get_document_by_id('_all_docs').rows.map { |doc|
87
- get_document_by_id(doc["id"])
88
- }
89
- end
90
- def uri
91
- return "http://#{host}/#{port}/#{name}"
92
- end
86
+ def pull_design(designName)
87
+ doc = @server.get_document_by_id(@name, designName)
88
+ return ShyCouch::Data::Design.new(designName).merge! doc
89
+ end
90
+ def all_docs
91
+ get_document_by_id('_all_docs').rows.map { |doc|
92
+ get_document_by_id(doc["id"])
93
+ }
94
+ end
95
+ def uri
96
+ return "http://#{host}/#{port}/#{name}"
97
+ end
93
98
 
94
- def pull_document(document)
95
- @server.pull_document(self.name, document)
96
- end
99
+ def pull_document(document)
100
+ @server.pull_document(self.name, document)
101
+ rescue RestClient::ResourceNotFound
102
+ raise ShyCouch::ResourceNotFound
103
+ end
97
104
 
98
- def push_document!(document)
99
- @server.push_document(self.name, document)
100
- end
101
-
102
- def query_view(design_name, view_name)
103
- queryString = "/_design/#{design_name}/_view/#{view_name}"
104
- ShyCouch::Data::ViewResult.new(get_document_by_id(queryString))
105
- end
106
-
107
- def add_design(design)
108
- throw TypeError unless design.kind_of?(ShyCouch::Data::Design)
109
- # if the db has already stored the design, update it
110
- if design_by_name(design.name) != nil
111
- design_by_name(design.name) == design
112
- end
113
- # otherwise, add it
114
- @designs << design
115
- end
116
-
117
- def add_designs_and_push!(*docs)
118
- docs.each do |doc|
119
- add_design(doc)
120
- end
121
- push_designs!
122
- end
123
-
124
- def add_design_and_push!(doc)
125
- add_design(doc)
126
- push_designs!
127
- end
128
-
129
- def push_designs!
130
- @designs.each do |design|
131
- # push the document and update it with the rev sent by the response
132
- design["_rev"] = push_document!(design)["rev"]
105
+ def push_document!(doc, opts = {})
106
+ raise(ShyCouch::DocumentValidationError, "Document missing required fields: #{doc.missing_needs}") unless doc.satisfies_needs?
107
+ raise ShyCouch::DocumentValidationError, "Document missing suggested fields, not overriden: #{doc.missing_suggestions}" unless doc.satisfies_suggestions?(opts)
108
+ @server.push_document(self.name, doc)
109
+ end
110
+
111
+ def delete_document!(doc, opts = {})
112
+ if !doc._id
113
+ if !doc._rev
114
+ raise ShyCouch::DocumentValidationError, "Cannot delete document without id and rev" unless doc._id
115
+ else
116
+ raise ShyCouch::DocumentValidationError, "Cannot delete document without id" unless doc._id
117
+ end
118
+ elsif !doc._rev
119
+ raise ShyCouch::DocumentValidationError, "Cannot delete document without id" unless doc._id
120
+ end
121
+ @server.delete_document(self.name, doc)
122
+ end
123
+
124
+ def query_view(design_name, view_name, opts = {})
125
+ # build query string from the options
126
+ uri = "_design/#{design_name}/_view/#{view_name}"
127
+ uri += "?#{query_string opts}" if opts.length > 0
128
+ result = ShyCouch::Data::ViewResultHandler.init(get_document_by_id(uri))
129
+ if opts.has_key? :key and result.length == 1
130
+ # Return it as a document if it was a key query
131
+ return result[0]
132
+ else
133
+ return result
133
134
  end
134
- end
135
-
136
- def view(design, view_obj)
137
- url = "#{design._id}/_view/#{view_obj.name}"
138
- get_document_by_id(url)
139
- end
140
-
141
- private
142
-
143
- def init(settings, design_documents = [])
135
+ end
136
+
137
+ def query_string(opts = {})
138
+ # builds a http query string from options
139
+ # need to stringify key queries
140
+ exceptions = [:from] # stuff that might get passed in that's not for a query option
141
+
142
+ return opts.map{ |k,v|
143
+ raise ShyCouch::ViewError, "Illegal view option: #{k}" unless @legal_view_options.has_key? k
144
+ if @legal_view_options[k][:stringify] == true
145
+ "#{k.to_s}=#{URI.escape(v.to_s.to_json)}" if !exceptions.include? k
146
+ else
147
+ "#{k.to_s}=#{v.to_s}" if !exceptions.include? k
148
+ end
149
+ }.join("&")
150
+ end
151
+
152
+ def add_design(design)
153
+ throw TypeError unless design.kind_of?(ShyCouch::Data::Design)
154
+ # if the db has already stored the design, update it
155
+ if design_by_name(design.name) != nil
156
+ design_by_name(design.name) == design
157
+ end
158
+ # otherwise, add it
159
+ @designs << design
160
+ end
161
+
162
+ def add_designs_and_push!(*docs)
163
+ docs.each do |doc|
164
+ add_design(doc)
165
+ end
166
+ push_designs!
167
+ end
168
+
169
+ def add_design_and_push!(doc)
170
+ add_design(doc)
171
+ push_designs!
172
+ end
173
+
174
+ def push_designs!
175
+ @designs.each do |design|
176
+ # push the document and update it with the rev sent by the response
177
+ begin
178
+ design._rev = push_document!(design)["rev"]
179
+ rescue ShyCouch::DesignConflict => e
180
+ # Get the rev of the existing one, then try again!
181
+ # If that fails, it should still throw an error
182
+ existing_design = get_document_by_id design._id
183
+ design._rev = existing_design._rev
184
+ design._rev = push_document!(design)["rev"]
185
+ end
186
+ end
187
+ end
188
+
189
+ def view(design, view_obj)
190
+ url = "#{design._id}/_view/#{view_obj.name}"
191
+ get_document_by_id(url)
192
+ end
193
+
194
+ private
195
+
196
+ def init(settings, design_documents = [])
197
+ validate_settings settings
144
198
  db_settings = settings["db"]
145
199
  @host, @port, @name, @user, @password = db_settings["host"],db_settings["port"], db_settings["name"],db_settings["user"], db_settings["password"]
146
200
  @designs = ShyCouch::Data::CouchDocumentCollection.new
147
201
  @server = CouchServerConnection.allocate
148
- end
149
-
150
- class CouchServerConnection
151
- def initialize(args, options=nil)#host, port, user, password, options = nil)
152
- @host = args["host"]
153
- @port = args["port"]
154
- @user = args["user"]
155
- @password = args["password"]
156
- @db_name = args["database"]
157
- @options = options
158
- end
202
+ @legal_view_options = {
203
+ :key => { :stringify => true },
204
+ :startkey => { :stringify => true },
205
+ :startkey_docid => { :stringify => false },
206
+ :endkey => { :stringify => true },
207
+ :endkey_docid => { :stringify => false },
208
+ :limit => { :stringify => false },
209
+ :stale => { :stringify => false },
210
+ :descending => { :stringify => false },
211
+ :skip => { :stringify => false },
212
+ :group => { :stringify => false },
213
+ :group_level => { :stringify => false },
214
+ :reduce => { :stringify => false },
215
+ :include_docs => { :stringify => false },
216
+ :inclusive_end => { :stringify => false },
217
+ :update_seq => { :stringify => false }
218
+ }
219
+ end
159
220
 
160
- def responds?
161
- if req(:get, "/")['couchdb'] == "Welcome"
162
- true
163
- else
164
- false
165
- end
166
- rescue Errno::ECONNREFUSED
167
- false
168
- end
221
+ def validate_settings settings
222
+ # Should check that host and port exist
223
+ # and that database name is specified
224
+ # and that there is a password if there is a username
225
+ raise ShyCouch::DatabaseError unless settings["db"]
226
+ raise ShyCouch::DatabaseError unless settings["db"]["host"]
227
+ raise ShyCouch::DatabaseError unless settings["db"]["port"]
228
+ raise ShyCouch::DatabaseError unless settings["db"]["name"]
229
+ if settings["db"]["user"]
230
+ raise ShyCouch::DatabaseError unless settings["db"]["password"]
231
+ end
232
+ end
233
+
234
+ class CouchServerConnection
235
+ def initialize(args, options=nil)#host, port, user, password, options = nil)
236
+ @host = args["host"]
237
+ @port = args["port"]
238
+ @user = args["user"]
239
+ @password = args["password"]
240
+ @db_name = args["database"]
241
+ @options = options
242
+ end
169
243
 
170
- def has_database?(db_name)
171
- req(:get, "/#{db_name}/")
172
- true
173
- rescue RestClient::ResourceNotFound
174
- false
175
- end
176
-
177
- def req(kind, uri, data = nil)
178
- raise TypeError unless [:get,:delete,:put,:post].include?(kind) # only support these 4 request methods currently
179
- res = (@user and @password ? couch_req_with_auth(kind, uri, data) : couch_req_without_auth(kind, uri, data))
180
- return JSON.parse(res)
181
- end
182
- def req_host
183
- "http://#{(@user + ':' + @password + '@') if @user and @password}#{@host}:#{@port}"
184
- end
185
- def couch_req_with_auth(kind, uri = nil, data = nil)
186
- uri ? uri = req_host + uri : uri = req_host
187
- if kind == :get or kind == :delete
188
- RestClient.method(kind).call(uri, :content_type => :json, :user => @user, :password => @password)
189
- else
190
- RestClient.method(kind).call(uri, data, :content_type => :json)#, :user => @user, :password => @password)
191
- end
192
- end
193
- def couch_req_without_auth(kind, uri, data = nil)
194
- uri ? uri = req_host + uri : uri = req_host
195
- if kind == :get or kind == :delete
196
- RestClient.method(kind).call(uri, :content_type => :json, :user => @user, :password => @password)
197
- else
198
- RestClient.method(kind).call(uri, data,:content_type => :json, :user => @user, :password => @password)
199
- end
200
-
201
- end
202
- #TODO - this is screwed
203
- def pull_all_doc_ids(db_name)
204
- req(:get,"/#{db_name}/_all_docs")["rows"].map { |doc| doc["id"] }
205
- end
244
+ def responds?
245
+ if req(:get, "/")['couchdb'] == "Welcome"
246
+ true
247
+ else
248
+ false
249
+ end
250
+ rescue Errno::ECONNREFUSED
251
+ false
252
+ end
206
253
 
207
- def all_docs_from_database(db_name)
208
- pull_all_doc_ids(db_name).map { |id| Data::CouchDocument.new(req(:get,"/#{db_name}/#{id}")) }
209
- end
254
+ def has_database?(db_name)
255
+ req(:get, "/#{db_name}/")
256
+ true
257
+ rescue RestClient::ResourceNotFound
258
+ false
259
+ end
260
+
261
+ def req(kind, uri, data = nil)
262
+ raise TypeError unless [:get,:delete,:put,:post].include?(kind) # only support these 4 request methods currently
263
+ if @user and @password
264
+ res = couch_req_with_auth kind, uri, data
265
+ else
266
+ res = couch_req_without_auth kind, uri, data
267
+ end
268
+ return JSON.parse(res)
269
+ rescue RestClient::Conflict => e
270
+ # attempting to update without rev will throw this error
271
+
272
+ # sometimes it's because a server booted and tried to push design documents that existed:
273
+ if JSON.parse(data)["_id"].include? "_design"
274
+ # throw a different error
275
+ raise ShyCouch::DesignConflict, "Design document update refused, or other resource conflict."
276
+ else
277
+ raise e
278
+ end
279
+
280
+ end
281
+ def req_host
282
+ "http://#{(@user + ':' + @password + '@') if @user and @password}#{@host}:#{@port}"
283
+ end
284
+ def couch_req_with_auth(kind, uri = nil, data = nil)
285
+ uri ? uri = req_host + uri : uri = req_host
286
+ if kind == :get or kind == :delete
287
+ RestClient.method(kind).call(uri, :content_type => :json, :user => @user, :password => @password)
288
+ else
289
+ RestClient.method(kind).call(uri, data, :content_type => :json, :user => @user, :password => @password)
290
+ end
291
+ end
292
+ def couch_req_without_auth(kind, uri, data = nil)
293
+ uri ? uri = req_host + uri : uri = req_host
294
+ if kind == :get or kind == :delete
295
+ RestClient.method(kind).call(uri, :content_type => :json)
296
+ else
297
+ RestClient.method(kind).call(uri, data,:content_type => :json)
298
+ end
299
+
300
+ end
301
+ #TODO - this is screwed
302
+ def pull_all_doc_ids(db_name)
303
+ req(:get,"/#{db_name}/_all_docs")["rows"].map { |doc| doc["id"] }
304
+ end
210
305
 
211
- def get_document_by_id(db_name, id)
212
- document = Data::CouchDocument.new(:data => req(:get,"/#{db_name}/#{id}"))
213
- end
214
- def pull_document(db_name, document)
215
- document = Data::CouchDocument.new(:data => req(:get,"/#{db_name}/#{document._id}"))
216
- end
306
+ def all_docs_from_database(db_name)
307
+ pull_all_doc_ids(db_name).map { |id| Data::CouchDocument.new(req(:get,"/#{db_name}/#{id}")) }
308
+ end
217
309
 
218
- def delete_document(db_name, id)
219
- delete("/#{db_name}/#{id}")
220
- RestClient.delete ""
221
- end
310
+ def get_document_by_id(db_name, id)
311
+ document = Data::CouchDocument.new(:data => req(:get,"/#{db_name}/#{id}"))
312
+ end
313
+ def pull_document(db_name, document)
314
+ document = Data::CouchDocument.new(:data => req(:get,"/#{db_name}/#{document._id}"))
315
+ end
222
316
 
223
- # Haven't decided whether PUT/POST should take a CouchDocument or a JSON string.
317
+ # Haven't decided whether PUT/POST should take a CouchDocument or a JSON string.
224
318
 
225
- def push_document(db_name, document)
226
- raise TypeError unless document.kind_of?(ShyCouch::Data::CouchDocument)
227
- raise JSON::GeneratorError unless document.valid?
228
- if document["_rev"]
229
- # puts "/#{db_name}/#{document._id}?rev=#{document._rev}/" #TODO - remove
230
- return req(:put, "/#{db_name}/#{document._id}?rev=#{document._rev}/", document.to_json)
231
- elsif document["_id"]
232
- return req(:put, "/#{db_name}/#{document._id}", document.to_json)
233
- else
234
- return req(:post, "/#{db_name}/", document.to_json)
235
- end
319
+ def push_document(db_name, document)
320
+ raise TypeError unless document.kind_of?(ShyCouch::Data::CouchDocument)
321
+ raise JSON::GeneratorError unless document.valid?
322
+ if document["_rev"]
323
+ return req(:put, "/#{db_name}/#{document._id}?rev=#{document._rev}/", document.to_json)
324
+ elsif document["_id"]
325
+ return req(:put, "/#{db_name}/#{document._id}", document.to_json)
326
+ else
327
+ return req(:post, "/#{db_name}/", document.to_json)
328
+ end
236
329
  end
237
330
 
238
- def create_db(db_name)
239
- req(:put, "/#{db_name}/")
240
- end
241
- def delete_db(db_name)
242
- req(:delete,"/#{db_name}/")
243
- end
331
+ def delete_document(db_name, document)
332
+ raise TypeError unless document.kind_of?(ShyCouch::Data::CouchDocument)
333
+ return req(:delete, "/#{db_name}/#{document._id}?rev=#{document._rev}")
334
+ end
244
335
 
245
- private
336
+ def create_db(db_name)
337
+ req(:put, "/#{db_name}/")
338
+ end
339
+ def delete_db(db_name)
340
+ req(:delete,"/#{db_name}/")
341
+ end
246
342
 
247
- # def handle_failure(req, res)
248
- # raise RuntimeError.new("#{res.code}:#{res.message}\nMETHOD:#{req.method}\nURI:#{req.path}\n#{res.body}")
249
- # end
250
- #
251
- # def handle_error(e)
252
- # raise RuntimeError.new("#{e.inspect}\n Maybe be due to illegal rev or id change")
253
- # end
254
-
255
- # def request(req)
256
- # res = Net::HTTP.start(@host, @port) { |http|
257
- # req.basic_auth(@user, @password) if @user and @password
258
- # http.request(req)
259
- # }
260
- # unless res.kind_of?(Net::HTTPSuccess)
261
- # handle_failure(req, res)
262
- # end
263
- # res
264
- # rescue Errno::ECONNRESET => e
265
- # handle_error(e)
266
- # end
267
- end
343
+ end
268
344
 
269
345
  end
270
346
  end