module Web.Scotty.Route
( get, post, put, delete, patch, options, addroute, matchAny, notFound,
capture, regex, function, literal
) where
import Control.Arrow ((***))
import Control.Concurrent.MVar
import Control.Exception (throw)
import Control.Monad.IO.Class
import qualified Control.Monad.State as MS
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Maybe (fromMaybe, isJust)
#if !(MIN_VERSION_base(4,8,0))
import Data.Monoid (mconcat)
#endif
import Data.String (fromString)
import qualified Data.Text.Lazy as T
import qualified Data.Text as TS
import Network.HTTP.Types
import Network.Wai (Request(..))
import qualified Network.Wai.Parse as Parse hiding (parseRequestBody)
import qualified Text.Regex as Regex
import Web.Scotty.Action
import Web.Scotty.Internal.Types
import Web.Scotty.Util
get :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()
get = addroute GET
post :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()
post = addroute POST
put :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()
put = addroute PUT
delete :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()
delete = addroute DELETE
patch :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()
patch = addroute PATCH
options :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()
options = addroute OPTIONS
matchAny :: (ScottyError e, MonadIO m) => RoutePattern -> ActionT e m () -> ScottyT e m ()
matchAny pattern action = mapM_ (\v -> addroute v pattern action) [minBound..maxBound]
notFound :: (ScottyError e, MonadIO m) => ActionT e m () -> ScottyT e m ()
notFound action = matchAny (Function (\req -> Just [("path", path req)])) (status status404 >> 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 :: (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 h env action
maybe tryNext return res
Nothing -> tryNext
else tryNext
matchRoute :: RoutePattern -> Request -> Maybe [Param]
matchRoute (Literal pat) req | pat == path req = Just []
| otherwise = Nothing
matchRoute (Function fun) req = fun req
matchRoute (Capture pat) req = go (T.split (=='/') pat) (T.split (=='/') $ path req) []
where go [] [] prs = Just prs
go [] r prs | T.null (mconcat r) = Just prs
| otherwise = Nothing
go p [] prs | T.null (mconcat p) = Just prs
| otherwise = Nothing
go (p:ps) (r:rs) prs | p == r = go ps rs prs
| T.null p = Nothing
| T.head p == ':' = go ps rs $ (T.tail p, r) : prs
| otherwise = Nothing
path :: Request -> T.Text
path = T.fromStrict . TS.cons '/' . TS.intercalate "/" . pathInfo
parseRequestBody :: MonadIO m
=> [B.ByteString]
-> Parse.BackEnd y
-> Request
-> m ([Parse.Param], [Parse.File y])
parseRequestBody bl s r =
case Parse.getRequestBodyType r of
Nothing -> return ([], [])
Just rbt -> do
mvar <- liftIO $ newMVar bl
let provider = modifyMVar mvar $ \bsold -> case bsold of
[] -> return ([], B.empty)
(b:bs) -> return (bs, b)
liftIO $ Parse.sinkRequestBody s rbt provider
mkEnv :: forall m. MonadIO m => Request -> [Param] -> m ActionEnv
mkEnv req captures = do
bodyState <- liftIO $ newMVar BodyUntouched
let rbody = requestBody req
takeAll :: ([B.ByteString] -> IO [B.ByteString]) -> IO [B.ByteString]
takeAll prefix = rbody >>= \b -> if B.null b then prefix [] else takeAll (prefix . (b:))
safeBodyReader :: IO B.ByteString
safeBodyReader = do
state <- takeMVar bodyState
let direct = putMVar bodyState BodyCorrupted >> rbody
case state of
s@(BodyCached _ []) ->
do putMVar bodyState s
return B.empty
BodyCached b (chunk:rest) ->
do putMVar bodyState $ BodyCached b rest
return chunk
BodyUntouched -> direct
BodyCorrupted -> direct
bs :: IO BL.ByteString
bs = do
state <- takeMVar bodyState
case state of
s@(BodyCached b _) ->
do putMVar bodyState s
return b
BodyCorrupted -> throw BodyPartiallyStreamed
BodyUntouched ->
do chunks <- takeAll return
let b = BL.fromChunks chunks
putMVar bodyState $ BodyCached b chunks
return b
shouldParseBody = isJust $ Parse.getRequestBodyType req
(formparams, fs) <- if shouldParseBody
then liftIO $ do putStrLn "consuming body"
wholeBody <- BL.toChunks `fmap` bs
parseRequestBody wholeBody Parse.lbsBackEnd req
else return ([], [])
let
convert (k, v) = (strictByteStringToLazyText k, strictByteStringToLazyText v)
parameters = captures ++ map convert formparams ++ queryparams
queryparams = parseEncodedParams $ rawQueryString req
return $ Env req parameters bs safeBodyReader [ (strictByteStringToLazyText k, fi) | (k,fi) <- fs ]
parseEncodedParams :: B.ByteString -> [Param]
parseEncodedParams bs = [ (T.fromStrict k, T.fromStrict $ fromMaybe "" v) | (k,v) <- parseQueryText bs ]
regex :: String -> RoutePattern
regex pattern = Function $ \ req -> fmap (map (T.pack . show *** T.pack) . zip [0 :: Int ..] . strip)
(Regex.matchRegexAll rgx $ T.unpack $ path req)
where rgx = Regex.mkRegex pattern
strip (_, match, _, subs) = match : subs
capture :: String -> RoutePattern
capture = fromString
function :: (Request -> Maybe [Param]) -> RoutePattern
function = Function
literal :: String -> RoutePattern
literal = Literal . T.pack