From a775330b4fa17fb367e55343e6fda6c7ae9e34b4 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Sun, 2 Jul 2023 22:58:30 +0200 Subject: Bootstrapped my haskell client --- haskell/src/SpaceTraders/APIClient/Agent.hs | 34 ++++++++++++++ haskell/src/SpaceTraders/APIClient/Client.hs | 69 ++++++++++++++++++++++++++++ haskell/src/SpaceTraders/Database.hs | 56 ++++++++++++++++++++++ haskell/src/SpaceTraders/Database/Agents.hs | 14 ++++++ haskell/src/SpaceTraders/Database/Tokens.hs | 18 ++++++++ haskell/src/SpaceTraders/Model/Agent.hs | 22 +++++++++ 6 files changed, 213 insertions(+) create mode 100644 haskell/src/SpaceTraders/APIClient/Agent.hs create mode 100644 haskell/src/SpaceTraders/APIClient/Client.hs create mode 100644 haskell/src/SpaceTraders/Database.hs create mode 100644 haskell/src/SpaceTraders/Database/Agents.hs create mode 100644 haskell/src/SpaceTraders/Database/Tokens.hs create mode 100644 haskell/src/SpaceTraders/Model/Agent.hs (limited to 'haskell/src/SpaceTraders') diff --git a/haskell/src/SpaceTraders/APIClient/Agent.hs b/haskell/src/SpaceTraders/APIClient/Agent.hs new file mode 100644 index 0000000..997d1e5 --- /dev/null +++ b/haskell/src/SpaceTraders/APIClient/Agent.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} + +module SpaceTraders.APIClient.Agent + ( RegisterMessage(..) + , myAgent + , register + ) where + +import Data.Aeson +import GHC.Generics +import qualified Data.Text as T +import Network.HTTP.Simple + +import SpaceTraders.APIClient.Client +import SpaceTraders.Model.Agent + +myAgent :: T.Text -> IO (Either APIError Agent) +myAgent t = send $ setRequestPath "/v2/my/agent" + $ tokenReq t + +data RegisterRequest = RegisterRequest { symbol :: T.Text + , faction :: T.Text + } deriving (ToJSON, Generic, Show) +data RegisterMessage = RegisterMessage { token :: T.Text + , agent :: Agent + } deriving (FromJSON, Generic, Show) + +register :: T.Text -> T.Text -> IO (Either APIError RegisterMessage) +register s f = send $ setRequestPath "/v2/register" + $ setRequestMethod "POST" + $ setRequestBodyJSON RegisterRequest{symbol = s, faction = f} + $ defaultReq diff --git a/haskell/src/SpaceTraders/APIClient/Client.hs b/haskell/src/SpaceTraders/APIClient/Client.hs new file mode 100644 index 0000000..e4744aa --- /dev/null +++ b/haskell/src/SpaceTraders/APIClient/Client.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE OverloadedStrings #-} + +module SpaceTraders.APIClient.Client + ( APIError(..) + , APIMessage(..) + , defaultReq + , fromJSONValue + , send + , tokenReq + ) where + +import Control.Exception +import Control.Monad +import Data.Aeson +import Data.Aeson.Types +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import Network.HTTP.Simple +import Network.HTTP.Types.Status + +data APIError = APIError { apiErrorCode :: Int + , apiErrorMessage :: T.Text + } deriving Show +instance Exception APIError +instance FromJSON APIError where + parseJSON (Object o) = do + e <- o .: "error" + APIError <$> e .: "code" + <*> e .: "message" + parseJSON _ = mzero + +data APIMessage = APIMessage { data_ :: Value } deriving (Show) +instance FromJSON APIMessage where + parseJSON (Object o) = APIMessage <$> o .: "data" + parseJSON _ = mzero + +defaultReq :: Request +defaultReq = setRequestHost "api.spacetraders.io" + $ setRequestPort 443 + $ setRequestSecure True + $ setRequestHeader "Content-Type" ["application/json"] + $ defaultRequest + +tokenReq :: T.Text -> Request +tokenReq token = setRequestHeader "Authorization" [T.encodeUtf8 $ "Bearer " <> token] + $ defaultReq + +fromJSONValue :: FromJSON a => Value -> Either String a +fromJSONValue = parseEither parseJSON + +send :: FromJSON a => Request -> IO (Either APIError a) +send request = do + response <- httpLbs request + let status = statusCode $ getResponseStatus response + body = getResponseBody response + if status >= 200 && status <= 299 + then case eitherDecode body of + Left e -> return $ Left APIError{apiErrorCode = -1000, apiErrorMessage = T.pack $ concat ["Error decoding JSON APIMessage: ", e]} + Right r -> case fromJSONValue (data_ r) of + Left e -> return $ Left APIError{apiErrorCode = -1001, apiErrorMessage = T.pack $ concat ["Error decoding JSON message contents: ", e]} + Right m -> return $ Right m + else case eitherDecode body of + Left e -> return $ Left APIError{apiErrorCode = -status, apiErrorMessage = T.pack $ concat ["Error decoding JSON APIError: ", e, ". Got HTTP body: ", show body]} + Right e -> return $ Left e + +--handleAPIError :: SomeException -> IO (Maybe RegisterMessage) +--handleAPIError e = do +-- print e +-- return Nothing diff --git a/haskell/src/SpaceTraders/Database.hs b/haskell/src/SpaceTraders/Database.hs new file mode 100644 index 0000000..fdc32d3 --- /dev/null +++ b/haskell/src/SpaceTraders/Database.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module SpaceTraders.Database + ( close + , open + ) where + +import Control.Exception +import qualified Database.SQLite.Simple as S +import Text.RawString.QQ + +migrations :: [S.Query] +migrations = [ + [r|CREATE TABLE schema_version ( + version INTEGER NOT NULL + ); + |], + [r|CREATE TABLE tokens ( + id INTEGER PRIMARY KEY, + data TEXT NOT NULL + ); + |], + [r|CREATE TABLE agents ( + id INTEGER PRIMARY KEY, + data TEXT NOT NULL + ); + |], + [r|CREATE TABLE contracts ( + id INTEGER PRIMARY KEY, + data TEXT NOT NULL + ); + |]] + +close :: S.Connection -> IO () +close conn = S.close conn + +open :: IO S.Connection +open = do + conn <- S.open "spacetraders.db" + S.execute_ conn "PRAGMA foreign_keys = ON;" + S.execute_ conn "PRAGMA journal_mode = WAL;" + S.withTransaction conn $ do + version <- getSchemaVersion conn `catch` defaultVersion + mapM_ (S.execute_ conn) $ drop version migrations + S.execute_ conn "DELETE FROM schema_version;" + S.execute conn "INSERT INTO schema_version (version) VALUES (?);" (S.Only $ length migrations) + return conn + +getSchemaVersion :: S.Connection -> IO Int +getSchemaVersion conn = do + [[v]] <- S.query_ conn "SELECT version FROM schema_version;" + return v + +defaultVersion :: SomeException -> IO Int +defaultVersion _ = return 0 diff --git a/haskell/src/SpaceTraders/Database/Agents.hs b/haskell/src/SpaceTraders/Database/Agents.hs new file mode 100644 index 0000000..5be7389 --- /dev/null +++ b/haskell/src/SpaceTraders/Database/Agents.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module SpaceTraders.Database.Agents + ( setAgent + ) where + +import Data.Aeson +import qualified Database.SQLite.Simple as S + +import SpaceTraders.Model.Agent + +setAgent :: S.Connection -> Agent -> IO () +setAgent conn agent = S.execute conn "INSERT INTO agents(data) VALUES (json(?));" (S.Only (encode agent)) diff --git a/haskell/src/SpaceTraders/Database/Tokens.hs b/haskell/src/SpaceTraders/Database/Tokens.hs new file mode 100644 index 0000000..b907609 --- /dev/null +++ b/haskell/src/SpaceTraders/Database/Tokens.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module SpaceTraders.Database.Tokens + ( getToken + , setToken + ) where + +import qualified Database.SQLite.Simple as S +import qualified Data.Text as T + +getToken :: S.Connection -> IO (T.Text) +getToken conn = do + [[token]] <- S.query_ conn "SELECT data FROM tokens;" + return token + +setToken :: S.Connection -> T.Text -> IO () +setToken conn value = S.execute conn "INSERT INTO tokens(data) VALUES (?);" (S.Only value) diff --git a/haskell/src/SpaceTraders/Model/Agent.hs b/haskell/src/SpaceTraders/Model/Agent.hs new file mode 100644 index 0000000..be97ac4 --- /dev/null +++ b/haskell/src/SpaceTraders/Model/Agent.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} + +module SpaceTraders.Model.Agent + ( Agent(accountId, credits, headquarters) + , agentSymbol + ) where + +import Data.Aeson +import GHC.Generics +import qualified Data.Text as T + +data Agent = Agent { accountId :: T.Text + , credits :: Integer + , headquarters :: T.Text + , startingFaction :: T.Text + , symbol :: T.Text + } deriving (FromJSON, Generic, Show, ToJSON) + +agentSymbol :: Agent -> T.Text +agentSymbol = symbol -- cgit v1.2.3