1
0
Fork 0

[haskell] Refactored everything with a ReaderT pattern

This commit is contained in:
Julien Dessaux 2023-07-18 00:02:33 +02:00
parent 9f75243ada
commit 4af96da567
Signed by: adyxax
GPG key ID: F92E51B86E07177E
14 changed files with 213 additions and 119 deletions

View file

@ -3,19 +3,34 @@
module Main (main) where module Main (main) where
import Control.Exception import Control.Exception
import System.Environment
import System.Posix.Process
import SpaceTraders import SpaceTraders
import SpaceTraders.Automation.Init import SpaceTraders.Automation.Init
import SpaceTraders.APIClient.Agent(myAgent) import SpaceTraders.APIClient.Agent(myAgent)
import SpaceTraders.APIClient.Client
import SpaceTraders.APIClient.Ships
import SpaceTraders.APIClient.Systems import SpaceTraders.APIClient.Systems
main :: IO () main :: IO ()
main = do main = do
config <- initST env <- initST
ma <- runSpaceTradersT myAgent config ma <- runSpaceTradersT myAgent env
print ma case ma of
s <- listSystems (token config) (conn config) Left (APIResetHappened _) -> do
p <- getExecutablePath
a <- getArgs
e <- getEnvironment
executeFile p False a (Just e)
Left e -> throwIO e
Right ma' -> print ma'
s <- runSpaceTradersT listSystems env
case s of case s of
Left e -> throwIO e Left e -> throwIO e
Right s' -> print $ length s' Right s' -> print $ length s'
deinitST config ships <- runSpaceTradersT listShips env
case ships of
Left e -> throwIO e
Right s' -> print $ s'
deinitST env

View file

@ -22,11 +22,11 @@ dependencies:
- directory - directory
- http-conduit - http-conduit
- http-types - http-types
- mtl
- raw-strings-qq - raw-strings-qq
- sqlite-simple - sqlite-simple
- text - text
- time - time
- transformers
- unix - unix
ghc-options: ghc-options:

View file

@ -3,21 +3,36 @@
module SpaceTraders module SpaceTraders
( SpaceTradersT ( SpaceTradersT
, runSpaceTradersT , runSpaceTradersT
, Config(..) , Env(..)
, HasDatabaseConn
, HasRequest
, ask , ask
, getConn
, getRequest
, liftIO , liftIO
) where ) where
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Reader import Control.Monad.Reader
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import qualified Data.Text as T import Network.HTTP.Simple
type SpaceTradersT a = ReaderT Config IO a type SpaceTradersT a = ReaderT Env IO a
runSpaceTradersT :: SpaceTradersT a -> Config -> IO a runSpaceTradersT :: SpaceTradersT a -> Env -> IO a
runSpaceTradersT = runReaderT runSpaceTradersT = runReaderT
data Config = Config { conn :: S.Connection data Env = Env { envConn :: S.Connection
, token :: T.Text , envRequest :: Request }
}
class HasDatabaseConn a where
getConn :: a -> S.Connection
instance HasDatabaseConn S.Connection where
getConn = id
instance HasDatabaseConn Env where
getConn = envConn
class HasRequest a where
getRequest :: a -> Request
instance HasRequest Env where
getRequest = envRequest

View file

