couch-simple-0.0.1.0: src/lib/Database/Couch/ResponseParser.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{- |
Module : Database.Couch.ResponseParser
Description : Code for parsing responses from Database.Couch.External
Copyright : Copyright (c) 2015, Michael Alan Dorman
License : MIT
Maintainer : mdorman@jaunder.io
Stability : experimental
Portability : POSIX
These relatively simple combinators can do simple extractions of data from the data returned by "Database.Couch.External" routines, as well as checking certain information about the actual response values.
-}
module Database.Couch.ResponseParser where
import Control.Monad (return, (>>=))
import Control.Monad.Reader (Reader, asks, runReader)
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
import Data.Aeson (FromJSON, Result (Error, Success),
Value (Object), fromJSON)
import Data.ByteString (ByteString)
import Data.Either (Either (Left, Right), either)
import Data.Eq ((==))
import Data.Foldable (find)
import Data.Function (($), (.))
import Data.Functor (fmap)
import Data.HashMap.Strict (lookup)
import Data.Maybe (Maybe, maybe)
import Data.Monoid (mempty)
import Data.Text (Text, pack)
import Data.Text.Encoding (decodeUtf8)
import Data.Text.Read (decimal)
import Data.Tuple (fst, snd)
import Database.Couch.Types (DocRev (DocRev), Error (AlreadyExists, Conflict, HttpError, ImplementationError, InvalidName, NotFound, ParseFail, Unauthorized))
import GHC.Integer (Integer)
import Network.HTTP.Client (HttpException (StatusCodeException))
import Network.HTTP.Types (HeaderName, ResponseHeaders,
Status, statusCode)
-- * Our primary interface
-- | Check the status code for a successful value and tries to decode to the user's desired type if so
standardParse :: FromJSON a => ResponseParser a
standardParse = do
checkStatusCode
responseValue >>= toOutputType
-- * Lower-level interfaces
-- | A type synonym for the Monad we're operating in
type ResponseParser = ExceptT Error (Reader (ResponseHeaders, Status, Value))
-- | Run a given parser over an initial value
runParse :: ResponseParser a -> Either Error (ResponseHeaders, Status, Value) -> Either Error a
runParse p (Right v) = (runReader . runExceptT) p v
runParse _ (Left v) = Left v
-- | Extract the response status from the Monad
responseStatus :: ResponseParser Status
responseStatus =
asks status
where
status (_, s, _) = s
-- | Extract the response headers from the Monad
responseHeaders :: ResponseParser ResponseHeaders
responseHeaders =
asks headers
where
headers (h, _, _) = h
-- | Extract the response value from the Monad
responseValue :: ResponseParser Value
responseValue =
asks value
where
value (_, _, v) = v
-- | Check the status code for the response
checkStatusCode :: ResponseParser ()
checkStatusCode = do
h <- responseHeaders
s <- responseStatus
case statusCode s of
200 -> return ()
201 -> return ()
202 -> return ()
304 -> return ()
400 -> do
error <- getKey "reason" >>= toOutputType
throwE $ InvalidName error
401 -> throwE Unauthorized
404 -> throwE NotFound
409 -> throwE Conflict
412 -> throwE AlreadyExists
415 -> throwE $ ImplementationError "The server says we sent a bad content type, which shouldn't happen. Please open an issue at https://github.com/mdorman/couch-simple/issues with a test case if possible."
_ -> throwE $ HttpError (StatusCodeException s h mempty)
-- | Try to retrieve a header from the response
maybeGetHeader :: HeaderName -> ResponseParser (Maybe ByteString)
maybeGetHeader header = do
h <- responseHeaders
return $ fmap snd (find ((== header) . fst) h)
-- | Retrieve a header from the response, or return an error if it's not present
getHeader :: HeaderName -> ResponseParser ByteString
getHeader header =
maybeGetHeader header >>= maybe (throwE NotFound) return
-- | Decode the Content-Length header from the response, or return an error if it's not present
getContentLength :: ResponseParser Integer
getContentLength = do
h <- getHeader "Content-Length"
either (throwE . ParseFail . pack) (return . fst) $ decimal (decodeUtf8 h)
-- | Get the document revision (ETag header), or return an error if it's not present
getDocRev :: ResponseParser DocRev
getDocRev = do
h <- getHeader "ETag"
return $ DocRev $ decodeUtf8 h
-- | Get the value of a particular key from the response value, or return an error if it's not found
getKey :: Text -> ResponseParser Value
getKey key = do
v <- responseValue
case v of
Object o -> maybe (throwE NotFound) return $ lookup key o
_ -> throwE NotFound
-- | Decode the response value to a particular type, or return an error if it can't be decoded
toOutputType :: (FromJSON a) => Value -> ResponseParser a
toOutputType v =
case fromJSON v of
Error e -> throwE $ ParseFail $ pack e
Success a -> return a