yipdw/analysand

View on GitHub
lib/analysand/instance.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'analysand/config_response'
require 'analysand/errors'
require 'analysand/http'
require 'analysand/response'
require 'analysand/session_response'
require 'base64'
require 'net/http/persistent'
require 'rack/utils'
require 'uri'

module Analysand
  ##
  # Wraps a CouchDB instance.
  #
  # This class is meant to be used for interacting with parts of CouchDB that
  # aren't associated with any particular database: session management, for
  # example.  If you're looking to do database operations,
  # Analysand::Database is where you want to be.
  #
  # Instances MUST be identified by an absolute URI; instantiating this class
  # with a relative URI will raise an exception.
  #
  # Common tasks
  # ============
  #
  # Opening an instance
  # -------------------
  #
  #     instance = Analysand::Instance(URI('http://localhost:5984'))
  #
  #
  # Pinging an instance
  # -------------------
  #
  #     instance.ping  # => #<Response code=200 ...>
  #
  #
  # Establishing a session
  # ----------------------
  #
  #     resp, = instance.post_session('username', 'password')
  #     cookie = resp.session_cookie
  #
  # For harmony, the same credentials hash accepted by database methods is
  # also supported:
  #
  #     resp = instance.post_session(:username => 'username',
  #                                  :password => 'password')
  #
  #
  # resp.success? will be true if the session cookie is not empty, false
  # otherwise.
  #
  #
  # Testing a session cookie for validity
  # -------------------------------------
  #
  #     resp = instance.get_session(cookie)
  #
  # In CouchDB 1.2.0, the response body is a JSON object that looks like
  #
  #       {
  #           "info": {
  #               "authentication_db": "_users",
  #               "authentication_handlers": [
  #                   "oauth",
  #                   "cookie",
  #                   "default"
  #               ]
  #           },
  #           "ok": true,
  #           "userCtx": {
  #               "name": "username",
  #               "roles": ["member"]
  #           }
  #       }
  #
  # resp.valid? will be true if userCtx['name'] is non-null, false otherwise.
  #
  #
  # Adding and removing admins
  # --------------------------
  #
  #     instance.put_admin('admin', 'password', credentials)
  #     # => #<ConfigResponse code=200 ...>
  #     instance.delete_admin('admin', credentials)
  #     # => #<ConfigResponse code=200 ...>
  #
  # Obviously, you'll need admin credentials to manage the admin list.
  #
  # There also exist bang-method variants:
  #
  #     instance.put_admin!('admin', 'password', bad_creds)
  #     # => raises ConfigurationNotSaved on failure
  #     instance.delete_admin!('admin', bad_creds)
  #     # => raises ConfigurationNotDeleted on failure
  #
  #
  # Getting and setting instance configuration
  # ------------------------------------------
  #
  #     v = instance.get_config('couchdb_httpd_auth/allow_persistent_cookies',
  #       credentials)
  #     v.value # => false
  #
  #     instance.put_config('couchdb_httpd_auth/allow_persistent_cookies',
  #       '"true"', credentials)
  #     # => #<Response code=200 ...>
  #
  #     v = instance.get_config('couchdb_httpd_auth/allow_persistent_cookies',
  #       credentials)
  #     v.value #=> '"true"'
  #
  #     instance.delete_config('couchdb_httpd_auth/allow_persistent_cookies',
  #       credentials)
  #
  # You can get configuration at any level:
  #
  #     v = instance.get_config('', credentials)
  #     v.body['stats']['rate']  # => "1000", or whatever you have it set to
  #
  # #get_config and #put_config both return Response-like objects.  You can
  # check for failure or success that way:
  #
  #     v = instance.get_config('couchdb_httpd_auth/allow_persistent_cookies')
  #     v.code # => '403'
  #
  #     instance.put_config('couchdb_httpd_auth/allow_persistent_cookies', '"false"')
  #     # => #<Response code=403 ...>
  #
  # If you want to set configuration and just want to let errors bubble
  # up the stack, you can use the bang-variants:
  #
  #     instance.put_config!('stats/rate', '"1000"')
  #     # => on non-2xx response, raises ConfigurationNotSaved
  #
  #     instance.delete_config!('stats/rate')
  #     # => on non-2xx response, raises ConfigurationNotDeleted
  #
  #
  # Other instance-level services
  # -----------------------------
  #
  # CouchDB can be extended with additional service handlers; authentication
  # handlers are a popular example.
  #
  # Instance exposes #get, #put, and #post methods to access arbitrary
  # endpoints.
  #
  # Examples:
  #
  #     instance.get('_log', {}, admin_credentials)
  #     instance.post('_browserid', { 'assertion' => assertion },
  #       { 'Content-Type' => 'application/json' })
  #     instance.put('_config/httpd/bind_address', '192.168.0.1', {},
  #       admin_credentials)
  #
  class Instance
    include Errors
    include Http
    include Rack::Utils

    def initialize(uri)
      init_http_client(uri)
    end

    def get(path, headers = {}, credentials = nil)
      _get(path, credentials, {}, headers)
    end

    def post(path, body = nil, headers = {}, credentials = nil)
      _post(path, credentials, {}, headers, body)
    end

    def put(path, body = nil, headers = {}, credentials = nil)
      _put(path, credentials, {}, headers, body)
    end

    def delete(path, headers = {}, credentials = nil)
      _delete(path, credentials, {}, headers)
    end

    def put_admin(username, password, credentials = nil)
      put_config("admins/#{username}", %Q{"#{password}"}, credentials)
    end

    def put_admin!(username, password, credentials = nil)
      raise_put_error { put_admin(username, password, credentials) }
    end

    def delete_admin(username, credentials = nil)
      delete_config("admins/#{username}", credentials)
    end

    def delete_admin!(username, credentials = nil)
      raise_delete_error { delete_admin(username, credentials) }
    end

    def post_session(*args)
      username, password = if args.length == 2
                             args
                           else
                             h = args.first
                             [h[:username], h[:password]]
                           end

      headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
      body = build_query('name' => username, 'password' => password)

      Response.new post('_session', body, headers)
    end

    def get_session(cookie)
      headers = { 'Cookie' => cookie }

      SessionResponse.new get('_session', headers)
    end

    def get_config(key, credentials = nil)
      ConfigResponse.new get("_config/#{key}", {}, credentials)
    end

    def put_config(key, value, credentials = nil)
      ConfigResponse.new put("_config/#{key}", value, {}, credentials)
    end

    def put_config!(key, value, credentials = nil)
      raise_put_error { put_config(key, value, credentials) }
    end

    def delete_config(key, credentials = nil)
      ConfigResponse.new delete("_config/#{key}", {}, credentials)
    end

    def delete_config!(key, credentials = nil)
      raise_delete_error { delete_config(key, credentials) }
    end

    private

    def raise_put_error
      yield.tap do |resp|
        raise ex(ConfigurationNotSaved, resp) unless resp.success?
      end
    end

    def raise_delete_error
      yield.tap do |resp|
        raise ex(ConfigurationNotDeleted, resp) unless resp.success?
      end
    end
  end
end

# vim:ts=2:sw=2:et:tw=78