@ -8,22 +8,20 @@ module SpaceTraders.APIClient.Agent
, register , register
) where ) where
import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import GHC.Generics import GHC.Generics
import qualified Data.Text as T import qualified Data.Text as T
import Network.HTTP.Simple import Network.HTTP.Simple
import qualified SpaceTraders as ST import SpaceTraders
import SpaceTraders.APIClient.Client import SpaceTraders.APIClient.Client
import SpaceTraders.Model.Agent(Agent) import SpaceTraders.Model.Agent(Agent)
import SpaceTraders.Model.Ship(Ship) import SpaceTraders.Model.Ship(Ship)
import SpaceTraders.Model.Contract import SpaceTraders.Model.Contract
myAgent :: ST.SpaceTradersT (APIResponse Agent) myAgent :: SpaceTradersT (APIResponse Agent)
myAgent = do myAgent = send $ setRequestPath "/v2/my/agent"
c <- ST.ask
ST.liftIO $ send $ setRequestPath "/v2/my/agent"
$ tokenReq (ST.token c)
data RegisterRequest = RegisterRequest { faction :: T.Text data RegisterRequest = RegisterRequest { faction :: T.Text
, symbol :: T.Text , symbol :: T.Text
@ -34,8 +32,7 @@ data RegisterMessage = RegisterMessage { agent :: Agent
, token :: T.Text , token :: T.Text
} deriving (FromJSON, Generic, Show) } deriving (FromJSON, Generic, Show)
register :: T.Text -> T.Text -> IO (APIResponse RegisterMessage) register :: (HasRequest env, MonadIO m, MonadReader env m) => T.Text -> T.Text -> m (APIResponse RegisterMessage)
register s f = send $ setRequestPath "/v2/register" register s f = send $ setRequestPath "/v2/register"
$ setRequestMethod "POST" . setRequestMethod "POST"
$ setRequestBodyJSON RegisterRequest{symbol = s, faction = f} . setRequestBodyJSON RegisterRequest{symbol = s, faction = f}
$ defaultReq

View file

@ -8,13 +8,13 @@ module SpaceTraders.APIClient.Client
, APIResponse , APIResponse
, defaultReq , defaultReq
, fromJSONValue , fromJSONValue
, paginatedReq
, send , send
, sendPaginated , sendPaginated
, tokenReq , tokenReq
) where ) where
import Control.Concurrent import Control.Concurrent
import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import Data.Aeson.Types import Data.Aeson.Types
import qualified Data.ByteString as B import qualified Data.ByteString as B
@ -23,9 +23,8 @@ import qualified Data.Text as T
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import Network.HTTP.Simple import Network.HTTP.Simple
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import System.Environment
import System.Posix.Process
import SpaceTraders
import SpaceTraders.APIClient.Errors import SpaceTraders.APIClient.Errors
import SpaceTraders.APIClient.Pagination import SpaceTraders.APIClient.Pagination
@ -47,31 +46,35 @@ defaultReq = setRequestHost "api.spacetraders.io"
$ setRequestHeader "Content-Type" ["application/json"] $ setRequestHeader "Content-Type" ["application/json"]
$ defaultRequest $ defaultRequest
paginatedReq :: T.Text -> Maybe Pagination -> Request
paginatedReq token Nothing = setRequestQueryString [("limit", Just "20")]
$ tokenReq token
paginatedReq token (Just myPage) = setRequestQueryString [("limit", Just . int2ByteString $ limit myPage), ("page", Just . int2ByteString $ page myPage)]
$ tokenReq token
where
int2ByteString = B.pack . map B.c2w . show
tokenReq :: T.Text -> Request tokenReq :: T.Text -> Request
tokenReq token = setRequestHeader "Authorization" [T.encodeUtf8 $ "Bearer " <> token] tokenReq token = setRequestHeader "Authorization" [T.encodeUtf8 $ "Bearer " <> token] defaultReq
$ defaultReq
fromJSONValue :: FromJSON a => Value -> Either String a fromJSONValue :: FromJSON a => Value -> Either String a
fromJSONValue = parseEither parseJSON fromJSONValue = parseEither parseJSON
send :: FromJSON a => Request -> IO (APIResponse a) int2ByteString :: Int -> B.ByteString
send request = do int2ByteString = B.pack . map B.c2w . show
response <- sendPaginated request
send :: (FromJSON a, HasRequest env, MonadIO m, MonadReader env m) => (Request -> Request) -> m (APIResponse a)
send requestBuilder = do
response <- sendPaginated Nothing requestBuilder
case response of case response of
Left e -> return $ Left e Left e -> return $ Left e
Right (APIMessage d _) -> return $ Right d Right (APIMessage d _) -> return $ Right d
sendPaginated :: FromJSON a => Request -> IO (APIPaginatedResponse a) sendPaginated :: (FromJSON a, HasRequest env, MonadIO m, MonadReader env m) => Maybe Pagination -> (Request -> Request) -> m (APIPaginatedResponse a)
sendPaginated request = do sendPaginated pagination requestBuilder = do
response <- httpLbs request env <- ask
let request = requestBuilder $ getRequest env
request' = case pagination of
Just myPage -> setRequestQueryString [("limit", Just . int2ByteString $ limit myPage), ("page", Just . int2ByteString $ page myPage)]
$ request
Nothing -> request
sendPaginated' request'
where
sendPaginated' :: (FromJSON a, MonadIO m) => Request -> m (APIPaginatedResponse a)
sendPaginated' request = do
response <- liftIO $ httpLbs request
let status = statusCode $ getResponseStatus response let status = statusCode $ getResponseStatus response
body = getResponseBody response body = getResponseBody response
if status >= 200 && status <= 299 if status >= 200 && status <= 299
@ -81,11 +84,6 @@ sendPaginated request = do
else case eitherDecode body of else case eitherDecode body of
Left e -> return . Left $ APIError (-status) (T.pack $ concat ["Error decoding JSON APIError: ", e, ". Got HTTP body: ", show body]) Null Left e -> return . Left $ APIError (-status) (T.pack $ concat ["Error decoding JSON APIError: ", e, ". Got HTTP body: ", show body]) Null
Right (APIRateLimit r) -> do Right (APIRateLimit r) -> do
threadDelay (1_000_000 * (round $ retryAfter r)) liftIO $ threadDelay (1_000_000 * (round $ retryAfter r))
sendPaginated request sendPaginated' request
Right (APIResetHappened _) -> do
p <- getExecutablePath
a <- getArgs
e <- getEnvironment
executeFile p False a (Just e)
Right e -> return $ Left e Right e -> return $ Left e

