Problem trying to unit test routes. Scotty, Persistent, and Hspec-WAI.
Unlike Yesod or Spock, Scotty doesn't have a nice place to store database handlers. I've got it working by having one massive "do" that starts up the database, keeps the database pool as a local variable, then uses that variable.
app :: IO ()
app = do
-- allocate_database $ \pool
-- scotty 8080 $do
-- handleSomeRoute pool
However, Hspec-WAI wants it in the IO Application form.
scottyApp :: ScottyM () -> IO Application
Is there a sane way to inject the DB connection pool into a scottyApp ?
Here's how you can do it. Basically you open the database before you make the hspec call:
{-# LANGUAGE OverloadedStrings #-}
import Test.Hspec
import Test.Hspec.Wai
import Network.Wai (Application)
import qualified Web.Scotty as S
allocate_db :: (Int -> IO a) -> IO a
allocate_db = undefined
handleSomeRoute :: Int -> S.ScottyM ()
handleSomeRoute = undefined
main2 :: IO ()
main2 = allocate_db $ \pool -> do
let app' = handleSomeRoute pool
hspec $ with (S.scottyApp app') $ do
describe "GET /" $ do
it "responds with 200" $ do
get "/" `shouldRespondWith` 200
Related
I would like for my default handler to be able to catch all of the exceptions that my App throws but in order for this to happen I need to manually call raise after manually adding some exception catching around my IO code.
below is an example minimal server:
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Lib
( someFunc
) where
import Web.Scotty.Trans
import Control.Monad.Trans
import Control.Monad.Reader
import Control.Monad.Catch
import Control.Monad.Except
import Data.Text.Lazy as TL
data AppEnv = AppEnv
{ appStuff :: String
}
newtype App a = App
{ unApp :: ReaderT AppEnv IO a
} deriving (Functor, Applicative, Monad, MonadIO, MonadReader AppEnv, MonadThrow)
someFunc :: IO ()
someFunc = do
let run a = runReaderT (unApp $ App a) (AppEnv "APPY STUFF")
scottyT 8080 run $ do
defaultHandler $ \(e :: TL.Text) -> do
liftIO $ print "HERE"
liftIO $ print $ showError e
html $ "Something Went Seriously Wrong"
get "/" $ do
(r :: (Either TL.Text String)) <- liftIO $ runExceptT $ do
(uId) <- lift $ readFile "./helloworld.txt"
return $ ("hello")
liftIO $ print r
case r of
Left l -> raise l
Right s -> (html "hello world")
get "/catch-this" $ do
error "Catch Me"
(html "hello world")
notFound $ do
html "That is not a valid route"
I would like to be able to catch all of my uncaught exceptions in my default handler however this is not the default behavior of scotty that only happens if you call raise. I could wrap all of my ActionM code blocks in ExceptT however this seems like a messy/mechanical way of solving this problem. I mostly want to do this for logging purposes so I can report out to Sentry or Log to a file and this would make it much more convenient.
I figured I'd throw this in there as I recently was looking for this same solution again. It's unfortunate but I was never able to get the behavior that I wanted out of scotty.
Fortunately since scotty is just a nice library to create WAI application's you can get a nice work around using the Settings type from warp and the Options type from scotty.
Below is an example of how you can approach this:
{-# LANGUAGE OverloadedStrings #-}
module Lib
( someFunc
) where
import Web.Scotty.Trans
import Data.Text
import qualified Data.Text.Lazy as TL
import Control.Monad.IO.Class
import Control.Exception
import Network.HTTP.Types
import System.IO.Error
import Network.Wai.Handler.Warp
import Network.Wai
myOpts :: Options
myOpts = Options 1 mySettings
mySettings :: Settings
mySettings = setOnExceptionResponse myHandler $ setPort 3002 $ defaultSettings
myHandler :: SomeException -> Response
myHandler se = responseLBS status500 [] "HERE WE ARE"
someFunc :: IO ()
someFunc = do
scottyOptsT myOpts id routes
myExceptions :: (MonadIO m) => TL.Text -> ActionT TL.Text m ()
myExceptions t = do
liftIO $ print t
html "error"
routes :: (MonadIO m) => ScottyT TL.Text m ()
routes = do
defaultHandler $ \str -> do
liftAndCatchIO $ print str
status status500
json ("welp you thought"::Text)
get "/:here" $ do
liftIO $ ioError $ userError "Hahah"
text "here"
You can then tap further into the Settings type provided by warp so that you could maybe log all the error messages or perform some custom action using the following methods setOnException setOnExceptionResponse.
I'd like to use a custom application type in place of IO in my program and use it with functions like race_ from the async library.
Specifically, I'm keen on passing two computations of type App to race_. Since race_ only accepts values of type IO, I wrapped those computations with return.
While this type-checks, I can see that neither of the computations is actually executed.
Here's a minimal example¹ illustrating the issue:
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
module RaceTest where
import Control.Monad.Reader ( MonadReader
, ReaderT(..)
, runReaderT
)
import Control.Monad.IO.Class ( MonadIO
, liftIO
)
import Control.Concurrent.Async ( race_ )
data Env = Env { val :: !Int }
newtype App a = App
{ unApp :: ReaderT Env IO a
} deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env)
runApp :: Env -> App a -> IO a
runApp env app = runReaderT (unApp app) env
main = runApp (Env 24) simpleApp
where
simpleApp :: App () = do
liftIO $ putStrLn "About to spawn threads"
liftIO $ race_ (return firstAsync) (return secondAsync)
firstAsync :: App () = liftIO $ putStrLn "First async"
secondAsync :: App () = liftIO $ putStrLn "Second async"
How can I run these computations of type App using race_?
¹ While it would be simple in this example to just get rid of the App type, in the application I'm building, I have App and Env types that allow logging using co-log similar to this setup. That's something I don't want to lose.
Your computations are not getting executed, because you're not actually passing them to race_. Instead, you're passing two IO computations, which return your App computations as a result. But not executing them.
In order to get them executed inside IO, use your function runApp that you already have. Since you'll need to pass an environment to it, and I'm assuming you'll want to use the same environment as simpleApp itself has, you can use ask to obtain it out of the MonadReader context:
simpleApp :: App () = do
liftIO $ putStrLn "About to spawn threads"
env <- ask
liftIO $ race_ (runApp env firstAsync) (runApp env secondAsync)
In the question Web, Scotty: connection pool as monad reader it is shown how to use ScottyT to embed a Reader monad in the stack to access a static configuration (in that case, a connection pool).
I have a similar question, but simpler – or at least I thought so…
I want to add a Reader to a single handler (i.e. a ActionT), not the whole app.
I started modifying the program from the question above, but I cannot figure out how to turn an ActionT Text (ReaderT String IO) into the ActionT Text IO the handler needs to be. After fumbling around and trying to use typed holes hoping to see how to construct this I have to give up for now and ask for help. I really feel this should be simple, but cannot figure out how to do this.
Here's the program, with the lines where I'm stuck highlighted:
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text.Lazy as T
import Data.Text.Lazy (Text)
import Control.Monad.Reader
import Web.Scotty.Trans
type ActionD = ActionT Text (ReaderT String IO)
main :: IO ()
main = do
scottyT 3000 id id app
-- Application
app :: ScottyT Text IO ()
app = do
get "/foo" $ do
h <- handler -- ?
runReaderT h "foo" -- ?
--get "/bar" $ do
-- h <- handler
-- runReaderT h "bar"
-- Route action handler
handler :: ActionD ()
handler = do
config <- lift ask
html $ T.pack $ show config
If you want to run each action in a separate reader, you don't need the more complex Scotty.Trans interface at all. You can just build you monad stack the other way around, with ReaderT on top.
import qualified Data.Text.Lazy as T
import Control.Monad.Reader
import Web.Scotty
type ActionD = ReaderT String ActionM
main :: IO ()
main = do
scotty 3000 app
-- Application
app :: ScottyM ()
app = do
get "/foo" $ do
runReaderT handler "foo"
-- Route action handler
handler :: ActionD ()
handler = do
config <- ask
lift $ html $ T.pack $ show config
Using the websockets library in the following way
{-# LANGUAGE OverloadedStrings #-}
module Main where
import System.IO
import System.IO.Unsafe
import Network.Socket hiding (recv)
import Network.WebSockets
import Network.Socket.ByteString
import qualified Data.ByteString.Char8 as B
import Debug.Trace
import Control.Applicative
fetch :: IO B.ByteString
fetch = do
B.putStrLn "connected"
[v4] <- getAddrInfo Nothing (Just "127.0.0.1") (Just "3000")
c <- socket (addrFamily v4) Stream 0x6
c `connect` (addrAddress v4)
recv c 512000
proxy :: TextProtocol p => WebSockets p ()
proxy = sendTextData . unsafePerformIO $! fetch
app :: Request -> WebSockets Hybi00 ()
app r = acceptRequest r >> r `traceShow` proxy
main :: IO ()
main = withSocketsDo $! runServer "0.0.0.0" 4000 app
causes fetch to occur only once and all websocket clients receive the same not fresh data.
How can I do arbitrary IO with websockets?
How can I get the above example to work with fresh fetches?
I would love to hear any suggestions or complete solutions. A way of doing it without touching iteratee would be exceptionally appreciated.
The WebSockets monad is an instance of the MonadIO typeclass, so you can do arbitrary IO-operations with the liftIO function.
In this case I'm guessing you want to do
proxy = liftIO fetch >>= sendTextData
You also need to add the import
import Control.Monad.IO.Class
I have the basic "hello world" application setup using wai, and would like to use wai-handler-devel, but am unsure how to go about it and can't find any examples of it in usage on a wai project.
{-# LANGUAGE OverloadedStrings #-}
import Network.Wai
import Network.HTTP.Types
import Network.Wai.Handler.Warp (run)
import Data.ByteString.Lazy.Char8 () -- Just for an orphan instance
app :: Application
app _ = return $ responseLBS
status200
[("Content-Type", "text/plain")]
"Hello, World!"
main :: IO ()
main = do
putStrLn $ "http://localhost:8080/"
run 8080 app
What do I need to do to get wai-handler-devel working with a basic wai app?
Note:
There is a fix here ( https://gist.github.com/1499226) i f you run into issues with "wai-handler-devel: command not found"
wai-handler-devel's Hackage page says that it should be invoked from the command-line like so:
$ wai-handler-devel <port> My.App.Module myApp
and that your application's type must look like this:
myApp :: (Application -> IO ()) -> IO ()
In this case, you should define myApp as follows:
myApp :: (Application -> IO ()) -> IO ()
myApp handler = handler app
although you may want to inline app entirely:
myApp :: (Application -> IO ()) -> IO ()
myApp handler = handler $ \_ -> return $ responseLBS
status200
[("Content-Type", "text/plain")]
"Hello, World!"
The type is like this so that you can do initialisation on start-up and the like in IO. I suggest reading the SmallApp and FullApp examples from wai-handler-devel's git repository; the latter is especially helpful, as it has debug output showing the flow of the code during a reload, and shows how to integrate a long-running database connection.
The run script for the FullApp example also shows how to use wai-handler-devel programmatically, including manually specifying Hamlet template dependencies (which the wai-handler-devel command-line tool determines automatically).
You should then be able to rewrite your main as follows:
main :: IO ()
main = do
putStrLn $ "http://localhost:8080/"
myApp (run 8080)
Of course, you could just as easily pass the run function from wai-handler-fastcgi, wai-handler-scgi or even wai-handler-webkit.