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 +4 -3
- data/Rakefile +2 -1
- data/VERSION +1 -1
- data/lib/ShyCouch.rb +274 -198
- data/lib/ShyCouch/data.rb +445 -243
- data/readme.md +117 -0
- data/test/test_ShyCouch.rb +17 -25
- data/test/test_couch_document.rb +171 -7
- data/test/test_couchdb_api.rb +45 -6
- data/test/test_couchdb_factory.rb +3 -3
- data/test/test_design_documents.rb +150 -129
- data/test/test_document_validation.rb +85 -0
- data/test/test_fields.rb +12 -12
- data/test/test_views.rb +129 -33
- metadata +49 -42
- data/.document +0 -5
- data/Gemfile.lock +0 -36
- data/README +0 -117
- data/ShyCouch.gemspec +0 -83
- data/design_notes.rb +0 -39
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 "
|
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
|
-
|
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.
|
1
|
+
0.7.0
|
data/lib/ShyCouch.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
59
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
76
|
+
def exists?
|
77
|
+
@server.has_database?(@name)
|
78
|
+
end
|
74
79
|
def create!
|
75
80
|
@server.create_db(@name)
|
76
81
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
82
|
+
def get_document_by_id(id)
|
83
|
+
@server.get_document_by_id(@name, id)
|
84
|
+
end
|
80
85
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
317
|
+
# Haven't decided whether PUT/POST should take a CouchDocument or a JSON string.
|
224
318
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
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
|
-
|
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
|