{-# 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)
data CouchConn = CouchConn
{ ccConn :: IORef (HandleStream BS.ByteString)
, ccURI :: URI
, ccHostname :: String
, ccPort :: Int
, ccAuth :: Maybe Authority
}
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
-> [(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)
]
request :: String
-> [(String,String)]
-> RequestMethod
-> [Header]
-> String
-> 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
-> CouchMonad a
-> IO a
runCouchDBURI uri act = bracket
(createCouchConnFromURI uri)
closeCouchConn
(flip runCouchDBWith act)
runCouchDB :: String
-> Int
-> CouchMonad a
-> IO a
runCouchDB hostname port act = bracket
(createCouchConn hostname port)
closeCouchConn
(flip runCouchDBWith act)
runCouchDB' :: CouchMonad a -> IO a
runCouchDB' = runCouchDB "127.0.0.1" 5984
runCouchDBWith :: CouchConn -> CouchMonad a -> IO a
runCouchDBWith conn (CouchMonad f) = fmap fst $ f conn
createCouchConn :: String
-> Int
-> IO (CouchConn)
createCouchConn hostname port = createCouchAuthConn hostname port Nothing
createCouchAuthConn :: String
-> Int
-> Maybe Authority
-> 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)
createCouchConnFromURI :: URI
-> 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)
closeCouchConn :: CouchConn -> IO ()
closeCouchConn (CouchConn conn _ _ _ _ ) = readIORef conn >>= close