{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-} ------------------------------------------------------------------------------ -- | -- Module : Pinboard.Client -- Copyright : (c) Jon Schoning, 2015 -- Maintainer : jonschoning@gmail.com -- Stability : experimental -- Portability : POSIX ------------------------------------------------------------------------------ module Pinboard.Client ( -- * Config fromApiToken -- | The PinboardConfig provides authentication via apiToken , PinboardConfig (..) -- * Monadic , runPinboard , pinboardJson -- * Single , runPinboardSingleRaw , runPinboardSingleRawBS , runPinboardSingleJson -- * Sending , sendPinboardRequest -- * Manager (http-client) , mgrOpenRaw , mgrOpen , mgrFail -- * JSON Handling ,parseJSONResponse ,decodeJSONResponse -- * Status Codes ,checkStatusCodeResponse ,checkStatusCode -- * Error Helpers ,addErrMsg ,createParserErr ,httpStatusPinboardError -- * Client Dependencies , module Pinboard.Client.Error , module Pinboard.Client.Types , module Pinboard.Client.Util ) where import Control.Exception (catch, SomeException, try, bracket) import Control.Monad.IO.Class (MonadIO (liftIO)) import Control.Monad.Reader (ask, runReaderT) import Control.Monad.Trans.Either (runEitherT, hoistEither) import Data.ByteString.Char8 (pack) import Data.Monoid ((<>)) import Data.Aeson (FromJSON, eitherDecodeStrict') import Network (withSocketsDo) import Network.HTTP.Types (urlEncode) import Network.HTTP.Types.Status (statusCode) import Network.HTTP.Client import Network.HTTP.Client.TLS import Pinboard.Client.Types import Pinboard.Client.Error import Pinboard.Client.Util import qualified Data.ByteString.Lazy as LBS import qualified Data.Text as T import qualified Data.Text.Encoding as T import Control.Applicative import Prelude -- | Create a default PinboardConfig using the supplied apiToken fromApiToken :: String -> PinboardConfig fromApiToken token = PinboardConfig { debug = False, apiToken = pack token } -------------------------------------------------------------------------------- -- | Execute computations in the Pinboard monad runPinboard :: PinboardConfig -> Pinboard a -> IO (Either PinboardError a) runPinboard config requests = bracket mgrOpen return (either (mgrFail ConnectionFailure) go) where go mgr = runReaderT (runEitherT requests) (config, mgr) `catch` mgrFail UnknownErrorType -- | Create a Pinboard value from a PinboardRequest w/ json deserialization pinboardJson :: FromJSON a => PinboardRequest -> Pinboard a pinboardJson req = do (config, mgr) <- ask result <- liftIO $ sendPinboardRequest (ensureResultFormatType FormatJson req) config mgr parseJSONResponse hoistEither result -------------------------------------------------------------------------------- runPinboardSingleRaw :: PinboardConfig -> PinboardRequest -> (Response LBS.ByteString -> a) -> IO (Either PinboardError a) runPinboardSingleRaw config req handler = bracket mgrOpen return (either (mgrFail ConnectionFailure) go) where go mgr = (Right <$> sendPinboardRequest req config mgr handler) `catch` mgrFail UnknownErrorType runPinboardSingleRawBS :: PinboardConfig -> PinboardRequest -> IO (Either PinboardError LBS.ByteString) runPinboardSingleRawBS config req = do res <- runPinboardSingleRaw config req id return $ do r <- res responseBody r <$ checkStatusCodeResponse r runPinboardSingleJson :: FromJSON a => PinboardConfig -> PinboardRequest -> IO (Either PinboardError a) runPinboardSingleJson config = runPinboard config . pinboardJson -------------------------------------------------------------------------------- sendPinboardRequest :: PinboardRequest -> PinboardConfig -> Manager -> (Response LBS.ByteString -> a) -> IO a sendPinboardRequest PinboardRequest{..} PinboardConfig{..} man handler = do let url = T.concat [ requestPath , "?" , T.decodeUtf8 $ paramsToByteString $ ("auth_token", urlEncode False apiToken) : encodeParams requestParams ] req <- buildReq $ T.unpack url res <- httpLbs req man return $ handler res -------------------------------------------------------------------------------- buildReq :: String -> IO Request buildReq url = do req <- parseUrl $ "https://api.pinboard.in/v1/" <> url return $ req { requestHeaders = [("User-Agent","pinboard.hs/0.7.5")] , checkStatus = \_ _ _ -> Nothing } -------------------------------------------------------------------------------- parseJSONResponse :: (FromJSON a) => Response LBS.ByteString -> Either PinboardError a parseJSONResponse response = either (Left . addErrMsg (toText (responseBody response))) (const $ decodeJSONResponse (responseBody response)) (checkStatusCodeResponse response) decodeJSONResponse :: FromJSON a => LBS.ByteString -> Either PinboardError a decodeJSONResponse s = let r = eitherDecodeStrict' (LBS.toStrict s) in either (Left . createParserErr . toText) Right r -------------------------------------------------------------------------------- checkStatusCodeResponse :: Response a -> Either PinboardError () checkStatusCodeResponse = checkStatusCode . statusCode . responseStatus checkStatusCode :: Int -> Either PinboardError () checkStatusCode = \case 200 -> Right () 400 -> httpStatusPinboardError BadRequest 401 -> httpStatusPinboardError UnAuthorized 402 -> httpStatusPinboardError RequestFailed 403 -> httpStatusPinboardError Forbidden 404 -> httpStatusPinboardError NotFound 429 -> httpStatusPinboardError TooManyRequests c | c >= 500 -> httpStatusPinboardError PinboardServerError _ -> httpStatusPinboardError UnknownHTTPCode -------------------------------------------------------------------------------- httpStatusPinboardError :: PinboardErrorHTTPCode -> Either PinboardError a httpStatusPinboardError err = Left $ defaultPinboardError { errorType = HttpStatusFailure , errorHTTP = Just err } addErrMsg :: T.Text -> PinboardError -> PinboardError addErrMsg msg err = err {errorMsg = msg} createParserErr :: T.Text -> PinboardError createParserErr msg = PinboardError ParseFailure msg Nothing Nothing Nothing -------------------------------------------------------------------------------- mgrOpenRaw :: IO Manager mgrOpenRaw = withSocketsDo . newManager $ managerSetProxy (proxyEnvironment Nothing) tlsManagerSettings mgrOpen :: IO (Either SomeException Manager) mgrOpen = try mgrOpenRaw mgrFail :: PinboardErrorType -> SomeException -> IO (Either PinboardError b) mgrFail e msg = return $ Left $ PinboardError e (toText msg) Nothing Nothing Nothing