Files
haskell/exercises/Set14b.hs
T

278 lines
10 KiB
Haskell

module Set14b where
-- In this exercise set, we're going to implement an HTTP API for a
-- simple bank. The user should be able to deposit money, withdraw
-- money and check an accounts balance over HTTP. The balances
-- themselves will be stored in an SQLite database.
--
-- It's a good idea to study Examples/Phonebook.hs and
-- Examples/PathServer.hs before jumping into this exercise set.
--
-- Let's start with some imports:
import Mooc.Todo
-- Utilities
import qualified Data.ByteString.Lazy as LB
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Read as TR
import Data.Text.Encoding (encodeUtf8)
import Text.Read (readMaybe)
-- HTTP server
import Network.Wai (pathInfo, responseLBS, Application)
import Network.Wai.Handler.Warp (run)
import Network.HTTP.Types (status200)
-- Database
import Database.SQLite.Simple (open,execute,execute_,query,query_,Connection,Query(..))
------------------------------------------------------------------------------
-- Ex 1: Let's start with implementing some database operations. The
-- database will contain one table, called events, with two columns:
-- account (a string) and amount (a number).
--
-- The database will not be storing the balances of the accounts, but
-- instead a _transaction log_: each withdrawal and deposit will be
-- its own row. The balance of the account can then be computed from
-- these.
--
-- Below, you'll find three queries:
-- * initQuery creates the database
-- * depositQuery adds an (account, amount) row into the database
-- * getAllQuery gets all (account, amount) pairs from the database.
-- getAllQuery isn't needed for the implementation, but you can use it
-- to test your answer.
--
-- Your task is to implement the IO operations openDatabase and deposit.
-- See below for their details.
--
-- Tip: creating a database with the filename "" will create a
-- temporary database that won't get saved to disk. Useful for
-- testing!
--
-- Example in GHCi:
-- Set14b> db <- openDatabase ""
-- Set14b> deposit db (T.pack "xxx") 13
-- Set14b> deposit db (T.pack "yyy") 5
-- Set14b> deposit db (T.pack "xxx") 7
-- Set14b> query_ db getAllQuery :: IO [(String,Int)]
-- [("xxx",13),("yyy",5),("xxx",7)]
initQuery :: Query
initQuery = Query (T.pack "CREATE TABLE IF NOT EXISTS events (account TEXT NOT NULL, amount NUMBER NOT NULL);")
depositQuery :: Query
depositQuery = Query (T.pack "INSERT INTO events (account, amount) VALUES (?, ?);")
getAllQuery :: Query
getAllQuery = Query (T.pack "SELECT account, amount FROM events;")
-- openDatabase should open an SQLite database using the given
-- filename, run initQuery on it, and produce a database Connection.
openDatabase :: String -> IO Connection
openDatabase = todo
-- given a db connection, an account name, and an amount, deposit
-- should add an (account, amount) row into the database
deposit :: Connection -> T.Text -> Int -> IO ()
deposit = todo
------------------------------------------------------------------------------
-- Ex 2: Fetching an account's balance. Below you'll find
-- balanceQuery, a query which gets all the amounts related to an
-- account from the database.
--
-- Implement the IO operation balance, which given an account, returns
-- the sum of all the amounts related to that account.
--
-- PS. if you know SQL you can do the summing in SQL by changing
-- balanceQuery, otherwise you can do it in the balance operation
-- itself. If you choose to edit the SQL query, remember that sum
-- can return null.
--
-- Example in GHCi:
-- Set14b> db <- openDatabase ""
-- Set14b> deposit db (T.pack "xxx") 13
-- Set14b> deposit db (T.pack "yyy") 5
-- Set14b> deposit db (T.pack "xxx") 7
-- Set14b> balance db (T.pack "xxx")
-- 20
-- Set14b> balance db (T.pack "yyy")
-- 5
-- Set14b> balance db (T.pack "zzz")
-- 0
balanceQuery :: Query
balanceQuery = Query (T.pack "SELECT amount FROM events WHERE account = ?;")
balance :: Connection -> T.Text -> IO Int
balance = todo
------------------------------------------------------------------------------
-- Ex 3: Now that we have the database part covered, let's think about
-- our API next. The datatype Command represents the various commands
-- users can issue: Deposit and Balance.
--
-- The HTTP API will use paths like the following:
-- * /deposit/smith/3 will deposit 3 into the account "smith"
-- * /balance/lopez will query the balance of the account "lopez"
--
-- Your task is to implement the function parseCommand that takes the
-- pathInfo (remember: a list of Texts) of a request, and returns the
-- Command it corresponds to.
--
-- The return type of this function is Maybe Command instead of
-- Command so that we can add error handling later. For now, you can
-- assume the input to parseCommand is always valid, and the return
-- value is always Just someCommand.
--
-- The function parseInt that reads an Int from a Text is provided for
-- you.
--
-- PS. the test outputs print Text values as if they were Strings,
-- just like GHCi prints Texts as Strings.
--
-- Examples:
-- parseCommand [T.pack "balance", T.pack "madoff"]
-- ==> Just (Balance "madoff")
-- parseCommand [T.pack "deposit", T.pack "madoff", T.pack "123456"]
-- ==> Just (Deposit "madoff" 123456)
data Command = Deposit T.Text Int | Balance T.Text
deriving (Show, Eq)
parseInt :: T.Text -> Maybe Int
parseInt = readMaybe . T.unpack
parseCommand :: [T.Text] -> Maybe Command
parseCommand = todo
------------------------------------------------------------------------------
-- Ex 4: Running commands. Implement the IO operation perform that takes a
-- database Connection, the result of parseCommand (a Maybe Command),
-- and runs the command in the database. Remember to use the
-- operations you implemented in exercises 1 and 2.
--
-- The perform operation should produce a Text that describes the result
-- of the command. The result of a Deposit command should be "OK" and
-- the result of a Balance command should be the balance, as a Text.
--
-- You don't need to handle the case where the command is Nothing yet,
-- you'll get to deal with that in exercise 8.
--
-- Example in GHCi:
-- Set14b> perform db (Just (Deposit (T.pack "madoff") 123456))
-- "OK"
-- Set14b> perform db (Just (Deposit (T.pack "madoff") 654321))
-- "OK"
-- Set14b> perform db (Just (Balance (T.pack "madoff")))
-- "777777"
-- Set14b> perform db (Just (Balance (T.pack "unknown")))
-- "0"
perform :: Connection -> Maybe Command -> IO T.Text
perform = todo
------------------------------------------------------------------------------
-- Ex 5: Next up, let's set up a simple HTTP server. Implement a WAI
-- Application simpleServer that always responds with a HTTP status
-- 200 and a text "BANK" to any request.
--
-- You can use the function encodeResponse to convert a Text into the
-- right kind of ByteString to give to responseLBS.
--
-- Example:
-- - In GHCi: run 8899 simpleServer
-- - Go to <http://localhost:8899> in your browser, you should see the text BANK
encodeResponse :: T.Text -> LB.ByteString
encodeResponse t = LB.fromStrict (encodeUtf8 t)
-- Remember:
-- type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
simpleServer :: Application
simpleServer request respond = todo
------------------------------------------------------------------------------
-- Ex 6: Now we finally have all the pieces we need to actually
-- implement our API. Implement a WAI Application called server that
-- receives a request, parses the Command it refers to, and runs the
-- command. Use the parseCommand, perform and encodeResponse
-- functions.
--
-- After you've implemented server, you can run the bank API from the
-- command line with
-- stack runhaskell Set14b.hs
-- This uses the main function provided below.
--
-- Tip: it can make debugging easier if you print the command before
-- performing it.
--
-- Example:
-- - Run the server with "stack runhaskell Set14b.hs"
-- - Open <http://localhost:3421/deposit/lopez/17> in your browser.
-- You should see the text OK.
-- - Open <http://localhost:3421/deposit/lopez/8> in your browser.
-- You should see the text OK.
-- - Open <http://localhost:3421/balance/lopez> in your browser.
-- You should see the text 25.
-- Remember:
-- type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
server :: Connection -> Application
server db request respond = todo
port :: Int
port = 3421
main :: IO ()
main = do
db <- openDatabase "bank.db"
putStr "Running on port: "
print port
run port (server db)
------------------------------------------------------------------------------
-- Ex 7: Add the possibility to withdraw funds to the API. Withdrawing
-- should happen via a /withdraw/<account>/<amount> path, similarly to
-- deposit. The response to a withdraw should be "OK", just like for a
-- deposit. You'll need to edit the Command datatype, and the
-- parseCommand and run functions to support this new command.
--
-- Hint: you can just use deposit IO operation to implement the
-- withdraw. You don't need new SQL queries.
--
-- Example:
-- - Run the server with "stack runhaskell Set14b.hs"
-- - Open <http://localhost:3421/deposit/simon/17> in your browser.
-- You should see the text OK.
-- - Open <http://localhost:3421/withdraw/simon/6> in your browser.
-- You should see the text OK.
-- - Open <http://localhost:3421/balance/simon> in your browser.
-- You should see the text 11.
------------------------------------------------------------------------------
-- Ex 8: Error handling. Modify the parseCommand function so that it
-- returns Nothing when the input is not valid. Modify the perform
-- function so that it produces an "ERROR" response given a Nothing.
--
-- Hint: the Maybe monad can help you with parseCommand, but you can
-- also just write normal code instead.
--
-- Examples:
-- - Run the server with "stack runhaskell Set14b.hs"
-- - All of these URLs should produce the text ERROR:
-- - http://localhost:3421/unknown/path
-- - http://localhost:3421/deposit/pekka
-- - http://localhost:3421/deposit/pekka/x
-- - http://localhost:3421/deposit/pekka/1x
-- - http://localhost:3421/deposit/pekka/1/3
-- - http://localhost:3421/balance
-- - http://localhost:3421/balance/matti/pekka