3

I'm struggling with the basics of getting an API up and running using WAI. The main issue is dealing with IO infecting everything. I believe that my problems will dissolve once I better understand Monads, but hopefully an answer to this question will be a good starting point.

The following is a short example that serves a static html page on the root url, and accepts a request with a username to /api/my-data that should return the corresponding user's data. I can't figure out how to use the IO Bytestring that is the request's body to do a map lookup, retrieve the data, and send the result back encoded in json.

I've tried using fmap to extract the Bytestring and then unpack to turn it into a string for lookup, but whatever I do, I end up chasing type errors related to the damn IO monad.

Anyway, here is the relevant code:

{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8
import qualified Data.Map as Map
import Data.Aeson
import Network.Wai
import Network.Wai.Parse
import Network.Wai.Middleware.Static
import Network.HTTP.Types
import Network.Wai.Handler.Warp (run)

userInfo :: Map.Map String (Map.Map String String)
userInfo = Map.fromList [("jsmith", Map.fromList [("firstName", "John"),
                                                  ("lastName", "Smith"),
                                                  ("email", "jsmith@gmail.com"),
                                                  ("password", "Testing012")]),
                         ("jeff.walker", Map.fromList [("firstName", "Jeff"),
                                                       ("lastName", "Walker"),
                                                       ("email", "jeff.walker@gmail.com"),
                                                       ("password", "Testing012")])]

getUserInfo :: B.ByteString -> Map.Map String String
getUserInfo body =
  case Map.lookup (B8.unpack body) userInfo of
    (Just x) -> x
    Nothing  -> Map.empty

app :: Application
app request respond = do
  case rawPathInfo request of
    "/"            -> respond index
    "/api/my-data" -> respond $ myData (getUserInfo (requestBody request))
    _              -> respond notFound

index :: Response
index = responseFile
  status200
  [("Content-Type", "text/html")]
  "../client/index.html"
  Nothing

myData :: IO (Map.Map String String) -> Response
myData user = responseLBS
  status200
  [("Content-Type", "application/json")]
  (encode user)


notFound :: Response
notFound = responseLBS
  status404
  [("Content-Type", "text/plain")]
  "404 - Not Found"

main :: IO ()
main = do
  putStrLn $ "http://localhost:8080/"
  run 8080 $ staticPolicy (addBase "../client/") $ app

This results in this error:

src/Core/Main.hs:32:54:
    Couldn't match expected type ‘B8.ByteString’
                with actual type ‘IO B8.ByteString’
    In the first argument of ‘getUserInfo’, namely
      ‘(requestBody request)’
    In the first argument of ‘myData’, namely
      ‘(getUserInfo (requestBody request))’

I can easily change the type of getUserInfo and myData to IO Bytestring -> IO (Map.Map String String) and IO (Map.Map String String) -> Response but then I end up with more type errors. Types are making my head spin.

unor
  • 92,415
  • 26
  • 211
  • 360
  • 3
    `Application` is just `Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived`. Which means in `app request respond = ..` you can perform the IO action which produces a bytestring, then pass that bytestring to pure functions in the rest of your code. Change `respond $ myData (getUserInfo (requestBody request))` to something like `do {x <- requestBody request; respond $ myData (getUserInfo x)}`; and change the type of `myData` to `Map.Map String String -> Response` – user2407038 Apr 18 '15 at 23:30

1 Answers1

1

Since requestBody has the following type:

requestBody :: Request -> IO ByteString

the resulting expression can't be directly passed to getUserInfo, which accepts a ByteString.

What you can do is, given that Application is simply Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived and in app you are in the IO monad, extract the ByteString with the do notation like this:

str <- requestBody request 

and then pass str to getUserInfo, like this:

app :: Application
app request respond = do
  str <- requestBody request
  case rawPathInfo request of
    "/"            -> respond index
    "/api/my-data" -> respond $ myData (getUserInfo str)
    _              -> respond notFound

at this point myData can simply accept a Map:

myData :: Map.Map String String -> Response

You should definitely read more about monads and IO in general before going any deeper with WAI though.

Shoe
  • 74,840
  • 36
  • 166
  • 272
  • `requestBody` has since been deprecated in favour of a more accurate name: `getRequestBodyChunk`; since this is what `requestBody` has been doing all along: return a _chunk_ of the request body, not the whole thing. As such, I don't think the solution you've posted here will work. – Ashesh Jun 30 '20 at 14:53