analysand 2.0.0 → 3.0.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/.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
|