From 30560aa57acc36165f5e0636d3fd7eb31d392be5 Mon Sep 17 00:00:00 2001 From: Marco Zocca Date: Sun, 1 Oct 2023 14:13:39 +0200 Subject: [PATCH] some progress --- Web/Scotty.hs | 27 ++--- Web/Scotty/Action.hs | 213 ++++++++++++++++++--------------- Web/Scotty/Cookie.hs | 10 +- Web/Scotty/Internal/Types.hs | 98 +++++++-------- Web/Scotty/Route.hs | 36 +++--- Web/Scotty/Trans.hs | 30 ++--- Web/Scotty/Util.hs | 26 +++- changelog.md | 1 + examples/basic.hs | 2 +- examples/exceptions.hs | 2 +- examples/globalstate.hs | 5 +- examples/reader.hs | 10 +- examples/scotty-examples.cabal | 5 +- test/Web/ScottySpec.hs | 7 +- 14 files changed, 258 insertions(+), 214 deletions(-) diff --git a/Web/Scotty.hs b/Web/Scotty.hs index 0fd7d64a..b5dcf4a8 100644 --- a/Web/Scotty.hs +++ b/Web/Scotty.hs @@ -34,12 +34,13 @@ module Web.Scotty -- * Parsing Parameters , Param, Trans.Parsable(..), Trans.readEither -- * Types - , ScottyM, ActionM, RoutePattern, File, Kilobytes, AErr + , ScottyM, ActionM, RoutePattern, File, Kilobytes, Handler(..) ) where -- With the exception of this, everything else better just import types. import qualified Web.Scotty.Trans as Trans +import qualified Control.Exception as E import Data.Aeson (FromJSON, ToJSON) import qualified Data.ByteString as BS import Data.ByteString.Lazy.Char8 (ByteString) @@ -50,10 +51,10 @@ import Network.Socket (Socket) import Network.Wai (Application, Middleware, Request, StreamingBody) import Network.Wai.Handler.Warp (Port) -import Web.Scotty.Internal.Types (ScottyT, ActionT, AErr, Param, RoutePattern, Options, defaultOptions, File, Kilobytes) +import Web.Scotty.Internal.Types (ScottyT, ActionT, Handler(..), ErrorHandler, Param, RoutePattern, Options, defaultOptions, File, Kilobytes) -type ScottyM = ScottyT Text IO -type ActionM = ActionT Text IO +type ScottyM = ScottyT IO +type ActionM = ActionT IO -- | Run a scotty application using the warp server. scotty :: Port -> ScottyM () -> IO () @@ -75,16 +76,8 @@ scottySocket opts sock = Trans.scottySocketT opts sock id scottyApp :: ScottyM () -> IO Application scottyApp = Trans.scottyAppT id --- | Global handler for uncaught exceptions. --- --- Uncaught exceptions normally become 500 responses. --- You can use this to selectively override that behavior. --- --- Note: IO exceptions are lifted into Scotty exceptions by default. --- This has security implications, so you probably want to provide your --- own defaultHandler in production which does not send out the error --- strings as 500 responses. -defaultHandler :: (AErr -> ActionM ()) -> ScottyM () +-- | Global handler for user-defined exceptions. +defaultHandler :: ErrorHandler IO -> ScottyM () defaultHandler = Trans.defaultHandler -- | Use given middleware. Middleware is nested such that the first declared @@ -113,11 +106,11 @@ setMaxRequestBodySize = Trans.setMaxRequestBodySize -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions -- turn into HTTP 500 responses. -raise :: AErr -> ActionM a +raise :: E.Exception e => e -> ActionM a raise = Trans.raise -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions turn into HTTP responses corresponding to the given status. -raiseStatus :: Status -> AErr -> ActionM a +raiseStatus :: Status -> Text -> ActionM a raiseStatus = Trans.raiseStatus -- | Abort execution of this action and continue pattern matching routes. @@ -155,7 +148,7 @@ finish = Trans.finish -- | Catch an exception thrown by 'raise'. -- -- > raise "just kidding" `rescue` (\msg -> text msg) -rescue :: ActionM a -> (AErr -> ActionM a) -> ActionM a +rescue :: E.Exception e => ActionM a -> (e -> ActionM a) -> ActionM a rescue = Trans.rescue -- | Like 'liftIO', but catch any IO exceptions and turn them into Scotty exceptions. diff --git a/Web/Scotty/Action.hs b/Web/Scotty/Action.hs index 7ccde6a1..6a319370 100644 --- a/Web/Scotty/Action.hs +++ b/Web/Scotty/Action.hs @@ -1,3 +1,6 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PackageImports #-} {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} @@ -58,11 +61,13 @@ import Control.Monad.Trans.Except import Control.Concurrent.MVar import qualified Data.Aeson as A +import Data.Bool (bool) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.CaseInsensitive as CI import Data.Default.Class (def) import Data.Int +import Data.String (IsString(..)) import qualified Data.Text as ST import qualified Data.Text.Encoding as STE import qualified Data.Text.Lazy as T @@ -80,51 +85,74 @@ import Network.Wai (Request, Response, StreamingBody, Application, req import Numeric.Natural import Prelude () -import Prelude.Compat +import "base-compat-batteries" Prelude.Compat import Web.Scotty.Internal.Types -import Web.Scotty.Util (mkResponse, addIfNotPresent, add, replace, lazyTextToStrictByteString, strictByteStringToLazyText, catch, catchAny, tryAny) +import Web.Scotty.Util (mkResponse, addIfNotPresent, add, replace, lazyTextToStrictByteString, strictByteStringToLazyText, catch, catches, catchesOptionally, catchAny, try, tryAny) import Network.Wai.Internal (ResponseReceived(..)) --- Nothing indicates route failed (due to Next) and pattern matching should continue. +-- | Nothing indicates route failed (due to Next) and pattern matching should continue. -- Just indicates a successful response. -- runAction :: (ScottyError e, Monad m) => ErrorHandler e m -> ActionEnv -> ActionT e m () -> m (Maybe Response) --- runAction :: MonadUnliftIO m => --- Maybe a -> ActionEnv -> ActionT e m () -> m (Maybe Response) runAction :: MonadUnliftIO m => - Maybe (AErr -> ActionT e m ()) - -> ActionEnv -> ActionT e m () -> m (Maybe Response) -runAction h env action = do - ei <- flip runReaderT env $ runAM $ tryAny (action `catch` defH h) + Maybe (ErrorHandler m) -- ^ if present, this handler is in charge of user-defined exceptions + -> ActionEnv -> ActionT m () -> m (Maybe Response) +runAction mh env action = do + ok <- flip runReaderT env $ runAM $ tryNext (catchesOptionally action mh actionErrorHandler ) res <- getResponse env - return $ either (const Nothing) (const $ Just $ mkResponse res) ei + return $ bool Nothing (Just $ mkResponse res) ok + -- return $ either (const Nothing) (const $ Just $ mkResponse res) ei + +{-| +* ActionError should only be caught by runAction +* handlers should not throw ActionError (= do not export ActionError constructors) +-} --- | Default error handler for all actions. --- defH :: (Monad m) => ErrorHandler e m -> ActionError -> ActionT e m () --- defH :: MonadIO m => Maybe a -> ActionError -> ActionT e m () -defH :: MonadUnliftIO m => Maybe (AErr -> ActionT e m ()) -> ActionError -> ActionT e m () -defH _ (Redirect url) = do +tryNext :: MonadUnliftIO m => m a -> m Bool +tryNext io = catch (io >> pure True) $ \e -> + case e of + Next -> pure False + _ -> pure True + +-- | Error handler in charge of 'ActionError'. Rethrowing 'Next' here is caught by 'tryNext' +actionErrorHandler :: MonadIO m => ErrorHandler m -- ActionError -> ActionT m () +actionErrorHandler = Handler $ \case + Redirect url -> do status status302 setHeader "Location" url -defH Nothing (ActionError s e) = do + StatusError s e -> do status s let code = T.pack $ show $ statusCode s let msg = T.fromStrict $ STE.decodeUtf8 $ statusMessage s - html $ mconcat ["

