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 CHANGED
@@ -16,3 +16,4 @@ spec/reports
16
16
  test/tmp
17
17
  test/version_tmp
18
18
  tmp
19
+ .ruby-version
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 - a terrible burden for a couch
2
- -----------------------------------------
1
+ 1. Analysand
3
2
 
4
3
  Analysand is a CouchDB client library of dubious worth. It was extracted from
5
- https://github.com/amvorg-underground/catalog.
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
- Developing Analysand
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
@@ -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', '>= 0.12'
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'
@@ -6,7 +6,7 @@ module Analysand
6
6
  # records.
7
7
  class BulkResponse < Response
8
8
  def success?
9
- body.none? { |r| r.has_key?('error') }
9
+ super && body.none? { |r| r.has_key?('error') }
10
10
  end
11
11
  end
12
12
  end
@@ -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
- # Not all responses from sub-resources of _config return valid JSON objects,
7
- # but JSON.parse expects to see a full JSON object. This object implements a
8
- # bit of a hacky workaround if the body does not start with a '{' and end
9
- # with a '}', then it is not run through the JSON parser.
10
- class ConfigResponse < Response
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
- body = response.body.chomp
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
@@ -22,6 +22,9 @@ module Analysand
22
22
  class ConfigurationNotSaved < DatabaseError
23
23
  end
24
24
 
25
+ class ConfigurationNotDeleted < DatabaseError
26
+ end
27
+
25
28
  class DocumentNotSaved < DatabaseError
26
29
  end
27
30
 
@@ -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)
@@ -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.set_config('couchdb_httpd_auth/allow_persistent_cookies',
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 #set_config both return Response-like objects. You can
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.set_config('couchdb_httpd_auth/allow_persistent_cookies', false)
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.set_config!('stats/rate', 1000)
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 _post('_session', nil, {}, headers, body)
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 _get('_session', nil, {}, headers, nil)
216
+ SessionResponse.new get('_session', headers)
142
217
  end
143
218
 
144
219
  def get_config(key, credentials = nil)
145
- ConfigResponse.new _get("_config/#{key}", credentials)
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
- private
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 session(cookie, resp)
183
- token = cookie.split('=', 2).last
184
- fields = Base64.decode64(token).split(':')
231
+ def delete_config(key, credentials = nil)
232
+ ConfigResponse.new delete("_config/#{key}", {}, credentials)
233
+ end
185
234
 
186
- username = fields[0]
187
- time = fields[1].to_i(16)
235
+ def delete_config!(key, credentials = nil)
236
+ raise_delete_error { delete_config(key, credentials) }
237
+ end
188
238
 
189
- roles = resp.body.has_key?('userCtx') ?
190
- resp.body['userCtx']['roles'] : resp.body['roles']
239
+ private
191
240
 
192
- { :issued_at => time,
193
- :roles => roles,
194
- :token => cookie,
195
- :username => username
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
@@ -10,6 +10,14 @@ module Analysand
10
10
  c >= 200 && c <= 299
11
11
  end
12
12
 
13
+ def unauthorized?
14
+ code.to_i == 401
15
+ end
16
+
17
+ def not_found?
18
+ code.to_i == 404
19
+ end
20
+
13
21
  def conflict?
14
22
  code.to_i == 409
15
23
  end