diff --git a/Web/Scotty.hs b/Web/Scotty.hs index 9338df20..465d3a57 100644 --- a/Web/Scotty.hs +++ b/Web/Scotty.hs @@ -22,7 +22,7 @@ module Web.Scotty -- definition, as they completely replace the current 'Response' body. , text, html, file, json, source, raw -- ** Exceptions - , raise, rescue, next + , raise, rescue, next, defaultHandler -- * Parsing Parameters , Param, Trans.Parsable(..), Trans.readEither -- * Types @@ -45,8 +45,8 @@ import Network.Wai.Handler.Warp (Port) import Web.Scotty.Types (ScottyT, ActionT, Param, RoutePattern, Options, File) -type ScottyM = ScottyT IO -type ActionM = ActionT () IO -- TODO: something besides () for default error type? +type ScottyM = ScottyT Text IO +type ActionM = ActionT Text IO -- | Run a scotty application using the warp server. scotty :: Port -> ScottyM () -> IO () @@ -61,6 +61,10 @@ scottyOpts opts = Trans.scottyOptsT opts id id scottyApp :: ScottyM () -> IO Application scottyApp = Trans.scottyAppT id id +-- | Global handler for uncaught exceptions. +defaultHandler :: (Text -> ActionM ()) -> ScottyM () +defaultHandler = Trans.defaultHandler + -- | Use given middleware. Middleware is nested such that the first declared -- is the outermost middleware (it has first dibs on the request and last action -- on the response). Every middleware is run on each request. diff --git a/Web/Scotty/Action.hs b/Web/Scotty/Action.hs index 84d360c6..8090ba09 100644 --- a/Web/Scotty/Action.hs +++ b/Web/Scotty/Action.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedStrings, RankNTypes #-} module Web.Scotty.Action ( addHeader , body @@ -39,10 +39,10 @@ import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.CaseInsensitive as CI import Data.Conduit (Flush, Source) import Data.Default (def) -import Data.Monoid (mconcat, (<>)) +import Data.Monoid (mconcat) import qualified Data.Text as ST import qualified Data.Text.Lazy as T -import Data.Text.Lazy.Encoding (encodeUtf8, decodeUtf8) +import Data.Text.Lazy.Encoding (encodeUtf8) import Network.HTTP.Types import Network.Wai @@ -52,32 +52,30 @@ import Web.Scotty.Util -- Nothing indicates route failed (due to Next) and pattern matching should continue. -- Just indicates a successful response. -runAction :: Monad m => ActionEnv -> ActionT e m () -> m (Maybe Response) -runAction env action = do +runAction :: (ScottyError e, Monad m) => ErrorHandler e m -> ActionEnv -> ActionT e m () -> m (Maybe Response) +runAction h env action = do (e,r) <- flip MS.runStateT def $ flip runReaderT env $ runErrorT $ runAM - $ action `catchError` defaultHandler + $ action `catchError` (defH h) return $ either (const Nothing) (const $ Just $ mkResponse r) e -defaultHandler :: Monad m => ActionError e -> ActionT e m () -defaultHandler (Redirect url) = do +-- | Default error handler for all actions. +defH :: (ScottyError e, Monad m) => ErrorHandler e m -> ActionError e -> ActionT e m () +defH _ (Redirect url) = do status status302 setHeader "Location" url -defaultHandler (StringError msg) = do +defH Nothing (ActionError e) = do status status500 - html $ mconcat ["

500 Internal Server Error

", msg] -defaultHandler Next = next -defaultHandler (ActionError _) = do - status status500 - html $ mconcat ["

500 Internal Server Error

" - ,"
Uncaught Custom Exception"] + html $ mconcat ["

500 Internal Server Error