View file

@ -0,0 +1,36 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module SpaceTraders.APIClient.Ships
( listShips
) where
import Control.Exception
--import qualified Data.Text as T
--import qualified Database.SQLite.Simple as S
import Network.HTTP.Simple
import SpaceTraders
import SpaceTraders.APIClient.Client
import SpaceTraders.APIClient.Pagination
--import SpaceTraders.Database.Ships
import SpaceTraders.Model.Ship(Ship)
import Debug.Trace
listShips :: SpaceTradersT (APIResponse [Ship])
listShips = do
listShips' Pagination{limit=20, page=1, total=0}
where
listShips' :: Pagination -> SpaceTradersT (APIResponse [Ship])
listShips' p = do
resp <- sendPaginated (Just p) $ setRequestPath "/v2/my/ships"
case resp of
Left e -> throw e
Right (APIMessage r (Just p')) -> do
liftIO $ traceIO $ show p'
--if (length r == 0 || ((page p') * (limit p')
--addShips conn r
--listShips' (nextPage p')
return $ Right r
_ -> undefined

View file

@ -7,28 +7,26 @@ module SpaceTraders.APIClient.Systems
) where ) where
import Control.Exception import Control.Exception
import qualified Data.Text as T
import qualified Database.SQLite.Simple as S
import Network.HTTP.Simple import Network.HTTP.Simple
import SpaceTraders
import SpaceTraders.APIClient.Client import SpaceTraders.APIClient.Client
import SpaceTraders.APIClient.Pagination import SpaceTraders.APIClient.Pagination
import SpaceTraders.Database.Systems import SpaceTraders.Database.Systems
import SpaceTraders.Model.System(System) import SpaceTraders.Model.System(System)
listSystems :: T.Text -> S.Connection -> IO (APIResponse [System]) listSystems :: SpaceTradersT (APIResponse [System])
listSystems t conn = do listSystems = do
s <- getSystems conn s <- getSystems
listSystems' Pagination{limit=20, page=((length s) `div` 20) + 1, total=0} listSystems' Pagination{limit=20, page=((length s) `div` 20) + 1, total=0}
where where
listSystems' :: Pagination -> IO (APIResponse [System]) listSystems' :: Pagination -> SpaceTradersT (APIResponse [System])
listSystems' p = do listSystems' p = do
resp <- sendPaginated $ setRequestPath "/v2/systems" resp <- sendPaginated (Just p) $ setRequestPath "/v2/systems"
$ paginatedReq t (Just p)
case resp of case resp of
Left e -> throwIO e Left e -> throw e
Right (APIMessage [] _) -> Right <$> getSystems conn Right (APIMessage [] _) -> Right <$> getSystems
Right (APIMessage r (Just p')) -> do Right (APIMessage r (Just p')) -> do
addSystems conn r addSystems r
listSystems' (nextPage p') listSystems' (nextPage p')
_ -> undefined _ -> undefined