", code, " ", msg, "

", T.pack (show e)] -defH h@(Just f) (ActionError _ e) = f e `catch` (defH h) -- so handlers can throw exceptions themselves -- TODO -defH _ Next = next -defH _ Finish = return () + html $ mconcat ["

", code, " ", msg, "

", e] + Next -> next + Finish -> return () + +-- -- defH :: (Monad m) => ErrorHandler e m -> ActionError -> ActionT e m () +-- defH :: MonadUnliftIO m => Maybe (AErr -> ActionT m ()) -> ActionError -> ActionT m () +-- defH _ (Redirect url) = do +-- status status302 +-- setHeader "Location" url +-- defH Nothing (ActionError s e) = do +-- status s +-- let code = T.pack $ show $ statusCode s +-- let msg = T.fromStrict $ STE.decodeUtf8 $ statusMessage s +-- html $ mconcat ["

", code, " ", msg, "

", T.pack (show e)] +-- defH h@(Just f) (ActionError _ e) = f e `catch` (defH h) -- so handlers can throw exceptions themselves -- TODO +-- defH _ Next = next -- rethrow 'Next' +-- defH _ Finish = return () -- stop -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions -- turn into HTTP 500 responses. -raise :: (Monad m) => AErr -> ActionT e m a -raise = raiseStatus status500 +-- raise :: (Monad m) => AErr -> ActionT m a +raise :: (MonadIO m, E.Exception e) => e -> ActionT m a +raise = E.throw -- raiseStatus status500 -- | Throw an exception, which can be caught with 'rescue'. Uncaught exceptions turn into HTTP responses corresponding to the given status. -raiseStatus :: (Monad m) => Status -> AErr -> ActionT e m a -raiseStatus s = E.throw . ActionError s +raiseStatus :: (Monad m) => Status -> T.Text -> ActionT m a +raiseStatus s = E.throw . StatusError s -- | Abort execution of this action and continue pattern matching routes. @@ -141,23 +169,21 @@ raiseStatus s = E.throw . ActionError s -- > get "/foo/:baz" $ do -- > w <- param "baz" -- > text $ "You made a request to: " <> w -next :: (Monad m) => ActionT e m a +next :: (Monad m) => ActionT m a next = E.throw Next -- | Catch an exception thrown by 'raise'. -- --- > raise "just kidding" `rescue` (\msg -> text msg) -rescue :: (MonadUnliftIO m) => ActionT e m a -> (AErr -> ActionT e m a) -> ActionT e m a -rescue action h = catch action $ \e -> case e of - ActionError _ err -> h err -- handle errors - other -> E.throw other -- rethrow internal error types - --- | Like 'liftIO', but catch any IO exceptions and turn them into 'ScottyError's. -liftAndCatchIO = undefined -- FIXME --- liftAndCatchIO :: (ScottyError e, MonadIO m) => IO a -> ActionT e m a --- liftAndCatchIO io = ActionT $ do --- r <- liftIO $ liftM Right io `E.catch` (\ e -> return $ Left $ stringError $ show (e :: E.SomeException)) --- either throwError return r +-- > raise JustKidding `rescue` (\msg -> text msg) +rescue :: (MonadUnliftIO m, E.Exception e) => ActionT m a -> (e -> ActionT m a) -> ActionT m a +rescue = catch + +-- | Catch any synchronous IO exceptions +liftAndCatchIO :: MonadIO m => IO a -> ActionT m a +liftAndCatchIO io = liftIO $ do + r <- tryAny io + either E.throwIO pure r + -- | Redirect to given URL. Like throwing an uncatchable exception. Any code after the call to redirect -- will not be run. @@ -167,7 +193,7 @@ liftAndCatchIO = undefined -- FIXME -- OR -- -- > redirect "/foo/bar" -redirect :: (Monad m) => T.Text -> ActionT e m a +redirect :: (Monad m) => T.Text -> ActionT m a redirect = E.throw . Redirect -- | Finish the execution of the current action. Like throwing an uncatchable @@ -175,25 +201,25 @@ redirect = E.throw . Redirect -- -- /Since: 0.10.3/ -- finish :: (ScottyError e, Monad m) => ActionT e m a -finish :: (Monad m) => ActionT e m a +finish :: (Monad m) => ActionT m a finish = E.throw Finish -- | Get the 'Request' object. -request :: Monad m => ActionT e m Request +request :: Monad m => ActionT m Request request = ActionT $ envReq <$> ask -- | Get list of uploaded files. -files :: Monad m => ActionT e m [File] +files :: Monad m => ActionT m [File] files = ActionT $ envFiles <$> ask -- | Get a request header. Header name is case-insensitive. -header :: (Monad m) => T.Text -> ActionT e m (Maybe T.Text) +header :: (Monad m) => T.Text -> ActionT m (Maybe T.Text) header k = do hs <- requestHeaders <$> request return $ fmap strictByteStringToLazyText $ lookup (CI.mk (lazyTextToStrictByteString k)) hs -- | Get all the request headers. Header names are case-insensitive. -headers :: (Monad m) => ActionT e m [(T.Text, T.Text)] +headers :: (Monad m) => ActionT m [(T.Text, T.Text)] headers = do hs <- requestHeaders <$> request return [ ( strictByteStringToLazyText (CI.original k) @@ -201,13 +227,13 @@ headers = do | (k,v) <- hs ] -- | Get the request body. -body :: (ScottyError e, MonadIO m) => ActionT e m BL.ByteString +body :: (MonadIO m) => ActionT m BL.ByteString body = ActionT ask >>= (liftIO . envBody) -- | Get an IO action that reads body chunks -- -- * This is incompatible with 'body' since 'body' consumes all chunks. -bodyReader :: Monad m => ActionT e m (IO B.ByteString) +bodyReader :: Monad m => ActionT m (IO B.ByteString) bodyReader = ActionT $ envBodyChunk <$> ask -- | Parse the request body as a JSON object and return it. @@ -219,27 +245,26 @@ bodyReader = ActionT $ envBodyChunk <$> ask -- 422 Unprocessable Entity. -- -- These status codes are as per https://www.restapitutorial.com/httpstatuscodes.html. -jsonData = undefined --- jsonData :: (A.FromJSON a, ScottyError e, MonadIO m) => ActionT e m a --- jsonData = do --- b <- body --- when (b == "") $ do --- let htmlError = "jsonData - No data was provided." --- raiseStatus status400 $ stringError htmlError --- case A.eitherDecode b of --- Left err -> do --- let htmlError = "jsonData - malformed." --- `mappend` " Data was: " `mappend` BL.unpack b --- `mappend` " Error was: " `mappend` err --- raiseStatus status400 $ stringError htmlError --- Right value -> case A.fromJSON value of --- A.Error err -> do --- let htmlError = "jsonData - failed parse." --- `mappend` " Data was: " `mappend` BL.unpack b `mappend` "." --- `mappend` " Error was: " `mappend` err --- raiseStatus status422 $ stringError htmlError --- A.Success a -> do --- return a +jsonData :: (A.FromJSON a, MonadIO m) => ActionT m a +jsonData = do + b <- body + when (b == "") $ do + let htmlError = "jsonData - No data was provided." + raiseStatus status400 $ T.pack htmlError + case A.eitherDecode b of + Left err -> do + let htmlError = "jsonData - malformed." + `mappend` " Data was: " `mappend` BL.unpack b + `mappend` " Error was: " `mappend` err + raiseStatus status400 $ T.pack htmlError + Right value -> case A.fromJSON value of + A.Error err -> do + let htmlError = "jsonData - failed parse." + `mappend` " Data was: " `mappend` BL.unpack b `mappend` "." + `mappend` " Error was: " `mappend` err + raiseStatus status422 $ T.pack htmlError + A.Success a -> do + return a -- | Get a parameter. First looks in captures, then form data, then query parameters. -- @@ -248,11 +273,11 @@ jsonData = undefined -- * If parameter is found, but 'parseParam' 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, ScottyError e, Monad m) => T.Text -> ActionT e m a +param :: (Parsable a, MonadIO m) => T.Text -> ActionT m a param k = do val <- ActionT $ (lookup k . getParams) <$> ask case val of - -- Nothing -> raise $ stringError $ "Param: " ++ T.unpack k ++ " not found!" -- FIXME + Nothing -> raiseStatus status500 $ "Param: " <> k <> " not found!" -- FIXME Just v -> either (const next) return $ parseParam v {-# DEPRECATED param "(#204) Not a good idea to treat all parameters identically. Use captureParam, formParam and queryParam instead. "#-} @@ -262,7 +287,7 @@ param k = do -- -- * If the parameter is found, but 'parseParam' fails to parse to the correct type, 'next' is called. -- captureParam :: (Parsable a, ScottyError e, Monad m) => T.Text -> ActionT e m a -captureParam :: (Parsable a, Monad m) => T.Text -> ActionT e m a +captureParam :: (Parsable a, Monad m) => T.Text -> ActionT m a captureParam = paramWith CaptureParam envCaptureParams status500 -- | Get a form parameter. @@ -271,7 +296,7 @@ captureParam = paramWith CaptureParam envCaptureParams status500 -- -- * This function raises a code 400 also if the parameter is found, but 'parseParam' fails to parse to the correct type. -- formParam :: (Parsable a, ScottyError e, Monad m) => T.Text -> ActionT e m a -formParam :: (Parsable a, Monad m) => T.Text -> ActionT e m a +formParam :: (Parsable a, Monad m) => T.Text -> ActionT m a formParam = paramWith FormParam envFormParams status400 -- | Get a query parameter. @@ -280,7 +305,7 @@ formParam = paramWith FormParam envFormParams status400 -- -- * This function raises a code 400 also if the parameter is found, but 'parseParam' fails to parse to the correct type. -- queryParam :: (Parsable a, ScottyError e, Monad m) => T.Text -> ActionT e m a -queryParam :: (Parsable a, Monad m) => T.Text -> ActionT e m a +queryParam :: (Parsable a, Monad m) => T.Text -> ActionT m a queryParam = paramWith QueryParam envQueryParams status400 data ParamType = CaptureParam @@ -297,33 +322,33 @@ paramWith :: (Monad m, Parsable b) => -> (ActionEnv -> [Param]) -> Status -- ^ HTTP status to return if parameter is not found -> T.Text -- ^ parameter name - -> ActionT e m b + -> ActionT m b paramWith ty f err k = do val <- ActionT $ (lookup k . f) <$> ask case val of - -- Nothing -> raiseStatus err $ stringError (unwords [show ty, "parameter:", T.unpack k, "not found!"]) -- FIXME + Nothing -> raiseStatus err (T.unwords [T.pack (show ty), "parameter:", k, "not found!"]) Just v -> let handleParseError = \case CaptureParam -> next - -- _ -> raiseStatus err $ stringError (unwords ["Cannot parse", T.unpack v, "as a", show ty, "parameter"]) -- FIXME + _ -> raiseStatus err (T.unwords ["Cannot parse", v, "as a", T.pack (show ty), "parameter"]) in either (const $ handleParseError ty) return $ parseParam v -- | Get all parameters from capture, form and query (in that order). -params :: Monad m => ActionT e m [Param] +params :: Monad m => ActionT m [Param] params = paramsWith getParams {-# DEPRECATED params "(#204) Not a good idea to treat all parameters identically. Use captureParams, formParams and queryParams instead. "#-} -- | Get capture parameters -captureParams :: Monad m => ActionT e m [Param] +captureParams :: Monad m => ActionT m [Param] captureParams = paramsWith envCaptureParams -- | Get form parameters -formParams :: Monad m => ActionT e m [Param] +formParams :: Monad m => ActionT m [Param] formParams = paramsWith envFormParams -- | Get query parameters -queryParams :: Monad m => ActionT e m [Param] +queryParams :: Monad m => ActionT m [Param] queryParams = paramsWith envQueryParams -paramsWith :: Monad m => (ActionEnv -> a) -> ActionT e m a +paramsWith :: Monad m => (ActionEnv -> a) -> ActionT m a paramsWith f = ActionT (f <$> ask) {-# DEPRECATED getParams "(#204) Not a good idea to treat all parameters identically" #-} @@ -394,35 +419,35 @@ 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 = undefined -- FIXME +status :: MonadIO m => Status -> ActionT m () +status = modifyResponse . setStatus -- Not exported, but useful in the functions below. changeHeader :: MonadIO m => (CI.CI B.ByteString -> B.ByteString -> [(HeaderName, B.ByteString)] -> [(HeaderName, B.ByteString)]) - -> T.Text -> T.Text -> ActionT e m () + -> T.Text -> T.Text -> ActionT m () changeHeader f k = modifyResponse . setHeaderWith . f (CI.mk $ lazyTextToStrictByteString k) . lazyTextToStrictByteString -- | Add to the response headers. Header names are case-insensitive. -addHeader :: MonadIO m => T.Text -> T.Text -> ActionT e m () +addHeader :: MonadIO m => T.Text -> T.Text -> ActionT m () addHeader = changeHeader add -- | Set one of the response headers. Will override any previously set value for that header. -- Header names are case-insensitive. -setHeader :: MonadIO m => T.Text -> T.Text -> ActionT e m () +setHeader :: MonadIO m => T.Text -> T.Text -> ActionT m () setHeader = changeHeader replace -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/plain; charset=utf-8\" if it has not already been set. -text :: (MonadIO m) => T.Text -> ActionT e m () +text :: (MonadIO m) => T.Text -> ActionT m () text t = do changeHeader addIfNotPresent "Content-Type" "text/plain; charset=utf-8" raw $ encodeUtf8 t -- | Set the body of the response to the given 'T.Text' value. Also sets \"Content-Type\" -- header to \"text/html; charset=utf-8\" if it has not already been set. -html :: (MonadIO m) => T.Text -> ActionT e m () +html :: (MonadIO m) => T.Text -> ActionT m () html t = do changeHeader addIfNotPresent "Content-Type" "text/html; charset=utf-8" raw $ encodeUtf8 t @@ -430,15 +455,15 @@ html t = do -- | Send a file as the response. Doesn't set the \"Content-Type\" header, so you probably -- want to do that on your own with 'setHeader'. Setting a status code will have no effect -- because Warp will overwrite that to 200 (see 'Network.Wai.Handler.Warp.Internal.sendResponse'). -file :: Monad m => FilePath -> ActionT e m () -file = undefined -- FIXME +file :: MonadIO m => FilePath -> ActionT m () +file = modifyResponse . setContent . ContentFile -rawResponse :: Monad m => Response -> ActionT e m () -rawResponse = undefined -- FIXME +rawResponse :: MonadIO m => Response -> ActionT m () +rawResponse = modifyResponse . setContent . ContentResponse -- | Set the body of the response to the JSON encoding of the given value. Also sets \"Content-Type\" -- header to \"application/json; charset=utf-8\" if it has not already been set. -json :: (A.ToJSON a, MonadIO m) => a -> ActionT e m () +json :: (A.ToJSON a, MonadIO m) => a -> ActionT m () json v = do changeHeader addIfNotPresent "Content-Type" "application/json; charset=utf-8" raw $ A.encode v @@ -446,18 +471,18 @@ 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 'setHeader'. -stream :: MonadIO m => StreamingBody -> ActionT e m () +stream :: MonadIO m => StreamingBody -> ActionT m () stream = modifyResponse . setContent . ContentStream -- | 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 'setHeader'. -raw :: MonadIO m => BL.ByteString -> ActionT e m () +raw :: MonadIO m => BL.ByteString -> ActionT m () raw = modifyResponse . setContent . ContentBuilder . fromLazyByteString -- | Nest a whole WAI application inside a Scotty handler. -- See Web.Scotty for further documentation -nested :: (MonadIO m) => Network.Wai.Application -> ActionT e m () +nested :: (MonadIO m) => Network.Wai.Application -> ActionT m () nested app = do -- Is MVar really the best choice here? Not sure. r <- request diff --git a/Web/Scotty/Cookie.hs b/Web/Scotty/Cookie.hs index c9a3d0e9..aaea7553 100644 --- a/Web/Scotty/Cookie.hs +++ b/Web/Scotty/Cookie.hs @@ -92,7 +92,7 @@ import qualified Data.Text.Lazy.Encoding as TL (encodeUtf8, decodeUtf8) -- | Set a cookie, with full access to its options (see 'SetCookie') setCookie :: (MonadIO m) => SetCookie - -> ActionT e m () + -> ActionT m () setCookie c = addHeader "Set-Cookie" (TL.decodeUtf8 . toLazyByteString $ renderSetCookie c) @@ -100,26 +100,26 @@ setCookie c = addHeader "Set-Cookie" (TL.decodeUtf8 . toLazyByteString $ renderS setSimpleCookie :: (MonadIO m) => Text -- ^ name -> Text -- ^ value - -> ActionT e m () + -> ActionT m () setSimpleCookie n v = setCookie $ makeSimpleCookie n v -- | Lookup one cookie name getCookie :: (Monad m) => Text -- ^ name - -> ActionT e m (Maybe Text) + -> ActionT m (Maybe Text) getCookie c = lookup c <$> getCookies -- | Returns all cookies getCookies :: (Monad m) - => ActionT e m CookiesText + => ActionT m CookiesText getCookies = (maybe [] parse) <$> header "Cookie" where parse = parseCookiesText . BSL.toStrict . TL.encodeUtf8 -- | Browsers don't directly delete a cookie, but setting its expiry to a past date (e.g. the UNIX epoch) ensures that the cookie will be invalidated (whether and when it will be actually deleted by the browser seems to be browser-dependent). deleteCookie :: (MonadIO m) => Text -- ^ name - -> ActionT e m () + -> ActionT m () deleteCookie c = setCookie $ (makeSimpleCookie c "") { setCookieExpires = Just $ posixSecondsToUTCTime 0 } diff --git a/Web/Scotty/Internal/Types.hs b/Web/Scotty/Internal/Types.hs index 03aca168..9f757b69 100644 --- a/Web/Scotty/Internal/Types.hs +++ b/Web/Scotty/Internal/Types.hs @@ -1,9 +1,12 @@ +{-# LANGUAGE PackageImports #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} +{-# language ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE RecordWildCards #-} @@ -15,12 +18,11 @@ import Blaze.ByteString.Builder (Builder) import Control.Applicative import Control.Concurrent.MVar import Control.Concurrent.STM (STM, TVar, atomically, newTVarIO, readTVarIO, readTVar, writeTVar, modifyTVar') -import Control.Exception (Exception) import qualified Control.Exception as E import qualified Control.Monad as Monad import Control.Monad (MonadPlus(..)) import Control.Monad.Base (MonadBase, liftBase, liftBaseDefault) -import Control.Monad.Catch (MonadCatch, catch, MonadThrow, throwM) +import Control.Monad.Catch (MonadCatch, MonadThrow) import Control.Monad.Error.Class import qualified Control.Monad.Fail as Fail import Control.Monad.IO.Class (MonadIO(..)) @@ -47,7 +49,7 @@ import Network.Wai.Handler.Warp (Settings, defaultSettings) import Network.Wai.Parse (FileInfo) import Prelude () -import Prelude.Compat +import "base-compat-batteries" Prelude.Compat --------------------- Options ----------------------- data Options = Options { verbose :: Int -- ^ 0 = silent, 1(def) = startup banner @@ -60,11 +62,13 @@ data Options = Options { verbose :: Int -- ^ 0 = silent, 1(def) = startup banner -- servers using `setFdCacheDuration`. } +defaultOptions :: Options defaultOptions = Options 1 defaultSettings newtype RouteOptions = RouteOptions { maxRequestBodySize :: Maybe Kilobytes -- max allowed request size in KB } +defaultRouteOptions :: RouteOptions defaultRouteOptions = RouteOptions Nothing type Kilobytes = Int @@ -87,75 +91,75 @@ data BodyInfo = BodyInfo { bodyInfoReadProgress :: MVar Int -- ^ index into the --------------- Scotty Applications ----------------- -data ScottyState e m = +data ScottyState m = ScottyState { middlewares :: [Wai.Middleware] , routes :: [BodyInfo -> Middleware m] - , handler :: Maybe (AErr -> ActionT e m ()) -- ErrorHandler e m + , handler :: Maybe (ErrorHandler m) -- Maybe (AErr -> ActionT m ()) -- ErrorHandler e m , routeOptions :: RouteOptions } -defaultScottyState :: ScottyState e m +defaultScottyState :: ScottyState m defaultScottyState = ScottyState [] [] Nothing defaultRouteOptions -addMiddleware :: Wai.Middleware -> ScottyState e m -> ScottyState e m +addMiddleware :: Wai.Middleware -> ScottyState m -> ScottyState m addMiddleware m s@(ScottyState {middlewares = ms}) = s { middlewares = m:ms } -addRoute :: (BodyInfo -> Middleware m) -> ScottyState e m -> ScottyState e m +addRoute :: (BodyInfo -> Middleware m) -> ScottyState m -> ScottyState m addRoute r s@(ScottyState {routes = rs}) = s { routes = r:rs } -addHandler :: Maybe (AErr -> ActionT e m ()) -> ScottyState e m -> ScottyState e m -addHandler h s = s { handler = h } +setHandler :: Maybe (ErrorHandler m) -> ScottyState m -> ScottyState m +setHandler h s = s { handler = h } -updateMaxRequestBodySize :: RouteOptions -> ScottyState e m -> ScottyState e m +updateMaxRequestBodySize :: RouteOptions -> ScottyState m -> ScottyState m updateMaxRequestBodySize RouteOptions { .. } s@ScottyState { routeOptions = ro } = let ro' = ro { maxRequestBodySize = maxRequestBodySize } in s { routeOptions = ro' } -newtype ScottyT e m a = ScottyT { runS :: State (ScottyState e m) a } +newtype ScottyT m a = ScottyT { runS :: State (ScottyState m) a } deriving ( Functor, Applicative, Monad ) ------------------ Scotty Errors -------------------- --- data ActionError e --- = Redirect Text --- | Next --- | Finish --- | ActionError Status e --- deriving (Show, Typeable) --- instance (Typeable e, Show e) => Exception (ActionError e) + data ActionError = Redirect Text | Next | Finish - | ActionError Status AErr + | StatusError Status Text -- e.g. 422 Unprocessable Entity when JSON body parsing fails + -- | SomeActionError Status AErr deriving (Show, Typeable) -instance Exception ActionError +instance E.Exception ActionError --- | FIXME placeholder for a more informative concrete error type -newtype AErr = AErr Text deriving (Show, Typeable) +-- actionErrorHandler = Handler (\(e1 :: ActionError e)) --- | In order to use a custom exception type (aside from 'Text'), you must --- define an instance of 'ScottyError' for that type. -class ScottyError e where - stringError :: String -> e - showError :: e -> Text +-- | Handler for a specific type of exception, see 'handleActionError' +-- +-- TODO export the constructor to the user since they will need to implement their +-- own handler +data Handler m a = forall e . E.Exception e => Handler (e -> m a) +instance Monad m => Functor (Handler m) where + fmap f (Handler h) = Handler (\e -> f <$> h e) -instance ScottyError Text where - stringError = pack - showError = id --- instance ScottyError e => ScottyError (ActionError e) where --- stringError = ActionError status500 . stringError --- showError (Redirect url) = url --- showError Next = pack "Next" --- showError Finish = pack "Finish" --- showError (ActionError _ e) = showError e -type ErrorHandler e m = Maybe (e -> ActionT e m ()) +-- -- | FIXME placeholder for a more informative concrete error type +-- newtype AErr = AErr { aErrText :: Text } deriving (Show, Typeable) +-- instance E.Exception AErr +-- instance IsString AErr where +-- fromString = AErr . pack -data ScottyException = RequestException BS.ByteString Status deriving (Show, Typeable) -instance Exception ScottyException +-- -- | In order to use a custom exception type (aside from 'Text'), you must +-- -- define an instance of 'ScottyError' for that type. +-- class ScottyError e where +-- stringError :: String -> e +-- showError :: e -> Text + +type ErrorHandler m = Handler (ActionT m) () +-- type ErrorHandler e m = Maybe (e -> ActionT e m ()) + +data ScottyException = RequestException BS.ByteString Status deriving (Show, Typeable) +instance E.Exception ScottyException ------------------ Scotty Actions ------------------- type Param = (Text, Text) @@ -175,7 +179,7 @@ data ActionEnv = Env { envReq :: Request getResponse :: MonadIO m => ActionEnv -> m ScottyResponse getResponse ae = liftIO $ readTVarIO (envResponse ae) -modifyResponse :: (MonadIO m) => (ScottyResponse -> ScottyResponse) -> ActionT e m () +modifyResponse :: (MonadIO m) => (ScottyResponse -> ScottyResponse) -> ActionT m () modifyResponse f = do tv <- asks envResponse liftIO $ atomically $ modifyTVar' tv f @@ -207,10 +211,10 @@ defaultScottyResponse :: ScottyResponse defaultScottyResponse = SR status200 [] (ContentBuilder mempty) -newtype ActionT e m a = ActionT { runAM :: ReaderT ActionEnv m a } +newtype ActionT m a = ActionT { runAM :: ReaderT ActionEnv m a } deriving newtype (Functor, Applicative, Alternative, Monad, MonadIO, MonadReader ActionEnv, MonadTrans, MonadThrow, MonadCatch, MonadBase b, MonadBaseControl b, MonadTransControl, MonadUnliftIO) -instance (Semigroup a) => Semigroup (ScottyT e m a) where +instance (Semigroup a) => Semigroup (ScottyT m a) where x <> y = (<>) <$> x <*> y instance @@ -221,7 +225,7 @@ instance #if !(MIN_VERSION_base(4,8,0)) , Functor m #endif - ) => Monoid (ScottyT e m a) where + ) => Monoid (ScottyT m a) where mempty = return mempty #if !(MIN_VERSION_base(4,11,0)) mappend = (<>) @@ -233,18 +237,18 @@ instance , Functor m #endif , Semigroup a - ) => Semigroup (ActionT e m a) where + ) => Semigroup (ActionT m a) where x <> y = (<>) <$> x <*> y instance - ( Monad m, ScottyError e, Monoid a + ( Monad m, Monoid a #if !(MIN_VERSION_base(4,11,0)) , Semigroup a #endif #if !(MIN_VERSION_base(4,8,0)) , Functor m #endif - ) => Monoid (ActionT e m a) where + ) => Monoid (ActionT m a) where mempty = return mempty #if !(MIN_VERSION_base(4,11,0)) mappend = (<>) diff --git a/Web/Scotty/Route.hs b/Web/Scotty/Route.hs index f2f0d3c7..73388d48 100644 --- a/Web/Scotty/Route.hs +++ b/Web/Scotty/Route.hs @@ -1,5 +1,6 @@ {-# LANGUAGE FlexibleContexts, FlexibleInstances, OverloadedStrings, RankNTypes, ScopedTypeVariables #-} +{-# language PackageImports #-} module Web.Scotty.Route ( get, post, put, delete, patch, options, addroute, matchAny, notFound, capture, regex, function, literal @@ -22,46 +23,46 @@ import Network.HTTP.Types import Network.Wai (Request(..)) import Prelude () -import Prelude.Compat +import "base-compat-batteries" Prelude.Compat import qualified Text.Regex as Regex import Web.Scotty.Action -import Web.Scotty.Internal.Types (RoutePattern(..), RouteOptions, ActionEnv(..), ActionT, ScottyState(..), ScottyT(..), Middleware, BodyInfo, ScottyError(..), ErrorHandler, AErr, handler, addRoute, defaultScottyResponse) +import Web.Scotty.Internal.Types (RoutePattern(..), RouteOptions, ActionEnv(..), ActionT, ScottyState(..), ScottyT(..), ErrorHandler, Middleware, BodyInfo, handler, addRoute, defaultScottyResponse) import Web.Scotty.Util (strictByteStringToLazyText) import Web.Scotty.Body (cloneBodyInfo, getBodyAction, getBodyChunkAction, getFormParamsAndFilesAction) -- | get = 'addroute' 'GET' -get :: (MonadUnliftIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () +get :: (MonadUnliftIO m) => RoutePattern -> ActionT m () -> ScottyT m () get = addroute GET -- | post = 'addroute' 'POST' -post :: (MonadUnliftIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () +post :: (MonadUnliftIO m) => RoutePattern -> ActionT m () -> ScottyT m () post = addroute POST -- | put = 'addroute' 'PUT' -put :: (MonadUnliftIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () +put :: (MonadUnliftIO m) => RoutePattern -> ActionT m () -> ScottyT m () put = addroute PUT -- | delete = 'addroute' 'DELETE' -delete :: (MonadUnliftIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () +delete :: (MonadUnliftIO m) => RoutePattern -> ActionT m () -> ScottyT m () delete = addroute DELETE -- | patch = 'addroute' 'PATCH' -patch :: (MonadUnliftIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () +patch :: (MonadUnliftIO m) => RoutePattern -> ActionT m () -> ScottyT m () patch = addroute PATCH -- | options = 'addroute' 'OPTIONS' -options :: (MonadUnliftIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () +options :: (MonadUnliftIO m) => RoutePattern -> ActionT m () -> ScottyT m () options = addroute OPTIONS -- | Add a route that matches regardless of the HTTP verb. -matchAny :: (MonadUnliftIO m) => RoutePattern -> ActionT e m () -> ScottyT e m () -matchAny pattern action = ScottyT $ MS.modify $ \s -> addRoute (route (routeOptions s) (handler s) Nothing pattern action) s +matchAny :: (MonadUnliftIO m) => RoutePattern -> ActionT m () -> ScottyT m () +matchAny pat action = ScottyT $ MS.modify $ \s -> addRoute (route (routeOptions s) (handler s) Nothing pat action) s -- | Specify an action to take if nothing else is found. Note: this _always_ matches, -- so should generally be the last route specified. -notFound :: (MonadUnliftIO m) => ActionT e m () -> ScottyT e m () +notFound :: (MonadUnliftIO m) => ActionT m () -> ScottyT 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, @@ -70,25 +71,28 @@ notFound action = matchAny (Function (\req -> Just [("path", path req)])) (statu -- > addroute GET "/" $ text "beam me up!" -- -- The path spec can include values starting with a colon, which are interpreted --- as /captures/. These are named wildcards that can be looked up with 'param'. +-- as /captures/. These are named wildcards that can be looked up with 'captureParam'. -- -- > addroute GET "/foo/:bar" $ do --- > v <- param "bar" +-- > v <- captureParam "bar" -- > text v -- -- >>> curl http://localhost:3000/foo/something -- something -addroute :: (MonadUnliftIO m) => StdMethod -> RoutePattern -> ActionT e m () -> ScottyT e m () +-- +-- NB: the 'RouteOptions' and the exception handler of the newly-created route will be +-- copied from the previously-created routes. +addroute :: (MonadUnliftIO m) => StdMethod -> RoutePattern -> ActionT m () -> ScottyT m () addroute method pat action = ScottyT $ MS.modify $ \s -> addRoute (route (routeOptions s) (handler s) (Just method) pat action) s route :: (MonadUnliftIO m) => RouteOptions - -> Maybe (AErr -> ActionT e m()) -> Maybe StdMethod -> RoutePattern -> ActionT e m () -> BodyInfo -> Middleware m + -> Maybe (ErrorHandler m) -> Maybe StdMethod -> RoutePattern -> ActionT m () -> BodyInfo -> Middleware m route opts h method pat action bodyInfo app req = let tryNext = app req {- | We match all methods in the case where 'method' is 'Nothing'. - See https://github.com/scotty-web/scotty/issues/196 + See https://github.com/scotty-web/scotty/issues/196 and 'matchAny' -} methodMatches :: Bool methodMatches = maybe True (\x -> (Right x == parseMethod (requestMethod req))) method diff --git a/Web/Scotty/Trans.hs b/Web/Scotty/Trans.hs index 8324a3a9..79b84d6e 100644 --- a/Web/Scotty/Trans.hs +++ b/Web/Scotty/Trans.hs @@ -11,7 +11,7 @@ -- the comments on each of these functions for more information. module Web.Scotty.Trans ( -- * scotty-to-WAI - scottyT, scottyAppT, scottyOptsT, scottySocketT, Options(..) + scottyT, scottyAppT, scottyOptsT, scottySocketT, Options(..), Scotty.defaultOptions -- * Defining Middleware and Routes -- -- | 'Middleware' and routes are run in the order in which they @@ -34,7 +34,7 @@ module Web.Scotty.Trans -- definition, as they completely replace the current 'Response' body. , text, html, file, json, stream, raw, nested -- ** Exceptions - , raise, raiseStatus, rescue, next, finish, defaultHandler, ScottyError(..), liftAndCatchIO + , raise, raiseStatus, rescue, next, finish, defaultHandler, liftAndCatchIO -- * Parsing Parameters , Param, Parsable(..), readEither -- * Types @@ -69,7 +69,7 @@ import Web.Scotty.Body (newBodyInfo) scottyT :: (Monad m, MonadIO n) => Port -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. - -> ScottyT e m () + -> ScottyT m () -> n () scottyT p = scottyOptsT $ defaultOptions { settings = setPort p (settings defaultOptions) } @@ -78,7 +78,7 @@ scottyT p = scottyOptsT $ defaultOptions { settings = setPort p (settings defaul scottyOptsT :: (Monad m, MonadIO n) => Options -> (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. - -> ScottyT e m () + -> ScottyT m () -> n () scottyOptsT opts runActionToIO s = do when (verbose opts > 0) $ @@ -92,7 +92,7 @@ scottySocketT :: (Monad m, MonadIO n) => Options -> Socket -> (m Response -> IO Response) - -> ScottyT e m () + -> ScottyT m () -> n () scottySocketT opts sock runActionToIO s = do when (verbose opts > 0) $ do @@ -105,7 +105,7 @@ scottySocketT opts sock runActionToIO s = do -- NB: scottyApp === scottyAppT id scottyAppT :: (Monad m, Monad n) => (m Response -> IO Response) -- ^ Run monad 'm' into 'IO', called at each action. - -> ScottyT e m () + -> ScottyT m () -> n Application scottyAppT runActionToIO defs = do let s = execState (runS defs) defaultScottyState @@ -121,26 +121,18 @@ notFoundApp :: Monad m => Scotty.Application m notFoundApp _ = return $ responseBuilder status404 [("Content-Type","text/html")] $ fromByteString "

404: File Not Found!

" --- | Global handler for uncaught exceptions. --- --- Uncaught exceptions normally become 500 responses. --- You can use this to selectively override that behavior. --- --- Note: IO exceptions are lifted into 'ScottyError's by 'stringError'. --- This has security implications, so you probably want to provide your --- own defaultHandler in production which does not send out the error --- strings as 500 responses. -defaultHandler :: (Monad m) => (AErr -> ActionT e m ()) -> ScottyT e m () -defaultHandler f = ScottyT $ modify $ addHandler $ Just (\e -> status status500 >> f e) +-- | Global handler for user-defined exceptions. +defaultHandler :: (Monad m) => (ErrorHandler m) -> ScottyT m () +defaultHandler f = ScottyT $ modify $ setHandler $ 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 :: Middleware -> ScottyT e m () +middleware :: Middleware -> ScottyT m () middleware = ScottyT . modify . addMiddleware -- | Set global size limit for the request body. Requests with body size exceeding the limit will not be -- processed and an HTTP response 413 will be returned to the client. Size limit needs to be greater than 0, -- otherwise the application will terminate on start. -setMaxRequestBodySize :: Kilobytes -> ScottyT e m () +setMaxRequestBodySize :: Kilobytes -> ScottyT m () setMaxRequestBodySize i = assert (i > 0) $ ScottyT . modify . updateMaxRequestBodySize $ defaultRouteOptions { maxRequestBodySize = Just i } diff --git a/Web/Scotty/Util.hs b/Web/Scotty/Util.hs index 3ae5ef5a..2ce9f367 100644 --- a/Web/Scotty/Util.hs +++ b/Web/Scotty/Util.hs @@ -10,6 +10,8 @@ module Web.Scotty.Util , readRequestBody -- * exceptions , catch + , catches + , catchesOptionally , catchAny , try , tryAny @@ -19,9 +21,11 @@ import Network.Socket (SockAddr(..), Socket, getSocketName, socketPort) import Network.Wai import Control.Monad (when) +import Control.Monad.IO.Class (MonadIO(..)) import Control.Monad.IO.Unlift (MonadUnliftIO(..)) import Control.Exception (Exception (..), SomeException (..), IOException, SomeAsyncException (..)) -import qualified Control.Exception as EUnsafe (throw, throwIO, catch) +import qualified Control.Exception as EUnsafe (fromException, throw, throwIO, catch) +import Data.Maybe (maybeToList) import Network.HTTP.Types @@ -104,9 +108,23 @@ readRequestBody rbody prefix maxSize = do -- exceptions +catchesOptionally :: MonadUnliftIO m => + m a + -> Maybe (Handler m a) -- ^ if present, this 'Handler' is tried first + -> Handler m a -> m a +catchesOptionally io mh h = io `catches` (maybeToList mh <> [h]) + +catches :: MonadUnliftIO m => m a -> [Handler m a] -> m a +catches io handlers = io `catch` catchesHandler handlers + +catchesHandler :: MonadIO m => [Handler m a] -> SomeException -> m a +catchesHandler handlers e = foldr tryHandler (liftIO (EUnsafe.throwIO e)) handlers + where tryHandler (Handler h) res + = case EUnsafe.fromException e of + Just e' -> h e' + Nothing -> res + -- | (from 'unliftio') Catch a synchronous (but not asynchronous) exception and recover from it. --- --- @since 0.1.0.0 catch :: (MonadUnliftIO m, Exception e) => m a -- ^ action @@ -126,7 +144,7 @@ catchAny = catch -- | (from 'safe-exceptions') Check if the given exception is synchronous isSyncException :: Exception e => e -> Bool isSyncException e = - case fromException (toException e) of + case EUnsafe.fromException (toException e) of Just (SomeAsyncException _) -> False Nothing -> True diff --git a/changelog.md b/changelog.md index 0f6ffa0a..1b68538f 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ * Add `Scotty.Cookie` module. * Change body parsing behaviour such that calls to 'next' don't result in POST request bodies disappearing (#147). * (Internal) Remove unused type RequestBodyState (#313) +* Get rid of data-default-class (#316) https://markkarpov.com/post/data-default.html ## 0.12.1 [2022.11.17] * Fix CPP bug that prevented tests from building on Windows. diff --git a/examples/basic.hs b/examples/basic.hs index c1aa0b6f..1fcac1dd 100644 --- a/examples/basic.hs +++ b/examples/basic.hs @@ -52,7 +52,7 @@ main = scotty 3000 $ do -- Of course you can catch your own errors. get "/rescue" $ do (do void $ raise "a rescued error"; redirect "http://www.we-never-go-here.com") - `rescue` (\m -> text $ "we recovered from " `mappend` m) + `rescue` (\m -> text $ "we recovered from " `mappend` aErrText m) -- Parts of the URL that start with a colon match -- any string, and capture that value as a parameter. diff --git a/examples/exceptions.hs b/examples/exceptions.hs index bddb1a41..cb42b32b 100644 --- a/examples/exceptions.hs +++ b/examples/exceptions.hs @@ -26,7 +26,7 @@ instance ScottyError Except where showError = fromString . show -- Handler for uncaught exceptions. -handleEx :: Monad m => Except -> ActionT Except m () +handleEx :: Monad m => Except -> ActionT m () handleEx Forbidden = do status status403 html "

Scotty Says No

" diff --git a/examples/globalstate.hs b/examples/globalstate.hs index d0ff6fc9..900744b3 100644 --- a/examples/globalstate.hs +++ b/examples/globalstate.hs @@ -10,6 +10,7 @@ module Main (main) where import Control.Concurrent.STM +import Control.Monad.IO.Unlift (MonadUnliftIO(..)) import Control.Monad.Reader import Data.Default.Class @@ -39,7 +40,7 @@ instance Default AppState where -- Also note: your monad must be an instance of 'MonadIO' for -- Scotty to use it. newtype WebM a = WebM { runWebM :: ReaderT (TVar AppState) IO a } - deriving (Applicative, Functor, Monad, MonadIO, MonadReader (TVar AppState)) + deriving (Applicative, Functor, Monad, MonadIO, MonadReader (TVar AppState), MonadUnliftIO) -- Scotty's monads are layered on top of our custom monad. -- We define this synonym for lift in order to be explicit @@ -66,7 +67,7 @@ main = do -- 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 :: ScottyT WebM () app = do middleware logStdoutDev get "/" $ do diff --git a/examples/reader.hs b/examples/reader.hs index bda30e2e..2b363039 100644 --- a/examples/reader.hs +++ b/examples/reader.hs @@ -8,11 +8,11 @@ module Main where import Control.Monad.Reader (MonadIO, MonadReader, ReaderT, asks, lift, runReaderT) -import Data.Default.Class (def) +import Control.Monad.IO.Unlift (MonadUnliftIO(..)) import Data.Text.Lazy (Text, pack) import Prelude () import Prelude.Compat -import Web.Scotty.Trans (ScottyT, get, scottyOptsT, text) +import Web.Scotty.Trans (ScottyT, defaultOptions, get, scottyOptsT, text) data Config = Config { environment :: String @@ -20,16 +20,16 @@ data Config = Config newtype ConfigM a = ConfigM { runConfigM :: ReaderT Config IO a - } deriving (Applicative, Functor, Monad, MonadIO, MonadReader Config) + } deriving (Applicative, Functor, Monad, MonadIO, MonadReader Config, MonadUnliftIO) -application :: ScottyT Text ConfigM () +application :: ScottyT ConfigM () application = do get "/" $ do e <- lift $ asks environment text $ pack $ show e main :: IO () -main = scottyOptsT def runIO application where +main = scottyOptsT defaultOptions runIO application where runIO :: ConfigM a -> IO a runIO m = runReaderT (runConfigM m) config diff --git a/examples/scotty-examples.cabal b/examples/scotty-examples.cabal index 0e98cab3..524c5654 100644 --- a/examples/scotty-examples.cabal +++ b/examples/scotty-examples.cabal @@ -88,6 +88,7 @@ executable scotty-globalstate stm, text, transformers, + unliftio-core >= 0.2, wai-extra GHC-options: -Wall -threaded @@ -117,10 +118,10 @@ executable scotty-reader hs-source-dirs: . build-depends: base >= 4.6 && < 5, base-compat >= 0.11 && < 0.13, - data-default-class, mtl, scotty, - text + text, + unliftio-core >= 0.2 GHC-options: -Wall -threaded executable scotty-upload diff --git a/test/Web/ScottySpec.hs b/test/Web/ScottySpec.hs index cf0e46f0..910f359e 100644 --- a/test/Web/ScottySpec.hs +++ b/test/Web/ScottySpec.hs @@ -5,6 +5,7 @@ import Test.Hspec import Test.Hspec.Wai import Control.Applicative +import qualified Control.Exception as E import Control.Monad import Data.Char import Data.String @@ -87,7 +88,11 @@ spec = do -- it "sets custom exception handler" $ do -- get "/" `shouldRespondWith` "divide by zero" {matchStatus = 500} - withApp (defaultHandler (\_ -> status status503) >> Scotty.get "/" (liftAndCatchIO $ E.throwIO E.DivideByZero)) $ do + describe "defaultHandler" $ do + withApp (do + let h = Handler (\(e :: E.ArithException) -> status status503) + defaultHandler h + Scotty.get "/" (liftAndCatchIO $ E.throwIO E.DivideByZero)) $ do it "allows to customize the HTTP status code" $ do get "/" `shouldRespondWith` "" {matchStatus = 503}