", showError e] +defH h@(Just f) (ActionError e) = f e `catchError` (defH h) -- so handlers can throw exceptions themselves +defH _ Next = next -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions -- turn into HTTP 500 responses. -raise :: Monad m => T.Text -> ActionT e m a -raise = throwError . StringError +raise :: (ScottyError e, Monad m) => e -> ActionT e m a +raise = throwError . ActionError -- | Abort execution of this action and continue pattern matching routes. -- Like an exception, any code after 'next' is not executed. @@ -93,16 +91,16 @@ raise = throwError . StringError -- > get "/foo/:bar" $ do -- > bar <- param "bar" -- > text "not a number" -next :: Monad m => ActionT e m a +next :: (ScottyError e, Monad m) => ActionT e m a next = throwError Next -- | Catch an exception thrown by 'raise'. -- -- > raise "just kidding" `rescue` (\msg -> text msg) -rescue :: Monad m => ActionT e m a -> (T.Text -> ActionT e m a) -> ActionT e m a -rescue action handler = catchError action $ \e -> case e of - StringError msg -> handler msg -- handle errors - other -> throwError other -- rethrow redirects and nexts +rescue :: (ScottyError e, Monad m) => ActionT e m a -> (e -> ActionT e m a) -> ActionT e m a +rescue action h = catchError action $ \e -> case e of + ActionError err -> h err -- handle errors + other -> throwError other -- rethrow internal error types -- | Redirect to given URL. Like throwing an uncatchable exception. Any code after the call to redirect -- will not be run. @@ -112,32 +110,32 @@ rescue action handler = catchError action $ \e -> case e of -- OR -- -- > redirect "/foo/bar" -redirect :: Monad m => T.Text -> ActionT e m a +redirect :: (ScottyError e, Monad m) => T.Text -> ActionT e m a redirect = throwError . Redirect -- | Get the 'Request' object. -request :: Monad m => ActionT e m Request +request :: (ScottyError e, Monad m) => ActionT e m Request request = ActionT $ liftM getReq ask -- | Get list of uploaded files. -files :: Monad m => ActionT e m [File] +files :: (ScottyError e, Monad m) => ActionT e m [File] files = ActionT $ liftM getFiles ask -- | Get a request header. Header name is case-insensitive. -reqHeader :: Monad m => T.Text -> ActionT e m (Maybe T.Text) +reqHeader :: (ScottyError e, Monad m) => T.Text -> ActionT e m (Maybe T.Text) reqHeader k = do hs <- liftM requestHeaders request return $ fmap strictByteStringToLazyText $ lookup (CI.mk (lazyTextToStrictByteString k)) hs -- | Get the request body. -body :: Monad m => ActionT e m BL.ByteString +body :: (ScottyError e, Monad m) => ActionT e m BL.ByteString body = ActionT $ liftM getBody ask -- | Parse the request body as a JSON object and return it. Raises an exception if parse is unsuccessful. -jsonData :: (A.FromJSON a, Monad m) => ActionT e m a +jsonData :: (A.FromJSON a, ScottyError e, Monad m) => ActionT e m a jsonData = do b <- body - maybe (raise $ "jsonData - no parse: " <> decodeUtf8 b) return $ A.decode b + maybe (raise $ stringError $ "jsonData - no parse: " ++ BL.unpack b) return $ A.decode b -- | Get a parameter. First looks in captures, then form data, then query parameters. -- @@ -146,15 +144,15 @@ jsonData = do -- * If parameter is found, but 'read' fails to parse to the correct type, 'next' is called. -- This means captures are somewhat typed, in that a route won't match if a correctly typed -- capture cannot be parsed. -param :: (Parsable a, Monad m) => T.Text -> ActionT e m a +param :: (Parsable a, ScottyError e, Monad m) => T.Text -> ActionT e m a param k = do val <- ActionT $ liftM (lookup k . getParams) ask case val of - Nothing -> raise $ mconcat ["Param: ", k, " not found!"] + Nothing -> raise $ stringError $ "Param: " ++ T.unpack k ++ " not found!" Just v -> either (const next) return $ parseParam v -- | Get all parameters from capture, form and query (in that order). -params :: Monad m => ActionT e m [Param] +params :: (ScottyError e, Monad m) => ActionT e m [Param] params = ActionT $ liftM getParams ask -- | Minimum implemention: 'parseParam' @@ -201,40 +199,40 @@ readEither t = case [ x | (x,"") <- reads (T.unpack t) ] of _ -> Left "readEither: ambiguous parse" -- | Set the HTTP response status. Default is 200. -status :: Monad m => Status -> ActionT e m () +status :: (ScottyError e, Monad m) => Status -> ActionT e m () status = ActionT . MS.modify . setStatus -- | Add to the response headers. Header names are case-insensitive. -addHeader :: Monad m => T.Text -> T.Text -> ActionT e m () +addHeader :: (ScottyError e, Monad m) => T.Text -> T.Text -> ActionT e m () addHeader k v = ActionT . MS.modify $ setHeaderWith $ add (CI.mk $ lazyTextToStrictByteString k) (lazyTextToStrictByteString v) -- | Set one of the response headers. Will override any previously set value for that header. -- Header names are case-insensitive. -setHeader :: Monad m => T.Text -> T.Text -> ActionT e m () +setHeader :: (ScottyError e, Monad m) => T.Text -> T.Text -> ActionT e m () setHeader k v = ActionT . MS.modify $ setHeaderWith $ replace (CI.mk $ lazyTextToStrictByteString k) (lazyTextToStrictByteString v) -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/plain\". -text :: Monad m => T.Text -> ActionT e m () +text :: (ScottyError e, Monad m) => T.Text -> ActionT e m () text t = do setHeader "Content-Type" "text/plain" raw $ encodeUtf8 t -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/html\". -html :: Monad m => T.Text -> ActionT e m () +html :: (ScottyError e, Monad m) => T.Text -> ActionT e m () html t = do setHeader "Content-Type" "text/html" raw $ encodeUtf8 t -- | Send a file as the response. Doesn't set the \"Content-Type\" header, so you probably -- want to do that on your own with 'header'. -file :: Monad m => FilePath -> ActionT e m () +file :: (ScottyError e, Monad m) => FilePath -> ActionT e m () file = ActionT . MS.modify . setContent . ContentFile -- | Set the body of the response to the JSON encoding of the given value. Also sets \"Content-Type\" -- header to \"application/json\". -json :: (A.ToJSON a, Monad m) => a -> ActionT e m () +json :: (A.ToJSON a, ScottyError e, Monad m) => a -> ActionT e m () json v = do setHeader "Content-Type" "application/json" raw $ A.encode v @@ -242,11 +240,11 @@ json v = do -- | Set the body of the response to a Source. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your -- own with 'header'. -source :: Monad m => Source IO (Flush Builder) -> ActionT e m () +source :: (ScottyError e, Monad m) => Source IO (Flush Builder) -> ActionT e m () source = ActionT . MS.modify . setContent . ContentSource -- | Set the body of the response to the given 'BL.ByteString' value. Doesn't set the -- \"Content-Type\" header, so you probably want to do that on your -- own with 'header'. -raw :: Monad m => BL.ByteString -> ActionT e m () +raw :: (ScottyError e, Monad m) => BL.ByteString -> ActionT e m () raw = ActionT . MS.modify . setContent . ContentBuilder . fromLazyByteString diff --git a/Web/Scotty/Helpers.hs b/Web/Scotty/Helpers.hs index 2e985a7f..4244f47a 100644 --- a/Web/Scotty/Helpers.hs +++ b/Web/Scotty/Helpers.hs @@ -13,6 +13,7 @@ import qualified Network.Wai as Wai -- Note that we only import the monad transformer version, to force -- us to be generic in the underyling monad. MonadIO constraints are fine. +import Web.Scotty.Types import Web.Scotty.Trans import Web.Scotty.Util @@ -21,5 +22,5 @@ import Web.Scotty.Util -- -- > redirect =<< addQueryString "/foo" -- -addQueryString :: Monad m => T.Text -> ActionT e m T.Text +addQueryString :: (ScottyError e, Monad m) => T.Text -> ActionT e m T.Text addQueryString r = liftM ((r <>) . strictByteStringToLazyText . Wai.rawQueryString) request diff --git a/Web/Scotty/Route.hs b/Web/Scotty/Route.hs index 7c3d9621..a74ffa61 100644 --- a/Web/Scotty/Route.hs +++ b/Web/Scotty/Route.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE OverloadedStrings, FlexibleContexts, FlexibleInstances, ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings, FlexibleContexts, FlexibleInstances, RankNTypes #-} module Web.Scotty.Route ( get, post, put, delete, patch, addroute, matchAny, notFound, capture, regex, function, literal @@ -32,32 +32,32 @@ import Web.Scotty.Types import Web.Scotty.Util -- | get = 'addroute' 'GET' -get :: MonadIO m => RoutePattern -> ActionT e m () -> ScottyT m () +get :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () get = addroute GET -- | post = 'addroute' 'POST' -post :: MonadIO m => RoutePattern -> ActionT e m () -> ScottyT m () +post :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () post = addroute POST -- | put = 'addroute' 'PUT' -put :: MonadIO m => RoutePattern -> ActionT e m () -> ScottyT m () +put :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () put = addroute PUT -- | delete = 'addroute' 'DELETE' -delete :: MonadIO m => RoutePattern -> ActionT e m () -> ScottyT m () +delete :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () delete = addroute DELETE -- | patch = 'addroute' 'PATCH' -patch :: MonadIO m => RoutePattern -> ActionT e m () -> ScottyT m () +patch :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () patch = addroute PATCH -- | Add a route that matches regardless of the HTTP verb. -matchAny :: MonadIO m => RoutePattern -> ActionT e m () -> ScottyT m () +matchAny :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () matchAny pattern action = mapM_ (\v -> addroute v pattern action) [minBound..maxBound] -- | Specify an action to take if nothing else is found. Note: this _always_ matches, -- so should generally be the last route specified. -notFound :: MonadIO m => ActionT e m () -> ScottyT m () +notFound :: (ScottyError e, MonadIO m) => ActionT e m () -> ScottyT e m () notFound action = matchAny (Function (\req -> Just [("path", path req)])) (status status404 >> action) -- | Define a route with a 'StdMethod', 'T.Text' value representing the path spec, @@ -74,17 +74,17 @@ notFound action = matchAny (Function (\req -> Just [("path", path req)])) (statu -- -- >>> curl http://localhost:3000/foo/something -- something -addroute :: MonadIO m => StdMethod -> RoutePattern -> ActionT e m () -> ScottyT m () -addroute method pat action = ScottyT $ MS.modify $ addRoute $ route method pat action +addroute :: (ScottyError e, MonadIO m) => StdMethod -> RoutePattern -> ActionT e m () -> ScottyT e m () +addroute method pat action = ScottyT $ MS.modify $ \s -> addRoute (route (handler s) method pat action) s -route :: MonadIO m => StdMethod -> RoutePattern -> ActionT e m () -> Middleware m -route method pat action app req = +route :: (ScottyError e, MonadIO m) => ErrorHandler e m -> StdMethod -> RoutePattern -> ActionT e m () -> Middleware m +route h method pat action app req = let tryNext = app req in if Right method == parseMethod (requestMethod req) then case matchRoute pat req of Just captures -> do env <- mkEnv req captures - res <- runAction env action + res <- runAction h env action maybe tryNext return res Nothing -> tryNext else tryNext diff --git a/Web/Scotty/Trans.hs b/Web/Scotty/Trans.hs index f2c7d83c..3cd6162a 100644 --- a/Web/Scotty/Trans.hs +++ b/Web/Scotty/Trans.hs @@ -26,7 +26,7 @@ module Web.Scotty.Trans -- definition, as they completely replace the current 'Response' body. , text, html, file, json, source, raw -- ** Exceptions - , raise, rescue, next + , raise, rescue, next, defaultHandler, ScottyError(..) -- * Parsing Parameters , Param, Parsable(..), readEither -- * Types @@ -58,7 +58,7 @@ scottyT :: (Monad m, MonadIO n) => Port -> (forall a. m a -> n a) -- ^ Run monad 'm' into monad 'n', called once at 'ScottyT' level. -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. - -> ScottyT m () + -> ScottyT e m () -> n () scottyT p = scottyOptsT $ def { settings = (settings def) { settingsPort = p } } @@ -68,7 +68,7 @@ scottyOptsT :: (Monad m, MonadIO n) => Options -> (forall a. m a -> n a) -- ^ Run monad 'm' into monad 'n', called once at 'ScottyT' level. -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. - -> ScottyT m () + -> ScottyT e m () -> n () scottyOptsT opts runM runActionToIO s = do when (verbose opts > 0) $ @@ -81,7 +81,7 @@ scottyOptsT opts runM runActionToIO s = do scottyAppT :: (Monad m, Monad n) => (forall a. m a -> n a) -- ^ Run monad 'm' into monad 'n', called once at 'ScottyT' level. -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. - -> ScottyT m () + -> ScottyT e m () -> n Application scottyAppT runM runActionToIO defs = do s <- runM $ execStateT (runS defs) def @@ -92,8 +92,12 @@ notFoundApp :: Monad m => Scotty.Application m notFoundApp _ = return $ responseBuilder status404 [("Content-Type","text/html")] $ fromByteString "