View file

@ -6,53 +6,55 @@ module SpaceTraders.Automation.Init
) where ) where
import Control.Exception import Control.Exception
import Control.Monad.Reader
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import qualified Data.Text as T import qualified Data.Text as T
import System.Directory import System.Directory
import SpaceTraders import SpaceTraders
import qualified SpaceTraders.APIClient.Agent as STAA import SpaceTraders.APIClient.Agent
import SpaceTraders.APIClient.Errors import SpaceTraders.APIClient.Client
import SpaceTraders.Database import SpaceTraders.Database
import SpaceTraders.Database.Agents import SpaceTraders.Database.Agents
import SpaceTraders.Database.Contracts import SpaceTraders.Database.Contracts
import SpaceTraders.Database.Ships import SpaceTraders.Database.Ships
import SpaceTraders.Database.Tokens import SpaceTraders.Database.Tokens
deinitST :: Config -> IO () deinitST :: Env -> IO ()
deinitST config = do deinitST env = do
close $ conn config close $ getConn env
initST :: IO Config initST :: IO Env
initST = do initST = do
c <- open conn <- open
t <- getToken c `catch` handleNoToken c t <- runReaderT getToken conn `catch` handleNoToken conn
ma <- runSpaceTradersT STAA.myAgent (Config c t) let env = Env conn (tokenReq t)
ma <- runSpaceTradersT myAgent env
case ma of case ma of
Left (APIResetHappened _) -> wipe c Left (APIResetHappened _) -> wipe conn
Left e -> throwIO e Left e -> throwIO e
_ -> return $ Config c t _ -> return $ env
where where
handleNoToken :: S.Connection -> SomeException -> IO T.Text handleNoToken :: S.Connection -> SomeException -> IO T.Text
handleNoToken c _ = register c handleNoToken conn _ = runReaderT registerST (Env conn defaultReq)
register :: S.Connection -> IO (T.Text) registerST :: SpaceTradersT (T.Text)
register c = do registerST = do
r <- STAA.register "ADYXAX" "COSMIC" r <- register "ADYXAX" "COSMIC"
case r of case r of
Right r' -> do Right r' -> do
setAgent c $ STAA.agent r' addAgent $ agent r'
addContract c $ STAA.contract r' addContract $ contract r'
addShip c $ STAA.ship r' addShip $ ship r'
let t = STAA.token r' let t = token r'
setToken c $ t addToken t
return t return t
Left e' -> throwIO e' Left e' -> throw e'
wipe :: S.Connection -> IO Config wipe :: S.Connection -> IO Env
wipe c = do wipe c = do
close c close c
removeFile "spacetraders.db" removeFile "spacetraders.db"
conn' <- open conn' <- open
t <- register conn' t <- runReaderT registerST (Env conn' defaultReq)
return $ Config conn' t return $ Env conn' (tokenReq t)

View file

@ -1,13 +1,17 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module SpaceTraders.Database.Agents module SpaceTraders.Database.Agents
( setAgent ( addAgent
) where ) where
import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import SpaceTraders
import SpaceTraders.Model.Agent import SpaceTraders.Model.Agent
setAgent :: S.Connection -> Agent -> IO () addAgent :: (HasDatabaseConn env, MonadIO m, MonadReader env m) => Agent -> m ()
setAgent conn agent = S.execute conn "INSERT INTO agents(data) VALUES (json(?));" (S.Only (encode agent)) addAgent agent = do
env <- ask
liftIO $ S.execute (getConn env) "INSERT INTO agents(data) VALUES (json(?));" (S.Only (encode agent))

View file

