couch-simple-0.0.1.0: src/lib/Database/Couch/RequestBuilder.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Database.Couch.RequestBuilder
Description : Routines for creating the Request to send to CouchDB
Copyright : Copyright (c) 2015, Michael Alan Dorman
License : MIT
Maintainer : mdorman@jaunder.io
Stability : experimental
Portability : POSIX
-}
module Database.Couch.RequestBuilder where
import Control.Monad (return, (>>))
import Control.Monad.Reader (Reader, ask, runReader)
import Control.Monad.State (StateT, execStateT, get, put)
import Data.Aeson (ToJSON, encode)
import Data.ByteString (ByteString, intercalate, null)
import Data.Default (def)
import Data.Eq ((==))
import Data.Function (on, ($), (.))
import Data.List (unionBy)
import Data.Maybe (Maybe (Just), maybe)
import Data.Monoid (mempty, (<>))
import Data.Tuple (fst)
import Database.Couch.Types (Context, Credentials (Basic), DocId,
DocRev, ctxCookies, ctxCred, reqDb,
reqDocId, reqDocRev, reqHost,
reqPassword, reqPort, reqUser)
import Network.HTTP.Client (Request, RequestBody (RequestBodyLBS),
applyBasicAuth, cookieJar, host, method,
path, port, requestBody, requestHeaders,
setQueryString)
import Network.HTTP.Types (RequestHeaders, hAccept, hContentType)
-- | The state of our request as it's being built
data BuilderState =
BuilderState {
-- FIXME: Does it make more sense to hold all the individual
-- pieces explicitly, and then just have a BuilderState -> Request
-- function?
-- | The base request being built
bsRequest :: Request,
-- | The request itself only stores the 'queryString', so we accumulate pairs during construction, and use them to set the query string at the end.
bsQueryParam :: [(ByteString, Maybe ByteString)],
-- | If this is set, it will be prepended to the path.
bsDb :: ByteString,
-- | Again, stored this way for ease of manipulation, then properly assembled at the end.
bsPathSegments :: [ByteString]
}
-- | A type synonym for our builder monad
type RequestBuilder = StateT BuilderState (Reader Context)
-- | Given a 'Context', run our monadic builder function to produce a 'Request'.
runBuilder :: RequestBuilder () -> Context -> Request
runBuilder builder context =
finalize (runReader (execStateT (defaultRequest >> builder) (BuilderState def [] mempty [])) context)
-- | This actually takes the 'BuilderState' and does the assembly of the various state bits into a single 'Request'.
finalize :: BuilderState -> Request
finalize (BuilderState r q d p) =
setQueryString q r { path = calculatedPath }
where
calculatedPath = "/" <> intercalate "/" ((if null d then [] else [d]) <> p)
{- | The default set of modifications applied to the request.
* The host/port connection information is set
* The 'Accept' header is set to 'application/json'
* The 'Content-Type' headers is set to 'application/json'
* Any authentication session in the cookie jar is set
* Any Basic Authentication information is applied
Any or all of these may be overridden, but probably shouldn't be.
-}
defaultRequest :: RequestBuilder ()
defaultRequest = do
defaultHeaders [(hAccept, "application/json"), (hContentType, "application/json")]
setAuth
setConnection
setCookieJar
setMethod "GET"
-- * Applying 'Context' to the 'Request'
-- | Choose the database for the 'Request', based on what's in the 'Context'. This is the one thing that could arguably throw an error.
selectDb :: RequestBuilder ()
selectDb = do
c <- ask
(BuilderState r q _ p) <- get
put $ BuilderState r q (reqDb c) p
-- | Set the appropriate authentication markers on the 'Request', based on what's in the 'Context'.
setAuth :: RequestBuilder ()
setAuth = do
c <- ask
maybe (return ()) doApply (ctxCred c)
where
doApply cred = do
(BuilderState r q d p) <- get
put $ BuilderState (applyCred cred r) q d p
applyCred (Basic u p) = applyBasicAuth (reqUser u) (reqPassword p)
-- | Set the host and port for the 'Request', based on what's in the 'Context'.
setConnection :: RequestBuilder ()
setConnection = do
c <- ask
(BuilderState r q d p) <- get
put $ BuilderState r { host = reqHost c, port = reqPort c } q d p
-- | Set the 'CookieJar' for the 'Request', based on what's in the 'Context'.
setCookieJar :: RequestBuilder ()
setCookieJar = do
c <- ask
(BuilderState r q d p) <- get
put $ BuilderState r { cookieJar = Just $ ctxCookies c } q d p
-- * Setting Headers
-- | Add headers to a 'Request', leaving existing instances undisturbed.
addHeaders :: RequestHeaders -> RequestBuilder ()
addHeaders new = do
(BuilderState r q d p) <- get
let headers = requestHeaders r
put $ BuilderState r { requestHeaders = headers <> new } q d p
-- | Add headers to a 'Request', if they aren't already present.
defaultHeaders :: RequestHeaders -> RequestBuilder ()
defaultHeaders new = do
(BuilderState r q d p) <- get
let headers = requestHeaders r
put $ BuilderState r { requestHeaders = unionBy ((==) `on` fst) headers new } q d p
-- | Set headers on the 'Request', overriding any existing instances.
setHeaders :: RequestHeaders -> RequestBuilder ()
setHeaders new = do
(BuilderState r q d p) <- get
let headers = requestHeaders r
put $ BuilderState r { requestHeaders = unionBy ((==) `on` fst) new headers } q d p
-- * Setting Query Parameters
-- | Add query parameters to a 'Request', leaving existing parameters undisturbed.
addQueryParam :: [(ByteString, Maybe ByteString)] -> RequestBuilder ()
addQueryParam new = do
(BuilderState r q d p) <- get
put $ BuilderState r (q <> new) d p
-- | Add query parameters to a 'Request', if they aren't already present
defaultQueryParam :: [(ByteString, Maybe ByteString)] -> RequestBuilder ()
defaultQueryParam new = do
(BuilderState r q d p) <- get
put $ BuilderState r (unionBy ((==) `on` fst) q new) d p
-- | Set query parameters on the 'Request', overriding any existing instances.
setQueryParam :: [(ByteString, Maybe ByteString)] -> RequestBuilder ()
setQueryParam new = do
(BuilderState r q d p) <- get
put $ BuilderState r (unionBy ((==) `on` fst) new q) d p
-- * Setting the document path
-- | Add a path segment to the 'Request'. This is only appropriate for static paths.
addPath :: ByteString -> RequestBuilder ()
addPath new = do
(BuilderState r q d p) <- get
put $ BuilderState r q d (p <> [new])
-- | Add a path segment to the 'Request', given a 'DocId'.
selectDoc :: DocId -> RequestBuilder ()
selectDoc = addPath . reqDocId
-- * Handling optional revision information
-- | Set the rev for the 'Request'.
addRev :: DocRev -> RequestBuilder ()
addRev rev =
setHeaders [("ETag", reqDocRev rev)]
-- | Set the rev for the 'Request' if you have it.
maybeAddRev :: Maybe DocRev -> RequestBuilder ()
maybeAddRev =
maybe (return ()) addRev
-- * Miscellaneous request options
-- | Set the body of the request to the encoded JSON value
setJsonBody :: ToJSON a
=> a -- ^ The document content
-> RequestBuilder ()
setJsonBody new = do
(BuilderState r q d p) <- get
put $ BuilderState r { requestBody = RequestBodyLBS $ encode new } q d p
-- | Set the method for the 'Request'.
setMethod :: ByteString -> RequestBuilder ()
setMethod m = do
(BuilderState r q d p) <- get
put $ BuilderState r { method = m } q d p