{-# LANGUAGE NoImplicitPrelude #-} {- | Module : Database.Couch.Internal Description : The lowest low-level code for Database.Couch Copyright : Copyright (c) 2015, Michael Alan Dorman License : MIT Maintainer : mdorman@jaunder.io Stability : experimental Portability : POSIX This module is about things that are at such a low level, they're not even necessarily really CouchDB-specific. -} module Database.Couch.Internal where import Control.Monad (return, (>>=)) import Control.Monad.Catch (handle) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Aeson (FromJSON, Value (Null)) import Data.Aeson.Parser (json) import Data.Attoparsec.ByteString (IResult (Done, Fail, Partial), parseWith) import Data.Either (Either (Right, Left), either) import Data.Eq ((==)) import Data.Function (const, flip, ($), (.)) import Data.Maybe (Maybe (Just, Nothing)) import Data.Monoid (mempty) import Data.Text (pack) import Database.Couch.RequestBuilder (RequestBuilder, runBuilder) import Database.Couch.ResponseParser (ResponseParser, runParse, standardParse) import Database.Couch.Types (Context, Error (HttpError, ParseFail, ParseIncomplete), Result, ctxCookies, ctxManager) import Network.HTTP.Client (CookieJar, Manager, Request, brRead, checkStatus, method, responseBody, responseCookieJar, responseHeaders, responseStatus, withResponse) import Network.HTTP.Types (ResponseHeaders, Status, methodHead) {- | Make an HTTP request returning a JSON value This is our lowest-level non-streaming routine. It only handles performing the request and parsing the result into a JSON value. It presumes: * we will be receiving a deserializable JSON value * we do not need to stream out the result (though the input is parsed incrementally) The results of parsing the stream will be handed to a routine that take the output and return the value the user ultimately desires. We use "Data.Either" to handle indicating failure and such. Basing the rest of our library on a function where all dependencies are explicit should help make sure that other bits remain portable to, say, streaming interfaces. -} rawJsonRequest :: MonadIO m => Manager -- ^ The "Network.HTTP.Client.Manager" to use for the request -> Request -- ^ The actual request itself -> m (Either Error (ResponseHeaders, Status, CookieJar, Value)) rawJsonRequest manager request = liftIO (handle errorHandler $ withResponse request { checkStatus = const . const . const Nothing } manager responseHandler) where -- Simply convert any exception into an HttpError errorHandler = return . Left . HttpError -- Incrementally parse the body, reporting failures. responseHandler res = do result <- if method request == methodHead then return (Done mempty Null) else parseParts res return $ case result of (Done _ ret) -> return (responseHeaders res, responseStatus res, responseCookieJar res, ret) (Partial _) -> Left ParseIncomplete (Fail _ _ err) -> Left $ ParseFail $ pack err parseParts res = do let input = brRead (responseBody res) initial <- input parseWith input json initial {- | Higher-level wrapper around 'rawJsonRequest' Building on top of 'rawJsonRequest, this routine is designed to take a builder for the request and a parser for the result, and use them to request our transaction. This makes for a very declarative style when defining individual endpoints for CouchDB. In order to support more sophisticated forms of authentication than 'Basic', we do have to examine the cookie jar returned from the server, and perhaps tell the user that they should replace the cookie jar in their context with it. -} structureRequest :: MonadIO m => RequestBuilder () -- ^ The builder for the HTTP request -> ResponseParser a -- ^ A parser for the data type the requester seeks -> Context -- ^ A context for holding the HTTP manager and the cookie jar -> m (Result a) structureRequest builder parse context = rawJsonRequest manager request >>= parser where manager = ctxManager context request = runBuilder builder context parser = return . either Left parseContext parseContext (h, s, c, v) = runParse parse (Right (h, s, v)) >>= checkContextUpdate c checkContextUpdate c a = Right (a, if c == ctxCookies context then Nothing else Just c) {- | Make a HTTP request with standard CouchDB semantics This builds on 'structureRequest', with a standard parser for the response. -} standardRequest :: (FromJSON a, MonadIO m) => RequestBuilder () -> Context -> m (Result a) standardRequest = flip structureRequest standardParse