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 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