404: File Not Found!

" +-- | Global handler for uncaught custom exceptions. +defaultHandler :: Monad m => (e -> ActionT e m ()) -> ScottyT e m () +defaultHandler f = ScottyT $ modify $ addHandler $ Just f + -- | Use given middleware. Middleware is nested such that the first declared -- is the outermost middleware (it has first dibs on the request and last action -- on the response). Every middleware is run on each request. -middleware :: Monad m => Middleware -> ScottyT m () +middleware :: Monad m => Middleware -> ScottyT e m () middleware = ScottyT . modify . addMiddleware diff --git a/Web/Scotty/Types.hs b/Web/Scotty/Types.hs index e528e0f2..e6511b52 100644 --- a/Web/Scotty/Types.hs +++ b/Web/Scotty/Types.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving, InstanceSigs, MultiParamTypeClasses #-} +{-# LANGUAGE GeneralizedNewtypeDeriving, FlexibleInstances, MultiParamTypeClasses #-} module Web.Scotty.Types where import Blaze.ByteString.Builder (Builder) @@ -35,35 +35,56 @@ type Middleware m = Application m -> Application m type Application m = Request -> m Response --------------- Scotty Applications ----------------- -data ScottyState m = ScottyState { middlewares :: [Wai.Middleware] - , routes :: [Middleware m] - } +data ScottyState e m = + ScottyState { middlewares :: [Wai.Middleware] + , routes :: [Middleware m] + , handler :: ErrorHandler e m + } -instance Default (ScottyState m) where - def = ScottyState [] [] +instance Monad m => Default (ScottyState e m) where + def = ScottyState [] [] Nothing -addMiddleware :: Wai.Middleware -> ScottyState m -> ScottyState m +addMiddleware :: Wai.Middleware -> ScottyState e m -> ScottyState e m addMiddleware m s@(ScottyState {middlewares = ms}) = s { middlewares = m:ms } -addRoute :: Monad m => Middleware m -> ScottyState m -> ScottyState m +addRoute :: Monad m => Middleware m -> ScottyState e m -> ScottyState e m addRoute r s@(ScottyState {routes = rs}) = s { routes = r:rs } -newtype ScottyT m a = ScottyT { runS :: StateT (ScottyState m) m a } +addHandler :: ErrorHandler e m -> ScottyState e m -> ScottyState e m +addHandler h s = s { handler = h } + +newtype ScottyT e m a = ScottyT { runS :: StateT (ScottyState e m) m a } deriving ( Functor, Applicative, Monad, MonadIO ) -instance MonadTrans ScottyT where +instance MonadTrans (ScottyT e) where lift = ScottyT . lift ------------------- Scotty Actions ------------------- -type Param = (Text, Text) - +------------------ Scotty Errors -------------------- data ActionError e = Redirect Text - | StringError Text | Next | ActionError e -instance Error (ActionError e) where - strMsg = StringError . pack +class ScottyError e where + stringError :: String -> e + showError :: e -> Text + +instance ScottyError Text where + stringError = pack + showError = id + +instance ScottyError e => ScottyError (ActionError e) where + stringError = ActionError . stringError + showError (Redirect url) = url + showError Next = pack "Next" + showError (ActionError e) = showError e + +instance ScottyError e => Error (ActionError e) where + strMsg = stringError + +type ErrorHandler e m = Maybe (e -> ActionT e m ()) + +------------------ Scotty Actions ------------------- +type Param = (Text, Text) type File = (Text, FileInfo ByteString) @@ -88,14 +109,12 @@ instance Default ScottyResponse where newtype ActionT e m a = ActionT { runAM :: ErrorT (ActionError e) (ReaderT ActionEnv (StateT ScottyResponse m)) a } deriving ( Functor, Applicative, Monad, MonadIO ) -instance MonadTrans (ActionT e) where +instance ScottyError e => MonadTrans (ActionT e) where lift = ActionT . lift . lift . lift -instance Monad m => MonadError (ActionError e) (ActionT e m) where - throwError :: ActionError e -> ActionT e m a +instance (ScottyError e, Monad m) => MonadError (ActionError e) (ActionT e m) where throwError = ActionT . throwError - catchError :: ActionT e m a -> (ActionError e -> ActionT e m a) -> ActionT e m a catchError (ActionT m) f = ActionT (catchError m (runAM . f)) ------------------ Scotty Routes -------------------- diff --git a/examples/exceptions.hs b/examples/exceptions.hs index bf2dffa8..0ac9ed8c 100644 --- a/examples/exceptions.hs +++ b/examples/exceptions.hs @@ -1,61 +1,62 @@ {-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving, ScopedTypeVariables #-} --- An example of embedding a custom monad into --- Scotty's transformer stack, using ErrorT to provide --- custom exceptions and a centralized exception handler. module Main where import Control.Applicative import Control.Monad.Error -import Data.ByteString.Lazy hiding (pack) -import Data.ByteString.Lazy.Char8 (pack) import Data.Monoid +import Data.String (fromString) import Network.HTTP.Types import Network.Wai.Middleware.RequestLogger import Network.Wai -import Web.Scotty.Trans +import System.Random -newtype ExM a = ExM { runExM :: ErrorT Except IO a } - deriving (Functor, Applicative, Monad, MonadIO, MonadError Except) +import Web.Scotty.Trans -data Except = Forbidden | NotFound Int | Other ByteString +-- Define a custom exception type. +data Except = Forbidden | NotFound Int | StringEx String deriving (Show, Eq) -instance Error Except where - strMsg = Other . pack - -handleEx :: Except -> IO Response -handleEx Forbidden = return $ plainResponse status403 "Scotty says no." -handleEx (NotFound i) = return $ plainResponse status404 (pack $ "Can't find " ++ show i ++ ".") -handleEx (Other bs) = return $ plainResponse status500 bs - -plainResponse :: Status -> ByteString -> Response -plainResponse st bs = responseLBS st [("Content-type","text/plain")] bs - --- Scotty's monads are layered on top of our custom monad. --- We define this helper to put our exceptions in the right layer. -throwEx :: MonadTrans t => Except -> t ExM () -throwEx = lift . throwError +-- The type must be an instance of 'ScottyError'. +-- 'ScottyError' is essentially a combination of 'Error' and 'Show'. +instance ScottyError Except where + stringError = StringEx + showError = fromString . show + +-- Handler for uncaught exceptions. +handleEx :: Monad m => Except -> ActionT Except m () +handleEx Forbidden = do + status status403 + html "

