ShyCouch 0.6.0 → 0.7.0

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