katt-0.2.0.3: katt-lib/src/Utils/Katt/Utils.hs
{-# Language OverloadedStrings #-}
--------------------------------------------------------------------
-- |
-- Module : Utils.Katt.Utils
--
-- Contains shared type declarations and various utility functions.
--
module
Utils.Katt.Utils
where
import Control.Error hiding (tryIO)
import qualified Control.Exception as E
import Control.Lens
import Control.Monad (liftM)
import Control.Monad.Trans (lift)
import qualified Control.Monad.State as S
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Monoid ((<>))
import qualified Network.Wreq as W
import qualified Network.Wreq.Session as WS
import System.Exit (exitFailure)
import System.IO (stderr)
-- | Configuration layer consisting of configuration state.
type ConfigEnvInternal m = S.StateT ConfigState m
-- | Configuration layer wrapped with error handling.
type ConfigEnv m = EitherT ErrorDesc (ConfigEnvInternal m)
-- | Submissions consist of a problem identifier and a set of file paths.
type Submission = (KattisProblem, [FilePath])
-- | Error description alias.
type ErrorDesc = B.ByteString
-- | Submissions are identified with an integer id.
type SubmissionId = Integer
--
-- | Problem sessions are identified with an integer id.
type ProblemSession = Integer
-- | Project-specific state consists of the problem name.
type ProjectState = (KattisProblem)
-- | Global configuration, initialized from the /.kattisrc/ file.
data ConfigState =
ConfigState {
-- | Username.
user :: B.ByteString,
-- | API token (hash).
apiKey :: B.ByteString,
-- | Host to be considered as service.
host :: B.ByteString,
-- | URL to login page, relative 'host'.
loginPage :: B.ByteString,
-- | URL to submit page, relative 'host'.
submitPage :: B.ByteString,
-- | Project-specific state, optionally loaded.
project :: Maybe ProjectState
}
deriving Show
-- | HTTP client session and the host path.
type Session = (WS.Session, B.ByteString)
-- | A Kattis problem.
data KattisProblem
-- | Problem ID, unique.
= ProblemId Integer
-- | Associated name of the problem.
| ProblemName B.ByteString
deriving (Eq, Show)
-- | Language used in submission.
data KattisLanguage
-- | C++.
= LangCplusplus
-- | Java.
| LangJava
-- | C.
| LangC
-- | Haskell.
| LangHaskell
deriving (Eq, Show)
-- | Server response indicating successful login.
loginSuccess :: B.ByteString
loginSuccess = "Login successful"
-- | Extension of input test files.
inputTestExtension :: FilePath
inputTestExtension = ".in"
-- | Extension of reference ouput test files.
outputTestExtension :: FilePath
outputTestExtension = ".ans"
-- | Name of this program.
programName :: B.ByteString
programName = "katt"
-- | Relative path to project-specific configuration directory.
configDir :: B.ByteString
configDir = "." <> programName
-- | Relative path to folder containing tests.
testFolder :: FilePath
testFolder = "tests"
-- | URL to page with problem information, relative to 'host'.
problemAddress :: B.ByteString
problemAddress = "/problems/"
-- | Lift some error monad one layer.
unWrapTrans :: (Monad m, S.MonadTrans t) => EitherT e m a -> EitherT e (t m) a
unWrapTrans = EitherT . lift . runEitherT
-- | Execute an IO action and catch any exceptions.
tryIO :: S.MonadIO m => IO a -> EitherT ErrorDesc m a
tryIO = EitherT . S.liftIO . liftM (fmapL (B.pack . show)) .
(E.try :: (IO a -> IO (Either E.SomeException a)))
-- | Execute an IO action and catch any exceptions, tagged with description.
tryIOMsg :: S.MonadIO m => B.ByteString -> IO a -> EitherT ErrorDesc m a
tryIOMsg msg = EitherT . S.liftIO . liftM (fmapL $ const msg) .
(E.try :: (IO a -> IO (Either E.SomeException a)))
-- | Evaluate an error action and terminate process upon failure.
terminateOnFailure :: S.MonadIO m => ErrorDesc -> EitherT ErrorDesc m a -> m a
terminateOnFailure msg state = do
res <- runEitherT state
S.liftIO $ case res of
Left errorMessage -> do
B.hPutStrLn stderr $ msg <> ", error: " <> errorMessage
exitFailure
Right success -> return success
-- | Default HTTP options.
defaultOpts :: W.Options
defaultOpts = W.defaults
& W.header "User-Agent" .~ [programName]
-- | Retrieve a publicly available page, using HTTP GET.
retrievePublicPage :: B.ByteString -> ConfigEnv IO B.ByteString
retrievePublicPage path = do
host' <- S.gets host
reply <- tryIO $ W.getWith defaultOpts $ buildURL host' path
return . B.concat . BL.toChunks $ reply ^. W.responseBody
-- | Retrieve a page requiring authentication, using HTTP GET.
retrievePrivatePage :: Session -> B.ByteString -> EitherT ErrorDesc IO B.ByteString
retrievePrivatePage (sess, host') page = do
reply <- tryIO $ WS.getWith defaultOpts sess (buildURL host' page)
return . B.concat . BL.toChunks $ reply ^. W.responseBody
-- | Construct URL from host path (e.g. /http:\/\/x.com\/) and path (e.g. //).
buildURL :: B.ByteString -> B.ByteString -> String
buildURL host' path = B.unpack $ host' <> "/" <> path
-- | Authenticate and run the provided action.
withAuth :: (WS.Session -> EitherT ErrorDesc IO a) -> ConfigEnv IO a
withAuth action = do
conf <- S.get
EitherT . S.liftIO . WS.withSession $ \sess -> runEitherT $ do
let formData = [("token" :: B.ByteString, apiKey conf),
("user", user conf),
("script", "true")]
url = buildURL (host conf) (loginPage conf)
reply <- tryIO $ WS.postWith defaultOpts sess url formData
let response = B.concat . BL.toChunks $ reply ^. W.responseBody
tryAssert ("Login failure. Server returned: '" <> response <> "'")
(response == loginSuccess)
action sess
-- | Retrieve problem ID of a Kattis problem.
retrieveProblemId :: KattisProblem -> IO Integer
retrieveProblemId (ProblemId id') = return id'
retrieveProblemId (ProblemName _) = undefined
-- | Retrieve problem name of a Kattis problem.
retrieveProblemName :: KattisProblem -> IO B.ByteString
retrieveProblemName (ProblemId _) = undefined
retrieveProblemName (ProblemName name) = return name