{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables, DoAndIfThenElse #-} -- | Low-level method and tools of accessing CouchDB. module Database.CouchDB.Conduit.LowLevel ( CouchResponse, couch, couch', protect, protect' ) where import Prelude hiding (catch) import Control.Exception.Lifted (catch) import Control.Exception (SomeException) import Control.Monad.Trans.Class (lift) import Control.Monad.Base (liftBase) import Data.Maybe (fromJust) import qualified Data.ByteString as B import qualified Data.Aeson as A import qualified Data.ByteString.UTF8 as BU8 import qualified Data.HashMap.Lazy as M import qualified Data.Text as T import Data.Conduit (ResourceT, BufferedSource, ($$), resourceThrow) import Data.Conduit.Attoparsec (sinkParser) import qualified Network.HTTP.Conduit as H import qualified Network.HTTP.Types as HT import Database.CouchDB.Conduit -- | CouchDB response type CouchResponse m = H.Response (BufferedSource m B.ByteString) -- | The most general method of accessing CouchDB. This is a very thin wrapper -- around 'H.http'. Most of the time you should use one of the other access -- functions, but this function is needed for example to write and read -- attachments that are not in JSON format. couch :: MonadCouch m => HT.Method -- ^ Method -> (Path -> Path) -- ^ Path creation function -> HT.RequestHeaders -- ^ Headers -> HT.Query -- ^ Query args -> H.RequestBody m -- ^ Request body -> (CouchResponse m -> ResourceT m (CouchResponse m)) -- ^ Protect function. See 'protect' -> ResourceT m (CouchResponse m) couch meth pathFn hdrs qs reqBody protectFn = do conn <- lift couchConnection let req = H.def { H.method = meth , H.host = couchHost conn , H.requestHeaders = hdrs , H.port = couchPort conn , H.path = pathFn $ couchDB conn , H.queryString = HT.renderQuery False qs , H.requestBody = reqBody , H.checkStatus = const . const $ Nothing } -- Apply auth if needed let req' = if couchLogin conn == B.empty then req else H.applyBasicAuth (couchLogin conn) (couchPass conn) req res <- H.http req' (fromJust $ couchManager conn) protectFn res -- | Simplified version of 'couch'. This version uses standart path -- creation and protect functions. couch' :: MonadCouch m => HT.Method -- ^ Method -> Path -- ^ Path -> HT.RequestHeaders -- ^ Headers -> HT.Query -- ^ Query args -> H.RequestBody m -- ^ Request body -> ResourceT m (CouchResponse m) couch' meth p hdrs qs reqBody = couch meth (\dbP -> mkPath [dbP, p]) hdrs qs reqBody protect' -- | Protect 'H.Response' from bad status codes. If status code in list -- of status codes - just return response. Otherwise - throw 'CouchError'. -- -- Instead 'H.checkStatus', 'protect' parses CouchDB response body JSON and -- extract \"reason\" message. -- -- To protect from typical errors use 'protect''. protect :: MonadCouch m => [Int] -- ^ Good codes -> (CouchResponse m -> ResourceT m (CouchResponse m)) -- ^ handler -> CouchResponse m -- ^ Response -> ResourceT m (CouchResponse m) protect goodCodes h ~resp@(H.Response (HT.Status sc sm) _ bsrc) | sc `elem` goodCodes = h resp | otherwise = do v <- catch (bsrc $$ sinkParser A.json) (\(_::SomeException) -> return A.Null) liftBase $ resourceThrow $ CouchError (Just sc) $ msg v where msg v = BU8.toString sm ++ reason v reason (A.Object v) = case M.lookup "reason" v of Just (A.String t) -> ": " ++ T.unpack t _ -> "" reason _ = [] -- | Protect from typical status codes. It's equivalent of -- -- > protect [200, 201, 202, 304] return -- -- See 'protect' for details. protect' :: MonadCouch m => CouchResponse m -- ^ Response -> ResourceT m (CouchResponse m) protect' = protect [200, 201, 202, 304] return