codeforces-cli-0.1.0: src/Codeforces/Response.hs
--------------------------------------------------------------------------------
module Codeforces.Response
( ResponseError(..)
, responseErrorMsg
, getData
, getAuthorizedData
) where
import Codeforces.Config
import Codeforces.Logging
import Control.Exception ( try )
import Data.Aeson
import Data.Maybe
import Network.HTTP.Client
import Network.HTTP.Simple
--------------------------------------------------------------------------------
data CodeforcesStatus = Ok | Failed
deriving Show
instance FromJSON CodeforcesStatus where
parseJSON = withText "CodeforcesStatus"
$ \t -> pure $ if t == "OK" then Ok else Failed
-- | Represents the result object or error comment returned by the API.
--
-- Each successful response from the API contains a "status" field, and either
-- a "result" or "comment" when status is "OK" or "FAILED" respectively.
--
-- These two possibilities are represented by @ResponseOk@ and @ResponseFail@.
--
data CodeforcesResponse a = ResponseFail String | ResponseOk a
deriving Show
instance FromJSON a => FromJSON (CodeforcesResponse a) where
parseJSON = withObject "CodeforcesResponse" $ \o -> do
st <- o .: "status"
case st of
Ok -> ResponseOk <$> o .: "result"
Failed -> ResponseFail <$> o .: "comment"
--------------------------------------------------------------------------------
-- | An error that could occur during the retrieval and inital parsing of data
-- from the Codeforces API.
data ResponseError
= ApiFail String
-- ^ Corresponds to a @ResponseFail@ from the Codeforces API with the fail
-- comment.
| JsonError JSONException
-- ^ Wrapper around 'JSONException', used if the successful JSON response
-- could not be parsed.
| HttpError HttpException
-- ^ Wrapper around 'HttpException', used for situations like a failed
-- connection.
deriving Show
-- | Converts a 'ResponseError' to a friendly error message to display, and
-- details that can be logged separately.
responseErrorMsg :: ResponseError -> ErrorLog
responseErrorMsg (ApiFail e) = mkErrorLog e
responseErrorMsg (JsonError e) = "Couldn't parse result" <~> show e
responseErrorMsg (HttpError e) = case e of
HttpExceptionRequest _ content -> case content of
StatusCodeException resp _ ->
mkErrorLog $ "HTTP " <> show (getResponseStatusCode resp)
TooManyRedirects resps -> "Too many redirects" <~> show resps
ConnectionTimeout -> mkErrorLog "Connection timeout"
ConnectionFailure e2 -> "Connection failure" <~> show e2
other -> "HTTP error" <~> show other
InvalidUrlException url reason ->
"Invalid URL" <~> (url <> "\nfor " <> reason)
--------------------------------------------------------------------------------
-- | Converts a possible 'CodeforcesResponse' into an either type.
codeforcesDecode
:: FromJSON a
=> Either JSONException (CodeforcesResponse a)
-> Either ResponseError a
codeforcesDecode (Left e1 ) = Left $ JsonError e1
codeforcesDecode (Right resp) = case resp of
(ResponseFail e2 ) -> Left $ ApiFail e2
(ResponseOk obj) -> Right obj
--------------------------------------------------------------------------------
-- | Host name and the API's base URL, without trailing slash.
baseUrl :: String
baseUrl = "https://codeforces.com/api"
-- | 'getData' @path query@ is a general function for returning some result data
-- from the Codeforces API.
getData :: FromJSON a => String -> Query -> IO (Either ResponseError a)
getData path query = do
req <- parseRequest (baseUrl ++ path)
let request = setRequestQueryString (catQuery query) req
eresponse <- try $ httpJSONEither request
pure $ case eresponse of
Left e -> Left $ HttpError e
Right resp -> codeforcesDecode $ getResponseBody resp
-- | 'getAuthorizedData' @config path query@ requests and returns some result
-- data that requires authorization.
getAuthorizedData
:: FromJSON a
=> UserConfig
-> String
-> Query
-> IO (Either ResponseError a)
getAuthorizedData cfg p q = generateRequestParams cfg p q >>= getData p
--------------------------------------------------------------------------------
-- | Takes a list of 'QueryItem's and returns those that have a @Just@ parameter
-- value.
--
-- By default, @Nothing@ items in 'Query' are parsed into "&a&b" format. The
-- Codeforces API seems to be inconsistent with how it interprets requests like
-- this. Most notably, the @contest.standings@ endpoint returns an empty list of
-- ranklist rows when @Nothing@ is passed, but all ranklist rows when completely
-- omitted.
--
catQuery :: Query -> Query
catQuery = filter (isJust . snd)
--------------------------------------------------------------------------------