analysand 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/CHANGELOG +34 -0
- data/README +14 -8
- data/analysand.gemspec +1 -1
- data/lib/analysand/bulk_response.rb +1 -1
- data/lib/analysand/change_watcher.rb +8 -6
- data/lib/analysand/config_response.rb +14 -13
- data/lib/analysand/errors.rb +3 -0
- data/lib/analysand/http.rb +10 -0
- data/lib/analysand/instance.rb +108 -54
- data/lib/analysand/status_code_predicates.rb +8 -0
- data/lib/analysand/version.rb +1 -1
- data/spec/analysand/a_response.rb +23 -16
- data/spec/analysand/database_writing_spec.rb +12 -2
- data/spec/analysand/instance_spec.rb +69 -17
- data/spec/analysand/view_response_spec.rb +2 -2
- data/spec/analysand/view_streaming_spec.rb +3 -3
- data/spec/fixtures/vcr_cassettes/{unauthorized_set_config.yml → unauthorized_put_config.yml} +0 -0
- data/spec/spec_helper.rb +1 -0
- metadata +133 -219
- data/spec/analysand/a_session_grantor.rb +0 -40
- data/spec/fixtures/vcr_cassettes/set_config.yml +0 -77
data/.gitignore
CHANGED
data/CHANGELOG
CHANGED
@@ -1,8 +1,42 @@
|
|
1
1
|
Issue numbers refer to issues on Analysand's Github tracker:
|
2
2
|
https://github.com/yipdw/analysand/issues
|
3
3
|
|
4
|
+
3.0.0 (2013-07-08)
|
5
|
+
------------------
|
6
|
+
|
7
|
+
* Change Celluloid dependency to 0.14.
|
8
|
+
|
9
|
+
3.0.0.pre2 (2013-04-15)
|
10
|
+
-----------------------
|
11
|
+
|
12
|
+
* Change Celluloid dependency to 0.13.
|
13
|
+
Please note: Analysand does not require celluloid/autostart. It's up to you
|
14
|
+
to decide whether or not you need that for your application.
|
15
|
+
|
16
|
+
3.0.0.pre (2013-02-26)
|
17
|
+
----------------------
|
18
|
+
|
19
|
+
* Instance#set_config renamed to Instance#put_config
|
20
|
+
* Instance#put_admin, Instance#delete_admin for db admin setup
|
21
|
+
* JSON encoding/decoding removed from Instance#*_config methods: all values are
|
22
|
+
sent to/received from CouchDB verbatim. This means that you'll have to quote all values,
|
23
|
+
e.g.
|
24
|
+
|
25
|
+
instance.set_config("stats/rate", 1200)
|
26
|
+
|
27
|
+
becomes
|
28
|
+
|
29
|
+
instance.put_config("stats/rate", '"1200"')
|
30
|
+
|
31
|
+
|
32
|
+
x.y.z (2012-12-31)
|
33
|
+
------------------
|
34
|
+
|
35
|
+
* Analysand::Writing#bulk_docs! now raises BulkOperationFailed on 401 responses
|
36
|
+
|
4
37
|
2.0.0 (2012-11-29)
|
5
38
|
------------------
|
39
|
+
|
6
40
|
* Instance#establish_session and Instance#renew_session now return a (session,
|
7
41
|
Analysand::Response pair)
|
8
42
|
* Share HTTP code between Database and Instance
|
data/README
CHANGED
@@ -1,18 +1,20 @@
|
|
1
|
-
Analysand
|
2
|
-
-----------------------------------------
|
1
|
+
1. Analysand
|
3
2
|
|
4
3
|
Analysand is a CouchDB client library of dubious worth. It was extracted from
|
5
|
-
|
4
|
+
the a-m-v.org catalog application:
|
5
|
+
https://code.ninjawedding.org/git/amvorg-underground/catalog.git.
|
6
6
|
|
7
7
|
Analysand was written for Ruby 1.9. It is known to work on Ruby 1.9.3-p194 and
|
8
8
|
Rubinius 2.0.0.
|
9
9
|
|
10
|
-
Features
|
10
|
+
2. Features
|
11
11
|
|
12
12
|
* GET, PUT, DELETE on databases
|
13
13
|
* GET, PUT, DELETE, HEAD, COPY on documents
|
14
14
|
* GET, PUT on document attachments
|
15
15
|
* GET, POST on views
|
16
|
+
* GET, PUT on server configuration
|
17
|
+
* GET, PUT, POST on arbitrary service handlers
|
16
18
|
* POST /_session
|
17
19
|
* POST /_bulk_docs
|
18
20
|
* View streaming
|
@@ -20,8 +22,7 @@ Features:
|
|
20
22
|
* Cookie and HTTP Basic authentication for all of the above
|
21
23
|
* Database objects can be safely shared across threads
|
22
24
|
|
23
|
-
|
24
|
-
--------------------
|
25
|
+
3. Development
|
25
26
|
|
26
27
|
You'll need a CouchDB >= 1.1.0 instance. I recommend not using a CouchDB
|
27
28
|
instance that you're using for anything else; Analysand requires the presence
|
@@ -36,7 +37,12 @@ Naturally, we hang with all the cool kids:
|
|
36
37
|
* Code Climate: https://codeclimate.com/github/yipdw/analysand
|
37
38
|
* Gemnasium: https://gemnasium.com/yipdw/analysand
|
38
39
|
|
39
|
-
License
|
40
|
-
-------
|
40
|
+
4. License
|
41
41
|
|
42
42
|
Copyright 2012 David Yip; made available under the MIT license.
|
43
|
+
|
44
|
+
5. Special thanks
|
45
|
+
|
46
|
+
Fear of Tigers, 3LAU, Ellie Goulding, TeddyLoid, Susumu Hirasawa.
|
47
|
+
|
48
|
+
# vim:ts=2:sw=2:et:tw=78
|
data/analysand.gemspec
CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |gem|
|
|
17
17
|
|
18
18
|
gem.required_ruby_version = '>= 1.9'
|
19
19
|
|
20
|
-
gem.add_dependency 'celluloid', '
|
20
|
+
gem.add_dependency 'celluloid', '~> 0.14.0'
|
21
21
|
gem.add_dependency 'celluloid-io'
|
22
22
|
gem.add_dependency 'http_parser.rb'
|
23
23
|
gem.add_dependency 'json'
|
@@ -163,12 +163,6 @@ module Analysand
|
|
163
163
|
@running = false
|
164
164
|
end
|
165
165
|
|
166
|
-
##
|
167
|
-
# Called by Celluloid::IO's actor shutdown code.
|
168
|
-
def finalize
|
169
|
-
@socket.close if @socket && !@socket.closed?
|
170
|
-
end
|
171
|
-
|
172
166
|
##
|
173
167
|
# Can be used to set query parameters. query is a Hash. The query hash
|
174
168
|
# has two default parameters:
|
@@ -267,6 +261,14 @@ module Analysand
|
|
267
261
|
@socket.write("\r\n\r\n")
|
268
262
|
end
|
269
263
|
|
264
|
+
##
|
265
|
+
# @private
|
266
|
+
def disconnect
|
267
|
+
@socket.close if @socket && !@socket.closed?
|
268
|
+
end
|
269
|
+
|
270
|
+
finalizer :disconnect
|
271
|
+
|
270
272
|
##
|
271
273
|
# @private
|
272
274
|
def prepare_request
|
@@ -3,22 +3,23 @@ require 'analysand/response'
|
|
3
3
|
module Analysand
|
4
4
|
# Public: Wraps responses from /_config.
|
5
5
|
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
6
|
+
# GET/PUT/DELETE /_config does not return a valid JSON object in all cases.
|
7
|
+
# This response object therefore does The Simplest Possible Thing and just
|
8
|
+
# gives you back the response body as a string.
|
9
|
+
class ConfigResponse
|
10
|
+
include ResponseHeaders
|
11
|
+
include StatusCodePredicates
|
12
|
+
|
13
|
+
attr_reader :response
|
14
|
+
attr_reader :body
|
15
|
+
|
11
16
|
alias_method :value, :body
|
12
17
|
|
13
18
|
def initialize(response)
|
14
|
-
|
15
|
-
|
16
|
-
if body.start_with?('{') && body.end_with?('}')
|
17
|
-
super
|
18
|
-
else
|
19
|
-
@response = response
|
20
|
-
@body = body
|
21
|
-
end
|
19
|
+
@response = response
|
20
|
+
@body = response.body.chomp
|
22
21
|
end
|
23
22
|
end
|
24
23
|
end
|
24
|
+
|
25
|
+
# vim:ts=2:sw=2:et:tw=78
|
data/lib/analysand/errors.rb
CHANGED
data/lib/analysand/http.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require 'net/http/persistent'
|
2
3
|
require 'rack/utils'
|
3
4
|
require 'uri'
|
@@ -9,11 +10,20 @@ module Analysand
|
|
9
10
|
# SHOULD be a Net::HTTP::Persistent instance, and @uri SHOULD be a URI
|
10
11
|
# instance.
|
11
12
|
module Http
|
13
|
+
extend Forwardable
|
14
|
+
|
12
15
|
include Rack::Utils
|
13
16
|
|
14
17
|
attr_reader :http
|
15
18
|
attr_reader :uri
|
16
19
|
|
20
|
+
SSL_METHODS = %w(
|
21
|
+
certificate ca_file cert_store private_key
|
22
|
+
reuse_ssl_sessions ssl_version verify_callback verify_mode
|
23
|
+
).map { |m| [m, "#{m}="] }.flatten
|
24
|
+
|
25
|
+
def_delegators :http, *SSL_METHODS
|
26
|
+
|
17
27
|
def init_http_client(uri)
|
18
28
|
unless uri.respond_to?(:path) && uri.respond_to?(:absolute?)
|
19
29
|
uri = URI(uri)
|
data/lib/analysand/instance.rb
CHANGED
@@ -78,6 +78,24 @@ module Analysand
|
|
78
78
|
# resp.valid? will be true if userCtx['name'] is non-null, false otherwise.
|
79
79
|
#
|
80
80
|
#
|
81
|
+
# Adding and removing admins
|
82
|
+
# --------------------------
|
83
|
+
#
|
84
|
+
# instance.put_admin('admin', 'password', credentials)
|
85
|
+
# # => #<ConfigResponse code=200 ...>
|
86
|
+
# instance.delete_admin('admin', credentials)
|
87
|
+
# # => #<ConfigResponse code=200 ...>
|
88
|
+
#
|
89
|
+
# Obviously, you'll need admin credentials to manage the admin list.
|
90
|
+
#
|
91
|
+
# There also exist bang-method variants:
|
92
|
+
#
|
93
|
+
# instance.put_admin!('admin', 'password', bad_creds)
|
94
|
+
# # => raises ConfigurationNotSaved on failure
|
95
|
+
# instance.delete_admin!('admin', bad_creds)
|
96
|
+
# # => raises ConfigurationNotDeleted on failure
|
97
|
+
#
|
98
|
+
#
|
81
99
|
# Getting and setting instance configuration
|
82
100
|
# ------------------------------------------
|
83
101
|
#
|
@@ -85,33 +103,58 @@ module Analysand
|
|
85
103
|
# credentials)
|
86
104
|
# v.value # => false
|
87
105
|
#
|
88
|
-
# instance.
|
89
|
-
# true, credentials)
|
106
|
+
# instance.put_config('couchdb_httpd_auth/allow_persistent_cookies',
|
107
|
+
# '"true"', credentials)
|
90
108
|
# # => #<Response code=200 ...>
|
91
109
|
#
|
92
110
|
# v = instance.get_config('couchdb_httpd_auth/allow_persistent_cookies',
|
93
111
|
# credentials)
|
94
|
-
# v.value #=> true
|
112
|
+
# v.value #=> '"true"'
|
113
|
+
#
|
114
|
+
# instance.delete_config('couchdb_httpd_auth/allow_persistent_cookies',
|
115
|
+
# credentials)
|
95
116
|
#
|
96
117
|
# You can get configuration at any level:
|
97
118
|
#
|
98
119
|
# v = instance.get_config('', credentials)
|
99
120
|
# v.body['stats']['rate'] # => "1000", or whatever you have it set to
|
100
121
|
#
|
101
|
-
# #get_config and #
|
122
|
+
# #get_config and #put_config both return Response-like objects. You can
|
102
123
|
# check for failure or success that way:
|
103
124
|
#
|
104
125
|
# v = instance.get_config('couchdb_httpd_auth/allow_persistent_cookies')
|
105
126
|
# v.code # => '403'
|
106
127
|
#
|
107
|
-
# instance.
|
128
|
+
# instance.put_config('couchdb_httpd_auth/allow_persistent_cookies', '"false"')
|
108
129
|
# # => #<Response code=403 ...>
|
109
130
|
#
|
110
131
|
# If you want to set configuration and just want to let errors bubble
|
111
132
|
# up the stack, you can use the bang-variants:
|
112
133
|
#
|
113
|
-
# instance.
|
134
|
+
# instance.put_config!('stats/rate', '"1000"')
|
114
135
|
# # => on non-2xx response, raises ConfigurationNotSaved
|
136
|
+
#
|
137
|
+
# instance.delete_config!('stats/rate')
|
138
|
+
# # => on non-2xx response, raises ConfigurationNotDeleted
|
139
|
+
#
|
140
|
+
#
|
141
|
+
# Other instance-level services
|
142
|
+
# -----------------------------
|
143
|
+
#
|
144
|
+
# CouchDB can be extended with additional service handlers; authentication
|
145
|
+
# handlers are a popular example.
|
146
|
+
#
|
147
|
+
# Instance exposes #get, #put, and #post methods to access arbitrary
|
148
|
+
# endpoints.
|
149
|
+
#
|
150
|
+
# Examples:
|
151
|
+
#
|
152
|
+
# instance.get('_log', {}, admin_credentials)
|
153
|
+
# instance.post('_browserid', { 'assertion' => assertion },
|
154
|
+
# { 'Content-Type' => 'application/json' })
|
155
|
+
# instance.put('_config/httpd/bind_address', '192.168.0.1', {},
|
156
|
+
# admin_credentials)
|
157
|
+
#
|
115
158
|
class Instance
|
116
159
|
include Errors
|
117
160
|
include Http
|
@@ -121,6 +164,38 @@ module Analysand
|
|
121
164
|
init_http_client(uri)
|
122
165
|
end
|
123
166
|
|
167
|
+
def get(path, headers = {}, credentials = nil)
|
168
|
+
_get(path, credentials, {}, headers)
|
169
|
+
end
|
170
|
+
|
171
|
+
def post(path, body = nil, headers = {}, credentials = nil)
|
172
|
+
_post(path, credentials, {}, headers, body)
|
173
|
+
end
|
174
|
+
|
175
|
+
def put(path, body = nil, headers = {}, credentials = nil)
|
176
|
+
_put(path, credentials, {}, headers, body)
|
177
|
+
end
|
178
|
+
|
179
|
+
def delete(path, headers = {}, credentials = nil)
|
180
|
+
_delete(path, credentials, {}, headers)
|
181
|
+
end
|
182
|
+
|
183
|
+
def put_admin(username, password, credentials = nil)
|
184
|
+
put_config("admins/#{username}", %Q{"#{password}"}, credentials)
|
185
|
+
end
|
186
|
+
|
187
|
+
def put_admin!(username, password, credentials = nil)
|
188
|
+
raise_put_error { put_admin(username, password, credentials) }
|
189
|
+
end
|
190
|
+
|
191
|
+
def delete_admin(username, credentials = nil)
|
192
|
+
delete_config("admins/#{username}", credentials)
|
193
|
+
end
|
194
|
+
|
195
|
+
def delete_admin!(username, credentials = nil)
|
196
|
+
raise_delete_error { delete_admin(username, credentials) }
|
197
|
+
end
|
198
|
+
|
124
199
|
def post_session(*args)
|
125
200
|
username, password = if args.length == 2
|
126
201
|
args
|
@@ -132,68 +207,47 @@ module Analysand
|
|
132
207
|
headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
|
133
208
|
body = build_query('name' => username, 'password' => password)
|
134
209
|
|
135
|
-
Response.new
|
210
|
+
Response.new post('_session', body, headers)
|
136
211
|
end
|
137
212
|
|
138
213
|
def get_session(cookie)
|
139
214
|
headers = { 'Cookie' => cookie }
|
140
215
|
|
141
|
-
SessionResponse.new
|
216
|
+
SessionResponse.new get('_session', headers)
|
142
217
|
end
|
143
218
|
|
144
219
|
def get_config(key, credentials = nil)
|
145
|
-
ConfigResponse.new
|
146
|
-
end
|
147
|
-
|
148
|
-
def set_config(key, value, credentials = nil)
|
149
|
-
# This is a bizarre transformation that deserves some explanation.
|
150
|
-
#
|
151
|
-
# CouchDB configuration is made available as strings containing JSON
|
152
|
-
# data. GET /_config/stats, for example, will return something like
|
153
|
-
# this:
|
154
|
-
#
|
155
|
-
# {"rate":"1000","samples":"[0, 60, 300, 900]"}
|
156
|
-
#
|
157
|
-
# However, I'd really like to write
|
158
|
-
#
|
159
|
-
# instance.set_config('stats/samples', [0, 60, 300, 900])
|
160
|
-
#
|
161
|
-
# and I'd also like to be able to use values from get_config directly,
|
162
|
-
# just for symmetry:
|
163
|
-
#
|
164
|
-
# v = instance1.get_config('stats/samples')
|
165
|
-
# instance2.set_config('stats/samples', v)
|
166
|
-
#
|
167
|
-
# To accomplish this, we convert non-string values to JSON twice.
|
168
|
-
# Strings are passed through.
|
169
|
-
body = (String === value) ? value : value.to_json.to_json
|
170
|
-
|
171
|
-
ConfigResponse.new _put("_config/#{key}", credentials, {}, {}, body)
|
172
|
-
end
|
173
|
-
|
174
|
-
def set_config!(key, value, credentials = nil)
|
175
|
-
set_config(key, value, credentials).tap do |resp|
|
176
|
-
raise ex(ConfigurationNotSaved, resp) unless resp.success?
|
177
|
-
end
|
220
|
+
ConfigResponse.new get("_config/#{key}", {}, credentials)
|
178
221
|
end
|
179
222
|
|
180
|
-
|
223
|
+
def put_config(key, value, credentials = nil)
|
224
|
+
ConfigResponse.new put("_config/#{key}", value, {}, credentials)
|
225
|
+
end
|
226
|
+
|
227
|
+
def put_config!(key, value, credentials = nil)
|
228
|
+
raise_put_error { put_config(key, value, credentials) }
|
229
|
+
end
|
181
230
|
|
182
|
-
def
|
183
|
-
|
184
|
-
|
231
|
+
def delete_config(key, credentials = nil)
|
232
|
+
ConfigResponse.new delete("_config/#{key}", {}, credentials)
|
233
|
+
end
|
185
234
|
|
186
|
-
|
187
|
-
|
235
|
+
def delete_config!(key, credentials = nil)
|
236
|
+
raise_delete_error { delete_config(key, credentials) }
|
237
|
+
end
|
188
238
|
|
189
|
-
|
190
|
-
resp.body['userCtx']['roles'] : resp.body['roles']
|
239
|
+
private
|
191
240
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
241
|
+
def raise_put_error
|
242
|
+
yield.tap do |resp|
|
243
|
+
raise ex(ConfigurationNotSaved, resp) unless resp.success?
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def raise_delete_error
|
248
|
+
yield.tap do |resp|
|
249
|
+
raise ex(ConfigurationNotDeleted, resp) unless resp.success?
|
250
|
+
end
|
197
251
|
end
|
198
252
|
end
|
199
253
|
end
|