{-# 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