Scotty Says No

" +handleEx (NotFound i) = do + status status404 + html $ fromString $ "

Can't find " ++ show i ++ ".

" main :: IO () -main = do - let runM m = do - r <- runErrorT (runExM m) - either (\ ex -> fail $ "exception at startup: " ++ show ex) return r - -- 'runActionToIO' is called once per action. - runActionToIO m = runErrorT (runExM m) >>= either handleEx return - - scottyT 3000 runM runActionToIO $ do - middleware logStdoutDev - get "/" $ do - html $ mconcat ["Option 1 (Not Found)" - ,"
" - ,"Option 2 (Forbidden)" - ] - - get "/switch/:val" $ do - v :: Int <- param "val" - if even v then throwEx Forbidden else throwEx (NotFound v) - text "this will never be reached" +main = scottyT 3000 id id $ do -- note, we aren't using any additional transformer layers + -- so we can just use 'id' for the runners. + middleware logStdoutDev + + defaultHandler handleEx -- define what to do with uncaught exceptions + + get "/" $ do + html $ mconcat ["Option 1 (Not Found)" + ,"
" + ,"Option 2 (Forbidden)" + ,"
" + ,"Option 3 (Random)" + ] + + get "/switch/:val" $ do + v <- param "val" + if even v then raise Forbidden else raise (NotFound v) + text "this will never be reached" + + get "/random" $ do + rBool <- liftIO randomIO + i <- liftIO randomIO + let catchOne Forbidden = html "

