user3594595
user3594595

Reputation:

Using Request Parameters in WAI Without "IO" Causing Problems

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", "[email protected]"),
                                                  ("password", "Testing012")]),
                         ("jeff.walker", Map.fromList [("firstName", "Jeff"),
                                                       ("lastName", "Walker"),
                                                       ("email", "[email protected]"),
                                                       ("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.

Upvotes: 3

Views: 350

Answers (1)

Shoe
Shoe

Reputation: 76240

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.

Upvotes: 1

Related Questions