CouchDB-1.2.3: src/Database/CouchDB/HTTP.hs
-- |Maintains a persistent HTTP connection to a CouchDB database server.
-- CouchDB enjoys closing the connection if there is an error (document
-- not found, etc.) In such cases, 'CouchMonad' will automatically
-- reestablish the connection.
{-# LANGUAGE CPP #-}
module Database.CouchDB.HTTP
( request
, RequestMethod (..)
, CouchMonad
, Response (..)
, runCouchDB
, runCouchDB'
, runCouchDBURI
, CouchConn()
, createCouchConn
, createCouchConnFromURI
, runCouchDBWith
, closeCouchConn
) where
import Data.IORef
import Control.Concurrent
import Network.TCP
import Network.HTTP
import Network.URI
import Control.Exception (bracket)
import Control.Monad (liftM)
import Control.Monad.Trans (MonadIO (..))
import Data.Maybe (fromJust)
import qualified Data.ByteString as BS (ByteString, length)
import qualified Data.ByteString.UTF8 as UTF8 (fromString, toString)
import Network.HTTP.Auth
import Control.Applicative
import Control.Monad (ap)
-- |Describes a connection to a CouchDB database. This type is
-- encapsulated by 'CouchMonad'.
data CouchConn = CouchConn
{ ccConn :: IORef (HandleStream BS.ByteString)
, ccURI :: URI
, ccHostname :: String
, ccPort :: Int
, ccAuth :: Maybe Authority -- ^login credentials, if needed.
}
-- |A computation that interacts with a CouchDB database. This monad
-- encapsulates the 'IO' monad, a persistent HTTP connnection to a
-- CouchDB database and enough information to re-open the connection
-- if it is closed.
data CouchMonad a = CouchMonad (CouchConn -> IO (a,CouchConn))
instance Applicative CouchMonad where
pure = return
(<*>) = ap
instance Functor CouchMonad where
fmap = liftM
instance Monad CouchMonad where
return a = CouchMonad $ \conn -> return (a,conn)
(CouchMonad m) >>= k = CouchMonad $ \conn -> do
(a,conn') <- m conn
let (CouchMonad m') = k a
m' conn'
#if MIN_VERSION_base(4,13,0)
instance MonadFail CouchMonad where
#endif
fail msg = CouchMonad $ \conn -> do
fail $ "internal error: " ++ msg
instance MonadIO CouchMonad where
liftIO m = CouchMonad $ \conn -> m >>= \a -> return (a,conn)
makeURL :: String -- ^path
-> [(String,String)]
-> CouchMonad URI
makeURL path query = CouchMonad $ \conn -> do
return ( (ccURI conn) { uriPath = '/':path
, uriQuery = '?':(urlEncodeVars query)
}
,conn )
getConn :: CouchMonad (HandleStream BS.ByteString)
getConn = CouchMonad $ \conn -> do
r <- readIORef (ccConn conn)
return (r,conn)
getConnAuth :: CouchMonad (Maybe Authority)
getConnAuth = CouchMonad $ \conn -> return ((ccAuth conn),conn)
reopenConnection :: CouchMonad ()
reopenConnection = CouchMonad $ \conn -> do
c <- liftIO $ readIORef (ccConn conn) >>= close
connection <- liftIO $ openTCPConnection (ccHostname conn) (ccPort conn)
writeIORef (ccConn conn) connection
return ((), conn)
makeHeaders bodyLen =
[ Header HdrContentType "application/json"
, Header HdrConnection "keep-alive"
, Header HdrContentLength (show bodyLen)
]
-- |Send a request to the database. If the connection is closed, it is
-- reopened and the request is resent. On other errors, we raise an
-- exception.
request :: String -- ^path of the request
-> [(String,String)] -- ^dictionary of GET parameters
-> RequestMethod
-> [Header]
-> String -- ^body of the request
-> CouchMonad (Response String)
request path query method headers body = do
let body' = UTF8.fromString body
url <- makeURL path query
let allHeaders = (makeHeaders (BS.length body')) ++ headers
conn <- getConn
auth <- getConnAuth
let req' = Request url method allHeaders body'
let req = maybe req' (fillAuth req') auth
let retry 0 = do
fail $ "server error: " ++ show req
retry n = do
response <- liftIO $ sendHTTP conn req
case response of
Left err -> do
reopenConnection
retry (n-1)
Right val -> return (unUTF8 val)
retry 2
where
unUTF8 :: Response BS.ByteString -> Response String
unUTF8 (Response c r h b) = Response c r h (UTF8.toString b)
fillAuth :: Request a -> Authority -> Request a
fillAuth req auth = req { rqHeaders = new : rqHeaders req }
where new = Header HdrAuthorization (withAuthority auth req)
runCouchDBURI :: URI -- ^URI to connect
-> CouchMonad a
-> IO a
runCouchDBURI uri act = bracket
(createCouchConnFromURI uri)
closeCouchConn
(flip runCouchDBWith act)
runCouchDB :: String -- ^hostname
-> Int -- ^port
-> CouchMonad a
-> IO a
runCouchDB hostname port act = bracket
(createCouchConn hostname port)
closeCouchConn
(flip runCouchDBWith act)
-- |Connects to the CouchDB server at localhost:5984.
runCouchDB' :: CouchMonad a -> IO a
runCouchDB' = runCouchDB "127.0.0.1" 5984
-- |Run a CouchDB computation with an existing CouchDB connection.
runCouchDBWith :: CouchConn -> CouchMonad a -> IO a
runCouchDBWith conn (CouchMonad f) = fmap fst $ f conn
-- |Create a CouchDB connection for use with runCouchDBWith.
createCouchConn :: String -- ^hostname
-> Int -- ^port
-> IO (CouchConn)
createCouchConn hostname port = createCouchAuthConn hostname port Nothing
-- |Create a CouchDB connection with password authentication for use
-- with runCouchDBWith.
createCouchAuthConn :: String -- ^hostname
-> Int -- ^port
-> Maybe Authority -- ^Login credentials
-> IO (CouchConn)
createCouchAuthConn hostname port auth = do
let uriAuth = URIAuth "" hostname (':':(show port))
let baseURI = URI "http:" (Just uriAuth) "" "" ""
c <- openTCPConnection hostname port
conn <- newIORef c
return (CouchConn conn baseURI hostname port auth)
-- |Create a CouchDB from an URI connection for use with runCouchDBWith.
createCouchConnFromURI :: URI -- ^URI with possible login credentials
-> IO (CouchConn)
createCouchConnFromURI baseURI = do
createCouchAuthConn hostname port auth
where
ua = fromJust $ uriAuthority baseURI
hostname = uriRegName ua
port = uriAuthPort (Just baseURI) ua
ua2 = (fromJust.parseURIAuthority.uriToAuthorityString) baseURI
auth = (Just AuthBasic)
`ap` (return "")
`ap` (user ua2)
`ap` (password ua2)
`ap` (return baseURI)
-- |Closes an open CouchDB connection
closeCouchConn :: CouchConn -> IO ()
closeCouchConn (CouchConn conn _ _ _ _ ) = readIORef conn >>= close