Forbidden was randomly thrown, but we caught it." + catchOne other = raise other + raise (if rBool then Forbidden else NotFound i) `rescue` catchOne diff --git a/examples/globalstate.hs b/examples/globalstate.hs index 627cde6a..fa6170e3 100644 --- a/examples/globalstate.hs +++ b/examples/globalstate.hs @@ -14,6 +14,7 @@ import Control.Monad.Reader import Data.Default import Data.String +import Data.Text.Lazy (Text) import Network.Wai.Middleware.RequestLogger @@ -58,16 +59,23 @@ main = do -- 'runActionToIO' is called once per action. runActionToIO = runM - scottyT 3000 runM runActionToIO $ do - middleware logStdoutDev - get "/" $ do - c <- webM $ gets tickCount - text $ fromString $ show c + scottyT 3000 runM runActionToIO app - get "/plusone" $ do - webM $ modify $ \ st -> st { tickCount = tickCount st + 1 } - redirect "/" +-- This app doesn't use raise/rescue, so the exception +-- type is ambiguous. We can fix it by putting a type +-- annotation just about anywhere. In this case, we'll +-- just do it on the entire app. +app :: ScottyT Text WebM () +app = do + middleware logStdoutDev + get "/" $ do + c <- webM $ gets tickCount + text $ fromString $ show c - get "/plustwo" $ do - webM $ modify $ \ st -> st { tickCount = tickCount st + 2 } - redirect "/" + get "/plusone" $ do + webM $ modify $ \ st -> st { tickCount = tickCount st + 1 } + redirect "/" + + get "/plustwo" $ do + webM $ modify $ \ st -> st { tickCount = tickCount st + 2 } + redirect "/"