@ -4,10 +4,14 @@ module SpaceTraders.Database.Contracts
( addContract ( addContract
) where ) where
import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import SpaceTraders
import SpaceTraders.Model.Contract import SpaceTraders.Model.Contract
addContract :: S.Connection -> Contract -> IO () addContract :: (HasDatabaseConn env, MonadIO m, MonadReader env m) => Contract -> m ()
addContract conn contract = S.execute conn "INSERT INTO contracts(data) VALUES (json(?));" (S.Only (encode contract)) addContract contract = do
env <- ask
liftIO $ S.execute (getConn env) "INSERT INTO contracts(data) VALUES (json(?));" (S.Only (encode contract))

View file

@ -4,10 +4,14 @@ module SpaceTraders.Database.Ships
( addShip ( addShip
) where ) where
import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import SpaceTraders
import SpaceTraders.Model.Ship import SpaceTraders.Model.Ship
addShip :: S.Connection -> Ship -> IO () addShip :: (HasDatabaseConn env, MonadIO m, MonadReader env m) => Ship -> m ()
addShip conn ship = S.execute conn "INSERT INTO ships(data) VALUES (json(?));" (S.Only (encode ship)) addShip ship = do
env <- ask
liftIO $ S.execute (getConn env) "INSERT INTO ships(data) VALUES (json(?));" (S.Only (encode ship))

View file

@ -5,19 +5,23 @@ module SpaceTraders.Database.Systems
, getSystems , getSystems
) where ) where
import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import Data.Maybe import Data.Maybe
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import Data.Text.Encoding (encodeUtf8Builder) import SpaceTraders
import Data.ByteString.Builder(toLazyByteString) import SpaceTraders.Database.Utils
import SpaceTraders.Model.System import SpaceTraders.Model.System
addSystems :: S.Connection -> [System] -> IO () addSystems :: (HasDatabaseConn env, MonadIO m, MonadReader env m) => [System] -> m ()
addSystems conn systems = S.withTransaction conn $ S.executeMany conn "INSERT INTO systems(data) VALUES (json(?));" $ S.Only <$> map encode systems addSystems systems = do
env <- ask
let conn = getConn env
liftIO $ S.withTransaction conn $ S.executeMany conn "INSERT INTO systems(data) VALUES (json(?));" $ S.Only <$> map encode systems
getSystems :: S.Connection -> IO [System] getSystems :: (HasDatabaseConn env, MonadIO m, MonadReader env m) => m [System]
getSystems conn = do getSystems = do
ret <- S.query_ conn "SELECT data from systems;" env <- ask
return . catMaybes $ map (decode . toLazyByteString . encodeUtf8Builder . head) ret ret <- liftIO $ S.query_ (getConn env) "SELECT data from systems;"
return . catMaybes $ map (decodeText . head) ret

View file

@ -2,17 +2,23 @@
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
module SpaceTraders.Database.Tokens module SpaceTraders.Database.Tokens
( getToken ( addToken
, setToken , getToken
) where ) where
import Control.Monad.Reader
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import qualified Data.Text as T import qualified Data.Text as T
getToken :: S.Connection -> IO (T.Text) import SpaceTraders
getToken conn = do
[[token]] <- S.query_ conn "SELECT data FROM tokens;"
return token
setToken :: S.Connection -> T.Text -> IO () addToken :: (HasDatabaseConn env, MonadIO m, MonadReader env m) => T.Text -> m ()
setToken conn value = S.execute conn "INSERT INTO tokens(data) VALUES (?);" (S.Only value) addToken value = do
env <- ask
liftIO $ S.execute (getConn env) "INSERT INTO tokens(data) VALUES (?);" (S.Only value)
getToken :: (HasDatabaseConn env, MonadFail m, MonadIO m, MonadReader env m) => m T.Text
getToken = do
env <- ask
[[token]] <- liftIO $ S.query_ (getConn env) "SELECT data FROM tokens;"
return token

View file

@ -0,0 +1,11 @@
module SpaceTraders.Database.Utils
( decodeText
) where
import Data.Aeson
import Data.ByteString.Builder(toLazyByteString)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8Builder)
decodeText :: FromJSON a => T.Text -> Maybe a
decodeText = decode . toLazyByteString . encodeUtf8Builder