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. -- -- NOTE! Do not add anything to the name, otherwise you'll get weird -- test failures later. 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 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 in your browser. -- You should see the text OK. -- - Open in your browser. -- You should see the text OK. -- - Open 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// 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 in your browser. -- You should see the text OK. -- - Open in your browser. -- You should see the text OK. -- - Open 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