Wheb 0.0.1.1 → 0.1.0.0
raw patch · 24 files changed
+1545/−1313 lines, 24 filesdep +HUnitdep +QuickCheckdep +Whebdep −data-default
Dependencies added: HUnit, QuickCheck, Wheb, test-framework, test-framework-hunit, test-framework-quickcheck2, unix
Dependencies removed: data-default
Files
- Web/Wheb.hs +0/−104
- Web/Wheb/Cookie.hs +0/−53
- Web/Wheb/InitM.hs +0/−86
- Web/Wheb/Internal.hs +0/−110
- Web/Wheb/Plugins/Auth.hs +0/−155
- Web/Wheb/Plugins/Debug/MemoryBackend.hs +0/−70
- Web/Wheb/Plugins/Session.hs +0/−95
- Web/Wheb/Routes.hs +0/−156
- Web/Wheb/Types.hs +0/−161
- Web/Wheb/Utils.hs +0/−49
- Web/Wheb/WhebT.hs +0/−246
- Wheb.cabal +99/−28
- src/Web/Wheb.hs +115/−0
- src/Web/Wheb/Cookie.hs +53/−0
- src/Web/Wheb/InitM.hs +140/−0
- src/Web/Wheb/Internal.hs +109/−0
- src/Web/Wheb/Plugins/Auth.hs +155/−0
- src/Web/Wheb/Plugins/Debug/MemoryBackend.hs +71/−0
- src/Web/Wheb/Plugins/Session.hs +95/−0
- src/Web/Wheb/Routes.hs +168/−0
- src/Web/Wheb/Types.hs +174/−0
- src/Web/Wheb/Utils.hs +54/−0
- src/Web/Wheb/WhebT.hs +302/−0
- tests/Main.hs +10/−0
− Web/Wheb.hs
@@ -1,104 +0,0 @@-{-|--This module reexports Wheb modules. It should be the only thing you need to-import to get started.--@-import Web.Wheb-import Data.Text.Lazy (pack)--main :: IO ()-main = do- opts <- generateOptions $ addGET (pack \".\") rootPat $ (text (pack \"Hi!\"))- runWhebServer (opts :: MinOpts)-@---}-module Web.Wheb - (- -- * Handlers- -- ** ReaderT and StateT Functionality- -- *** ReaderT- getApp- , getWithApp- -- *** StateT- , getReqState- , putReqState- , modifyReqState- , modifyReqState'- - -- ** Dealing with responses- -- *** Creating a 'HandlerResponse'- , html- , text- , file- -- *** Setting a header- , setHeader- , setRawHeader- - -- * Settings- , getSetting- , getSetting'- , getSettings- - -- * Routes- , getRouteParams- , getRouteParam- , getRoute- , getRoute'- - -- * Request reading- , getRequest- , getRequestHeader- , getWithRequest- , getQueryParams- , getPOSTParam- , getPOSTParams- , getRawPOST- - -- ** Running Wheb- , runWhebServer- , runWhebServerT- , debugHandler- , debugHandlerT- - -- * Initialize- -- ** Routes- -- *** Named routes convenience functions- , addGET- , addPOST- -- *** Add raw routes- , addRoute- , addRoutes- -- ** Middlewares- , addWAIMiddleware- , addWhebMiddleware- -- ** Settings- , addSetting- , addSetting'- , addSettings- - -- * Running- , generateOptions- - -- * Routes- -- ** URL Patterns- , compilePat- , rootPat- - -- ** URL building- , (</>)- , grabInt- , grabText- , pT- , pS- -- * Types- , module Web.Wheb.Types- , Default (..)- ) where--import Web.Wheb.WhebT-import Web.Wheb.InitM-import Web.Wheb.Types-import Web.Wheb.Routes-import Data.Default
− Web/Wheb/Cookie.hs
@@ -1,53 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Web.Wheb.Cookie- ( setCookie- , setCookie'- , getCookie- , getCookies- , removeCookie- ) where- -import qualified Data.ByteString as BS-import qualified Blaze.ByteString.Builder as B-import Data.Maybe (fromMaybe)-import Data.Text.Lazy (Text)-import qualified Data.Text.Lazy as T-import qualified Data.Text.Lazy.Encoding as T-import Data.Time.Calendar-import Data.Time.Clock-import Web.Cookie--import Web.Wheb.Types-import Web.Wheb.Internal-import Web.Wheb.WhebT-import Web.Wheb.Utils--getDefaultCookie :: Monad m => WhebT g s m SetCookie-getDefaultCookie = return def -- Populate with settings...--setCookie :: Monad m => Text -> Text -> WhebT g s m ()-setCookie k v = getDefaultCookie >>= (setCookie' k v)--setCookie' :: Monad m => Text -> Text -> SetCookie -> WhebT g s m ()-setCookie' k v sc = setRawHeader ("Set-Cookie", cookieText)- where cookie = sc { setCookieName = lazyTextToSBS k- , setCookieValue = lazyTextToSBS v- }- cookieText = B.toByteString $ renderSetCookie cookie- -getCookies :: Monad m => WhebT g s m CookiesText-getCookies = (getRequestHeader "Cookie") >>= - (return . parseFunc . (fromMaybe T.empty))- where parseFunc = parseCookiesText . lazyTextToSBS--getCookie :: Monad m => Text -> WhebT g s m (Maybe Text)-getCookie k = getCookies >>= - (return . (fmap T.fromStrict) . (lookup (T.toStrict k)))--removeCookie :: Monad m => Text -> WhebT g s m ()-removeCookie k = do- defCookie <- getDefaultCookie- let utcLongAgo = UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)- expiredCookie = defCookie {setCookieExpires = Just utcLongAgo}- setCookie' k T.empty expiredCookie
− Web/Wheb/InitM.hs
@@ -1,86 +0,0 @@-{-# LANGUAGE RecordWildCards #-}--module Web.Wheb.InitM- (- -- * Routes- -- ** Named routes convenience functions- addGET- , addPOST- -- ** Add raw routes- , addRoute- , addRoutes- -- * Middlewares- , addWAIMiddleware- , addWhebMiddleware- -- * Settings- , addSetting- , addSetting'- , addSettings- - -- * Running- , generateOptions- - ) where- -import Control.Monad.IO.Class-import Control.Monad.Writer-import qualified Data.Map as M-import qualified Data.Text.Lazy as T-import Data.Typeable-import Network.Wai-import Network.Wai.Handler.Warp (defaultSettings)-import Network.HTTP.Types.Method--import Web.Wheb.Internal-import Web.Wheb.Routes-import Web.Wheb.Types-import Web.Wheb.Utils--addGET :: T.Text -> UrlPat -> WhebHandlerT g s m -> InitM g s m ()-addGET n p h = addRoute $ rGET (Just n) p h--addPOST :: T.Text -> UrlPat -> WhebHandlerT g s m -> InitM g s m ()-addPOST n p h = addRoute $ rPOST (Just n) p h--addRoute :: Route g s m -> InitM g s m ()-addRoute r = addRoutes [r]--addRoutes :: [Route g s m] -> InitM g s m ()-addRoutes rs = InitM $ tell $ mempty { initRoutes = rs }---- | Catch all routes regardless of method or path-catchAllRoutes :: WhebHandlerT g s m -> InitM g s m ()-catchAllRoutes h = addRoute $ Route Nothing (const True) parser h- where parser = UrlParser (const (Just [])) (const (Right $ T.pack "/*"))---- | Add generic "WAI" middleware-addWAIMiddleware :: Middleware -> InitM g s m ()-addWAIMiddleware m = InitM $ tell $ mempty { initWaiMw = m }---- | Add "Wheb" specific middleware-addWhebMiddleware :: WhebMiddleware g s m -> InitM g s m ()-addWhebMiddleware m = InitM $ tell $ mempty { initWhebMw = [m] }----- | Wrapped 'addSetting'' to help prevent monomorphism errors for simple settings.-addSetting :: T.Text -> T.Text -> InitM g s m ()-addSetting = addSetting'---- | Adds a setting value, replacing it if its key already exists.-addSetting' :: Typeable a => T.Text -> a -> InitM g s m ()-addSetting' k v = addSettings $ M.fromList [(k, MkVal v)]--addSettings :: CSettings -> InitM g s m ()-addSettings settings = InitM $ tell $ mempty { initSettings = settings }---- | Generate 'WhebOptions' from 'InitM' in 'IO'-generateOptions :: MonadIO m => InitM g s m g -> IO (WhebOptions g s m)-generateOptions m = do - (g, InitOptions {..}) <- runWriterT (runInitM m)- return $ WhebOptions { appRoutes = initRoutes- , runTimeSettings = initSettings- , warpSettings = defaultSettings- , startingCtx = g- , waiStack = initWaiMw- , whebMiddlewares = initWhebMw- , defaultErrorHandler = defaultErr }
− Web/Wheb/Internal.hs
@@ -1,110 +0,0 @@-{-# LANGUAGE RecordWildCards #-}--module Web.Wheb.Internal where--import Control.Monad.Error-import Control.Monad.IO.Class-import Control.Monad.Reader-import Control.Monad.State-import Control.Monad.Writer-import Data.Default-import qualified Data.Map as M-import qualified Data.Text.Lazy as T--import Network.HTTP.Types.Method-import Network.Wai-import Network.Wai.Parse--import Web.Wheb.Routes-import Web.Wheb.Types-import Web.Wheb.Utils---- * Converting to WAI application- --- | Convert 'WhebOptions' to 'Application' -optsToApplication :: (Default s) => WhebOptions g s m ->- (m EResponse -> IO EResponse) ->- Application-optsToApplication opts@(WhebOptions {..}) runIO r = do- pData <- liftIO (parseRequestBody lbsBackEnd r)- res <- runIO $ do- let mwData = baseData { postData = pData }- (mRes, st) <- runMiddlewares opts whebMiddlewares mwData- case mRes of- Just resp -> return $ Right resp- Nothing -> case (findUrlMatch stdMthd pathChunks appRoutes) of- Just (h, params) -> do- let hData = mwData { routeParams = params }- runWhebHandler opts h st hData - Nothing -> return $ Left Error404- either handleError return res- where baseData = HandlerData startingCtx r ([], []) [] opts- pathChunks = fmap T.fromStrict $ pathInfo r- stdMthd = either (\_-> GET) id $ parseMethod $ requestMethod r- handleError err = do- errRes <- runIO $ - runWhebHandler opts (defaultErrorHandler err) def baseData- either (return . (const uhOh)) return errRes---- * Running Handlers---- | Run all inner wheb monads to the top level.-runWhebHandler :: (Default s, Monad m) =>- WhebOptions g s m ->- WhebHandlerT g s m ->- InternalState s ->- HandlerData g s m ->- m EResponse-runWhebHandler opts@(WhebOptions {..}) handler st hd = do- (resp, InternalState {..}) <- flip runStateT st $ do- flip runReaderT hd $- runErrorT $- runWhebT handler- return $ fmap (convertResponse respHeaders) resp - where convertResponse hds (HandlerResponse status resp) =- toResponse status (M.toList hds) resp---- | Same as above but returns arbitrary type for debugging.-runDebugHandler :: (Default s, Monad m) =>- WhebOptions g s m ->- WhebT g s m a ->- HandlerData g s m ->- m (Either WhebError a)-runDebugHandler opts@(WhebOptions {..}) handler hd = do- flip evalStateT def $ do- flip runReaderT hd $- runErrorT $- runWhebT handler- where convertResponse hds (HandlerResponse status resp) =- toResponse status (M.toList hds) resp--- * Running Middlewares- --- | Runs middlewares in order, stopping if one returns a response-runMiddlewares :: (Default s, Monad m) =>- WhebOptions g s m ->- [WhebMiddleware g s m] ->- HandlerData g s m ->- m (Maybe Response, InternalState s)-runMiddlewares opts mWs hd = loop mWs def- where loop [] st = return (Nothing, st)- loop (mw:mws) st = do- mwResult <- (runWhebMiddleware opts st hd mw)- case mwResult of- (Just resp, nst) -> return mwResult- (Nothing, nst) -> loop mws nst--runWhebMiddleware :: (Default s, Monad m) =>- WhebOptions g s m ->- InternalState s ->- HandlerData g s m ->- WhebMiddleware g s m ->- m (Maybe Response, InternalState s)-runWhebMiddleware opts@(WhebOptions {..}) st hd mW = do- (eresp, is@InternalState {..}) <- flip runStateT st $ do- flip runReaderT hd $- runErrorT $- runWhebT mW- return $ (convertResponse respHeaders eresp, is)- where convertResponse hds (Right (Just (HandlerResponse status resp))) =- Just (toResponse status (M.toList hds) resp)- convertResponse _ _ = Nothing
− Web/Wheb/Plugins/Auth.hs
@@ -1,155 +0,0 @@-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE RankNTypes #-}--module Web.Wheb.Plugins.Auth - ( - -- * Main functions- login- , logout- , register- , getCurrentUser- , queryCurrentUser- , loginRequired- - -- * Middleware- , authMiddleware - - -- * Types- , AuthUser (..)- , AuthContainer (..)- , AuthApp (..)- , AuthState (..)- , AuthBackend (..)- , AuthError (..)-- , UserKey- , Password- , PwHash- - -- * Utils- , makePwHash- , verifyPw- , getUserSessionKey- ) where--import Control.Applicative ((<*>))-import Control.Monad (void, liftM)-import Control.Monad.Error-import Control.Monad.IO.Class-import Crypto.PasswordStore-import Data.Maybe-import Data.Text.Lazy (Text)-import Data.Text.Lazy as T-import Data.Text.Lazy.Encoding as T-import Data.Text.Encoding as ES--import Web.Wheb-import Web.Wheb.Types-import Web.Wheb.Plugins.Session- --- * Auth functions---- | Register a user-register :: (AuthApp a, MonadIO m) => UserKey -> Password -> WhebT a b m (Either AuthError AuthUser)-register un pw = runWithContainer $ backendRegister un pw---- | Log a user in-login :: (AuthApp a, AuthState b, MonadIO m) => UserKey -> Password -> WhebT a b m (Either AuthError AuthUser)-login un pw = do- loginResult <- (runWithContainer $ backendLogin un pw)- case loginResult of- Right au@(AuthUser userKey) -> do- sessionKey <- getUserSessionKey- deleteSessionValue sessionKey- setSessionValue sessionKey userKey- authSetUser (Just au)- _ -> return ()- return loginResult---- | Log a user out-logout :: (AuthApp a, AuthState b, MonadIO m) => WhebT a b m ()-logout = (runWithContainer backendLogout) >> (authSetUser Nothing)---- | Get the current user from the request state (Needs to be populated first--- with 'authMiddleware')-getCurrentUser :: (AuthState b, MonadIO m) => WhebT a b m (Maybe AuthUser)-getCurrentUser = liftM getAuthUser getReqState---- | Explicitly query a user with the backend. Since this is an IO hit, it is--- better to use the middleware and 'getCurrentUser'-queryCurrentUser :: (AuthApp a, MonadIO m) => WhebT a b m (Maybe AuthUser)-queryCurrentUser = getUserSessionKey >>= - getSessionValue' (T.pack "") >>=- (\uid -> runWithContainer $ backendGetUser uid)---- | Checks if a user is logged in with 'getCurrentUser' and prevents a handler--- from running if they aren't-loginRequired :: (AuthState b, MonadIO m) =>- WhebHandlerT a b m ->- WhebHandlerT a b m-loginRequired action = getCurrentUser >>=- (maybe (throwError Error403) (const action))---- * Middleware---- | Auto-populates the request state with the current user.-authMiddleware :: (AuthApp a, AuthState b, MonadIO m) => WhebMiddleware a b m-authMiddleware = do- cur <- queryCurrentUser- authSetUser cur- return Nothing---type UserKey = Text-type Password = Text-type PwHash = Text--data AuthError = DuplicateUsername | UserDoesNotExist | InvalidPassword- deriving (Show)--data AuthUser = AuthUser { uniqueUserKey :: UserKey } deriving (Show)--type PossibleUser = Maybe AuthUser--data AuthContainer = forall r. AuthBackend r => AuthContainer r---- | Interface for creating Auth backends-class SessionApp a => AuthApp a where- getAuthContainer :: a -> AuthContainer---- | Minimal implementation for a -class AuthState a where- getAuthUser :: a -> PossibleUser - modifyAuthUser :: (PossibleUser -> PossibleUser) -> a -> a---- | Interface for creating Auth backends-class AuthBackend c where- backendLogin :: (AuthApp a, MonadIO m) => SessionApp a => UserKey -> Password -> c -> WhebT a b m (Either AuthError AuthUser)- backendRegister :: (AuthApp a, MonadIO m) => UserKey -> Password -> c -> WhebT a b m (Either AuthError AuthUser)- backendGetUser :: (AuthApp a, MonadIO m) => UserKey -> c -> WhebT a b m (Maybe AuthUser)- backendLogout :: (AuthApp a, MonadIO m) => c -> WhebT a b m ()- backendLogout _ = getUserSessionKey >>= deleteSessionValue- --- * Internal--runWithContainer :: (AuthApp a, MonadIO m) =>- (forall r. AuthBackend r => r -> WhebT a s m b) -> - WhebT a s m b-runWithContainer f = do- AuthContainer authStore <- getWithApp getAuthContainer- f authStore--authSetUser :: (AuthApp a, AuthState b, MonadIO m) => PossibleUser -> WhebT a b m ()-authSetUser cur = modifyReqState' (modifyAuthUser (const cur))--getUserSessionKey :: (AuthApp a, MonadIO m) => WhebT a b m Text-getUserSessionKey = return $ T.pack "user-id" -- later read from settings.--makePwHash :: MonadIO m => Password -> WhebT a b m PwHash-makePwHash pw = liftM (T.fromStrict . ES.decodeUtf8) $ - liftIO $ makePassword (ES.encodeUtf8 $ T.toStrict pw) 14--verifyPw :: Text -> Text -> Bool-verifyPw pw hash = verifyPassword (ES.encodeUtf8 $ T.toStrict pw) - (ES.encodeUtf8 $ T.toStrict hash)
− Web/Wheb/Plugins/Debug/MemoryBackend.hs
@@ -1,70 +0,0 @@-module Web.Wheb.Plugins.Debug.MemoryBackend where--import Control.Monad (liftM)-import Control.Monad.IO.Class-import Control.Concurrent.STM-import Data.Text.Lazy (Text)-import Data.Map as M-import Data.Maybe (fromMaybe, fromJust)--import Web.Wheb-import Web.Wheb.Types-import Web.Wheb.Plugins.Auth-import Web.Wheb.Plugins.Session--data SessionData = SessionData - { sessionMemory :: TVar (M.Map Text (M.Map Text Text)) }-data UserData = UserData- { userStorage :: TVar (M.Map UserKey PwHash) }--- | In memory session backend. Session values --- will not persist after server restart.-instance SessionBackend SessionData where- backendSessionPut sessId key content (SessionData tv) =- let insertFunc = (\sess -> - Just $ M.insert key content (fromMaybe M.empty sess)- )- tVarFunc = M.alter insertFunc sessId- in liftIO $ atomically $ modifyTVar tv tVarFunc- backendSessionGet sessId key (SessionData tv) = do- curSessions <- liftIO $ readTVarIO tv- return $ (M.lookup sessId curSessions) >>= (M.lookup key)- backendSessionDelete sessId key (SessionData tv) =- liftIO $ atomically $ modifyTVar tv (M.update (Just . (M.delete key)) sessId)- backendSessionClear sessId (SessionData tv) =- liftIO $ atomically $ modifyTVar tv (M.delete sessId)---- | In memory auth backend. User values --- will not persist after server restart.-instance AuthBackend UserData where- backendGetUser name (UserData tv) = do- possUser <- liftM (M.lookup name) $ liftIO $ readTVarIO tv- case possUser of- Nothing -> return Nothing- Just _ -> return $ Just (AuthUser name)- backendLogin name pw (UserData tv) = do- users <- liftIO $ readTVarIO $ tv- let possUser = M.lookup name users- passCheck = fmap (verifyPw pw) possUser- case passCheck of- Just True -> return (Right $ AuthUser $ name)- Just False -> return (Left InvalidPassword)- Nothing -> return (Left UserDoesNotExist)- backendRegister name pw (UserData tv) = do- users <- liftIO $ readTVarIO $ tv- if M.member name users- then return (Left DuplicateUsername)- else do- pwHash <- makePwHash pw- liftIO $ atomically $ writeTVar tv (M.insert name pwHash users)- return (Right $ AuthUser name)- backendLogout _ = getUserSessionKey >>= deleteSessionValue- -initSessionMemory :: InitM g s m SessionContainer-initSessionMemory = do- tv <- liftIO $ newTVarIO $ M.empty- return $! SessionContainer $ SessionData tv--initAuthMemory :: InitM g s m AuthContainer-initAuthMemory = do- tv <- liftIO $ newTVarIO $ M.empty- return $! AuthContainer $ UserData tv
− Web/Wheb/Plugins/Session.hs
@@ -1,95 +0,0 @@-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE RankNTypes #-}--module Web.Wheb.Plugins.Session - ( SessionContainer (..)- , SessionApp (..)- , SessionBackend (..)- - , setSessionValue- , getSessionValue- , getSessionValue'- , deleteSessionValue- , generateSessionKey- , getCurrentSessionKey- , clearSessionKey- ) where- -import Control.Monad.IO.Class-import Control.Monad (liftM)-import Data.Maybe-import Data.Text.Lazy (Text, pack)-import Data.Text.Lazy.Encoding as T-import Data.UUID-import Data.UUID.V4--import Web.Wheb-import Web.Wheb.Cookie-import Web.Wheb.Types---- | Initial pass on abstract plugin for Sessions.--- Possibly add support for Typable to ease typecasting.---session_cookie_key = pack "-session-"--data SessionContainer = forall r. SessionBackend r => SessionContainer r--class SessionApp a where- getSessionContainer :: a -> SessionContainer--class SessionBackend c where- backendSessionPut :: (SessionApp a, MonadIO m) => Text -> Text -> Text -> c -> WhebT a b m ()- backendSessionGet :: (SessionApp a, MonadIO m) => Text -> Text -> c -> WhebT a b m (Maybe Text)- backendSessionDelete :: (SessionApp a, MonadIO m) => Text -> Text -> c -> WhebT a b m ()- backendSessionClear :: (SessionApp a, MonadIO m) => Text -> c -> WhebT a b m ()--runWithContainer :: (SessionApp a, MonadIO m) => (forall r. SessionBackend r => r -> WhebT a s m b) -> WhebT a s m b-runWithContainer f = do- SessionContainer sessStore <- getWithApp getSessionContainer- f sessStore--deleteSessionValue :: (SessionApp a, MonadIO m) => Text -> WhebT a b m ()-deleteSessionValue key= do- sessId <- getCurrentSessionKey - runWithContainer $ backendSessionDelete sessId key--setSessionValue :: (SessionApp a, MonadIO m) => Text -> Text -> WhebT a b m ()-setSessionValue key content = do- sessId <- getCurrentSessionKey - runWithContainer $ backendSessionPut sessId key content--getSessionValue :: (SessionApp a, MonadIO m) => Text -> WhebT a b m (Maybe Text)-getSessionValue key = do- sessId <- getCurrentSessionKey- runWithContainer $ backendSessionGet sessId key--getSessionValue' :: (SessionApp a, MonadIO m) => Text -> Text -> WhebT a b m Text-getSessionValue' def key = liftM (fromMaybe def) (getSessionValue key)- -getSessionCookie :: (SessionApp a, MonadIO m) => WhebT a b m (Maybe Text)-getSessionCookie = getCookie session_cookie_key- -generateSessionKey :: (SessionApp a, MonadIO m) => WhebT a b m Text-generateSessionKey = do- newKey <- liftM (T.decodeUtf8 . toLazyASCIIBytes) (liftIO nextRandom)- setCookie session_cookie_key newKey- return newKey--getCurrentSessionKey :: (SessionApp a, MonadIO m) => WhebT a b m Text-getCurrentSessionKey = do- curKey <- getSessionCookie- case curKey of- Just key -> return key- Nothing -> generateSessionKey--clearSessionKey :: (SessionApp a, MonadIO m) => WhebT a b m Text-clearSessionKey = do- curKey <- getSessionCookie- newKey <- generateSessionKey- case curKey of- Nothing -> return newKey- Just oldKey -> do- runWithContainer $ backendSessionClear oldKey- return newKey
− Web/Wheb/Routes.hs
@@ -1,156 +0,0 @@-module Web.Wheb.Routes- (- -- * Convenience constructors- rGET- , rPOST- -- * URL Patterns- , compilePat- , rootPat- - -- ** URL building- , (</>)- , grabInt- , grabText- , pT- , pS- - -- * Working with URLs- , getParam- , matchUrl- , generateUrl- , findUrlMatch- ) where- -import qualified Data.Text.Lazy as T-import Data.Text.Lazy.Read-import Data.Typeable-import Network.HTTP.Types.Method--import Data.Maybe (fromJust)-import Data.Monoid ((<>))--import Web.Wheb.Types---- * Convenience constructors--rGET :: (Maybe T.Text) -> UrlPat -> WhebHandlerT g s m -> Route g s m-rGET n p = Route n (==GET) (compilePat p)--rPOST :: (Maybe T.Text) -> UrlPat -> WhebHandlerT g s m -> Route g s m-rPOST n p = Route n (==POST) (compilePat p)---- * URL Patterns---- | Convert a 'UrlPat' to a 'UrlParser'-compilePat :: UrlPat -> UrlParser-compilePat (Composed a) = UrlParser (matchPat a) (buildPat a)-compilePat a = UrlParser (matchPat [a]) (buildPat [a])---- | Represents root path @/@-rootPat :: UrlPat-rootPat = Composed [] ------ | Allows for easier building of URL patterns--- This should be the primary URL constructor.--- --- @--- (\"blog\" '</>' ('grabInt' \"pk\") '</>' \"edit\" '</>' ('grabText' \"verb\"))--- @-(</>) :: UrlPat -> UrlPat -> UrlPat-(Composed a) </> (Composed b) = Composed (a ++ b)-a </> (Composed b) = Composed (a:b)-(Composed a) </> b = Composed (a ++ [b])-a </> b = Composed [a, b]---- | Parses URL parameter and matches on 'Int'-grabInt :: T.Text -> UrlPat-grabInt key = FuncChunk key f IntChunk- where rInt = decimal :: Reader Int- f = ((either (const Nothing) (Just . MkChunk . fst)) . rInt)---- | Parses URL parameter and matches on 'Text'-grabText :: T.Text -> UrlPat-grabText key = FuncChunk key (Just . MkChunk) TextChunk---- | Constructors to use w/o OverloadedStrings-pT :: T.Text -> UrlPat-pT = Chunk--pS :: String -> UrlPat-pS = pT . T.pack------ | Lookup and cast a URL parameter to its expected type if it exists.-getParam :: Typeable a => T.Text -> RouteParamList -> Maybe a-getParam k l = (lookup k l) >>= unwrap- where unwrap :: Typeable a => ParsedChunk -> Maybe a- unwrap (MkChunk a) = cast a---- | Convert URL chunks (split on /) -matchUrl :: [T.Text] -> UrlParser -> Maybe RouteParamList-matchUrl url (UrlParser f _) = f url---- | Runs a 'UrlParser' with 'RouteParamList' to a URL path-generateUrl :: UrlParser -> RouteParamList -> Either UrlBuildError T.Text-generateUrl (UrlParser _ f) = f---- | Sort through a list of routes to find a Handler and 'RouteParamList'-findUrlMatch :: StdMethod ->- [T.Text] ->- [Route g s m] ->- Maybe (WhebHandlerT g s m, RouteParamList)-findUrlMatch _ _ [] = Nothing-findUrlMatch rmtd path ((Route _ methodMatch (UrlParser f _) h):rs) - | not (methodMatch rmtd) = findUrlMatch rmtd path rs- | otherwise = case f path of- Just params -> Just (h, params)- Nothing -> findUrlMatch rmtd path rs---- | Implementation for a 'UrlParser' using pseudo-typed URL composition.--- Pattern will match path when the pattern is as long as the path, matching--- on a trailing slash. If the path is longer or shorter than the pattern, it--- should not match.--- Example:--- Given a url = "blog" </> (grabInt "pk") </> "edit"--- This will match on "/blog/1/edit" and /blog/9999/edit/"--- But not "/blog/1/", "/blog/1", "blog/foo/edit/", "/blog/9/edit/d",--- nor "/blog/9/edit//"-matchPat :: [UrlPat] -> [T.Text] -> Maybe RouteParamList-matchPat chunks t = parse t chunks []- where parse [] [] params = Just params- parse [] c params = Nothing- parse (u:[]) [] params | T.null u = Just params -- Match only 1 trailing slash- | otherwise = Nothing- parse (u:us) [] _ = Nothing- parse (u:us) ((Chunk c):cs) params | u == c = parse us cs params- | otherwise = Nothing- parse (u:us) ((FuncChunk k f _):cs) params = do- val <- f u- parse us cs ((k, val):params)- parse us ((Composed xs):cs) params = parse us (xs ++ cs) params--buildPat :: [UrlPat] -> RouteParamList -> Either UrlBuildError T.Text-buildPat pats params = fmap addSlashes $ build [] pats- where build acc [] = Right acc- build acc ((Chunk c):cs) = build (acc <> [c]) cs- build acc ((Composed xs):cs) = build acc (xs <> cs)- build acc ((FuncChunk k _ t):cs) = - case (showParam t k params) of- (Right v) -> build (acc <> [v]) cs- (Left err) -> Left err- slash = (T.pack "/")- addSlashes list = slash <> (T.intercalate slash list) <> slash--showParam :: ChunkType -> T.Text -> RouteParamList -> Either UrlBuildError T.Text-showParam chunkType k l = - case (lookup k l) of- Just (MkChunk v) -> case chunkType of- IntChunk -> toEither $ fmap (T.pack . show) (cast v :: Maybe Int)- TextChunk -> toEither (cast v :: Maybe T.Text)- Nothing -> Left NoParam- where toEither v = case v of- Just b -> Right b- Nothing -> Left $ ParamTypeMismatch k
− Web/Wheb/Types.hs
@@ -1,161 +0,0 @@-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE DeriveFunctor #-}-{-# LANGUAGE MultiParamTypeClasses #-}--module Web.Wheb.Types where--import Blaze.ByteString.Builder (Builder, fromLazyByteString)-import Control.Applicative-import Control.Monad.Error-import Control.Monad.Trans-import Control.Monad.IO.Class-import Control.Monad.State-import Control.Monad.Reader-import Control.Monad.Writer-import Data.Monoid ((<>))--import qualified Data.ByteString.Lazy as LBS-import Data.Default-import Data.Map as M-import Data.String (IsString(..))-import qualified Data.Text.Lazy as T-import Data.Typeable--import Network.Wai (Request, Response, Middleware, responseBuilder)-import Network.Wai.Handler.Warp as Warp-import Network.Wai.Parse-import Network.HTTP.Types.Method-import Network.HTTP.Types.Status-import Network.HTTP.Types.Header--import Data.ByteString (ByteString)----- | WhebT g s m------ * g -> The global confirgured context (Read-only data shared between threads)--- --- * s -> Request state initailized at the start of each request using Default------ * m -> Monad we are transforming-newtype WhebT g s m a = WhebT - { runWhebT :: ErrorT WhebError - (ReaderT (HandlerData g s m) (StateT (InternalState s) m)) a - } deriving ( Functor, Applicative, Monad, MonadIO )--instance MonadTrans (WhebT g s) where- lift = WhebT . lift . lift . lift--instance (Monad m) => MonadError WhebError (WhebT g s m) where- throwError = WhebT . throwError- catchError (WhebT m) f = WhebT (catchError m (runWhebT . f))---- | Writer Monad to build options.-newtype InitM g s m a = InitM { runInitM :: WriterT (InitOptions g s m) IO a}- deriving (Functor, Applicative, Monad, MonadIO)---- | Converts a type to a WAI 'Response'-class WhebContent a where- toResponse :: Status -> ResponseHeaders -> a -> Response---- | A Wheb response that represents a file.-data WhebFile = WhebFile T.Text--data HandlerResponse = forall a . WhebContent a => HandlerResponse Status a---- | Our 'ReaderT' portion of 'WhebT' uses this.-data HandlerData g s m = - HandlerData { globalCtx :: g- , request :: Request- , postData :: ([Param], [File LBS.ByteString])- , routeParams :: RouteParamList- , globalSettings :: WhebOptions g s m }---- | Our 'StateT' portion of 'WhebT' uses this.-data InternalState s =- InternalState { reqState :: s- , respHeaders :: M.Map HeaderName ByteString } - -data SettingsValue = forall a. (Typeable a) => MkVal a--data WhebError = Error500 String - | Error404- | Error403- | URLError T.Text UrlBuildError- deriving (Show)--instance Error WhebError where - strMsg = Error500--instance Default s => Default (InternalState s) where- def = InternalState def def---- | Monoid to use in InitM's WriterT-data InitOptions g s m =- InitOptions { initRoutes :: [ Route g s m ]- , initSettings :: CSettings- , initWaiMw :: Middleware- , initWhebMw :: [ WhebMiddleware g s m ] }--instance Monoid (InitOptions g s m) where- mappend (InitOptions a1 b1 c1 d1) (InitOptions a2 b2 c2 d2) = - InitOptions (a1 <> a2) (b1 <> b2) (c2 . c1) (d1 <> d2)- mempty = InitOptions mempty mempty id mempty---- | The main option datatype for Wheb-data WhebOptions g s m = MonadIO m => - WhebOptions { appRoutes :: [ Route g s m ]- , runTimeSettings :: CSettings- , warpSettings :: Warp.Settings- , startingCtx :: g- , waiStack :: Middleware- , whebMiddlewares :: [ WhebMiddleware g s m ]- , defaultErrorHandler :: WhebError -> WhebHandlerT g s m }--type EResponse = Either WhebError Response--type CSettings = M.Map T.Text SettingsValue- -type WhebHandler g s = WhebT g s IO HandlerResponse-type WhebHandlerT g s m = WhebT g s m HandlerResponse-type WhebMiddleware g s m = WhebT g s m (Maybe HandlerResponse)---- | A minimal type for WhebT-type MinWheb a = WhebT () () IO a---- | A minimal type for WhebOptions-type MinOpts = WhebOptions () () IO---- * Routes--type RouteParamList = [(T.Text, ParsedChunk)]-type MethodMatch = StdMethod -> Bool--data ParsedChunk = forall a. (Typeable a, Show a) => MkChunk a--data UrlBuildError = NoParam | ParamTypeMismatch T.Text | UrlNameNotFound- deriving (Show) ---- | A Parser should be able to extract params and regenerate URL from params.-data UrlParser = UrlParser - { parseFunc :: ([T.Text] -> Maybe RouteParamList)- , genFunc :: (RouteParamList -> Either UrlBuildError T.Text) }--data Route g s m = Route - { routeName :: (Maybe T.Text)- , routeMethod :: MethodMatch- , routeParser :: UrlParser- , routeHandler :: (WhebHandlerT g s m) }--data ChunkType = IntChunk | TextChunk--data UrlPat = Chunk T.Text- | Composed [UrlPat]- | FuncChunk - { chunkParamName :: T.Text- , chunkFunc :: (T.Text -> Maybe ParsedChunk)- , chunkType :: ChunkType }--instance IsString UrlPat where- fromString = Chunk . T.pack
− Web/Wheb/Utils.hs
@@ -1,49 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Web.Wheb.Utils where--import Blaze.ByteString.Builder (Builder- ,fromLazyByteString- ,toLazyByteString)-import Control.Monad-import Data.Monoid-import Data.Conduit as C-import Data.Conduit.List (fold)-import qualified Data.Text.Lazy as T-import qualified Data.Text.Lazy.Encoding as T-import qualified Data.Text.Encoding as TS-import Network.HTTP.Types.Status-import Network.Wai--import Web.Wheb.Types--lazyTextToSBS = TS.encodeUtf8 . T.toStrict-sbsToLazyText = T.fromStrict . TS.decodeUtf8---- | See a 'HandlerResponse's as 'Text'-showResponseBody :: HandlerResponse -> IO T.Text-showResponseBody (HandlerResponse s r) = - liftM (T.decodeUtf8 . toLazyByteString) builderBody- where chunkFlatAppend m (C.Chunk more) = m `mappend` more- chunkFlatAppend m _ = m- builderBody = body' (C.$$ fold chunkFlatAppend mempty)- (_, _, body') = responseToSource $ toResponse s [] r------------------------- Instances -------------------------instance WhebContent Builder where- toResponse = responseBuilder--instance WhebContent T.Text where- toResponse s hds = responseBuilder s hds . fromLazyByteString . T.encodeUtf8--instance WhebContent WhebFile where- toResponse s hds (WhebFile fp) = responseFile s hds (show fp) Nothing------------------------- Some defaults ------------------------defaultErr :: Monad m => WhebError -> WhebHandlerT g s m-defaultErr err = return $ HandlerResponse status500 $ - ("<h1>Error: " <> (T.pack $ show err) <> ".</h1>")--uhOh :: Response-uhOh = responseLBS status500 [("Content-Type", "text/html")]- "Something went wrong on the server."
− Web/Wheb/WhebT.hs
@@ -1,246 +0,0 @@-{-# LANGUAGE RecordWildCards #-}--module Web.Wheb.WhebT- (- -- * ReaderT and StateT Functionality- -- ** ReaderT- getApp- , getWithApp- -- ** StateT- , getReqState- , putReqState- , modifyReqState- , modifyReqState'- - -- * Responses- , setHeader- , setRawHeader- , html- , text- , file- - -- * Settings- , getSetting- , getSetting'- , getSettings- - -- * Routes- , getRouteParams- , getRouteParam- , getRoute- , getRoute'- - -- * Request reading- , getRequest- , getRequestHeader- , getWithRequest- , getQueryParams- , getPOSTParam- , getPOSTParams- , getRawPOST- - -- * Running Wheb- , runWhebServer- , runWhebServerT- , debugHandler- , debugHandlerT- )where--import Control.Monad.Error-import Control.Monad.IO.Class-import Control.Monad.Reader-import Control.Monad.State-import qualified Data.ByteString.Lazy as LBS-import Data.CaseInsensitive (mk)-import Data.Default-import qualified Data.Map as M-import Data.Maybe (fromMaybe)-import qualified Data.Text.Lazy as T-import Data.Typeable (Typeable, cast)-import Data.List (find)--import Network.HTTP.Types.Header-import Network.HTTP.Types.Status-import Network.HTTP.Types.URI-import Network.Wai-import Network.Wai.Handler.Warp as W-import Network.Wai.Parse--import Web.Wheb.Internal-import Web.Wheb.Routes-import Web.Wheb.Types-import Web.Wheb.Utils---- * ReaderT and StateT Functionality---- ** ReaderT---- | Get the 'g' in @WhebT g s m g@. This is a read-only state so only--- thread-safe resources such as DB connections should go in here.-getApp :: Monad m => WhebT g s m g-getApp = WhebT $ liftM globalCtx ask--getWithApp :: Monad m => (g -> a) -> WhebT g s m a-getWithApp = flip liftM getApp---- ** StateT---- | Get the 's' in @WhebT g s m g@. This is a read and writable state--- so you can get and put information in your state. Each request gets its own--- fresh state generated from "Default"-getReqState :: Monad m => WhebT g s m s-getReqState = WhebT $ liftM reqState get--putReqState :: Monad m => s -> WhebT g s m ()-putReqState s = WhebT $ modify (\is -> is {reqState = s})--modifyReqState :: Monad m => (s -> s) -> WhebT g s m s-modifyReqState f = do- s <- liftM f getReqState- putReqState s- return s--modifyReqState' :: Monad m => (s -> s) -> WhebT g s m ()-modifyReqState' f = modifyReqState f >> (return ())---- * Settings---- | Help prevent monomorphism errors for simple settings.-getSetting :: Monad m => T.Text -> WhebT g s m (Maybe T.Text)-getSetting = getSetting'---- | Open up underlying support for polymorphic global settings-getSetting' :: (Monad m, Typeable a) => T.Text -> WhebT g s m (Maybe a)-getSetting' k = liftM (\cs -> (M.lookup k cs) >>= unwrap) getSettings- where unwrap :: Typeable a => SettingsValue -> Maybe a- unwrap (MkVal a) = cast a---- | Get all settings.-getSettings :: Monad m => WhebT g s m CSettings-getSettings = WhebT $ liftM (runTimeSettings . globalSettings) ask---- * Routes---- | Get all route params.-getRouteParams :: Monad m => WhebT g s m RouteParamList-getRouteParams = WhebT $ liftM routeParams ask---- | Cast a route param into its type.-getRouteParam :: (Typeable a, Monad m) => T.Text -> WhebT g s m (Maybe a)-getRouteParam t = liftM (getParam t) getRouteParams---- | Convert 'Either' from 'getRoute'' into an error in the Monad-getRoute :: Monad m => T.Text -> RouteParamList -> WhebT g s m T.Text-getRoute t l = do- res <- getRoute' t l- case res of- Right t -> return t- Left err -> throwError $ URLError t err---- | Generate a route from a name and param list.-getRoute' :: Monad m => T.Text -> - RouteParamList -> - WhebT g s m (Either UrlBuildError T.Text)-getRoute' n l = WhebT $ liftM f ask- where findRoute (Route {..}) = fromMaybe False (fmap (==n) routeName)- buildRoute (Just (Route {..})) = generateUrl routeParser l- buildRoute (Nothing) = Left UrlNameNotFound- f = (buildRoute . (find findRoute) . appRoutes . globalSettings)---- * Request reading---- | Access the request-getRequest :: Monad m => WhebT g s m Request-getRequest = WhebT $ liftM request ask--getWithRequest :: Monad m => (Request -> a) -> WhebT g s m a-getWithRequest = flip liftM getRequest---- | Get the raw parsed POST data including files.-getRawPOST :: MonadIO m => WhebT g s m ([Param], [File LBS.ByteString])-getRawPOST = WhebT $ liftM postData ask---- | Get POST params as 'Text'-getPOSTParams :: MonadIO m => WhebT g s m [(T.Text, T.Text)]-getPOSTParams = liftM (fmap f . fst) getRawPOST- where f (a, b) = (sbsToLazyText a, sbsToLazyText b)---- | Maybe get one param if it exists.-getPOSTParam :: MonadIO m => T.Text -> WhebT g s m (Maybe T.Text)-getPOSTParam k = liftM (lookup k) getPOSTParams ---- | Get params from URL (e.g. from '/foo/?q=4')-getQueryParams :: Monad m => WhebT g s m Query-getQueryParams = getWithRequest queryString---- | Get a request header-getRequestHeader :: Monad m => T.Text -> WhebT g s m (Maybe T.Text)-getRequestHeader k = getRequest >>= f- where hk = mk $ lazyTextToSBS k- f = (return . (fmap sbsToLazyText) . (lookup hk) . requestHeaders)---- * Responses---- | Set a Strict ByteString header for the response-setRawHeader :: Monad m => Header -> WhebT g s m ()-setRawHeader (hn, hc) = WhebT $ modify insertHeader - where insertHeader is@(InternalState {..}) = - is { respHeaders = M.insert hn hc respHeaders }- --- | Set a header for the response-setHeader :: Monad m => T.Text -> T.Text -> WhebT g s m ()-setHeader hn hc = setRawHeader (mk $ lazyTextToSBS hn, lazyTextToSBS hc)---- | Give filepath and content type to serve a file from disk.-file :: Monad m => T.Text -> T.Text -> WhebHandlerT g s m-file fp ct = do- setHeader (T.pack "Content-Type") (ct) - return $ HandlerResponse status200 (WhebFile fp)---- | Return simple HTML from Text-html :: Monad m => T.Text -> WhebHandlerT g s m-html c = do- setHeader (T.pack "Content-Type") (T.pack "text/html") - return $ HandlerResponse status200 c---- | Return simple Text -text :: Monad m => T.Text -> WhebHandlerT g s m-text c = do- setHeader (T.pack "Content-Type") (T.pack "text/plain") - return $ HandlerResponse status200 c---- * Running a Wheb Application---- | Running a Handler with a custom Transformer-debugHandlerT :: (Default s) => WhebOptions g s m ->- (m (Either WhebError a) -> IO (Either WhebError a)) ->- Request ->- WhebT g s m a ->- IO (Either WhebError a)-debugHandlerT opts@(WhebOptions {..}) runIO r h = - runIO $ runDebugHandler opts h baseData- where baseData = HandlerData startingCtx r ([], []) [] opts---- | Convenience wrapper for 'debugHandlerT' function in 'IO'-debugHandler :: (Default s) => WhebOptions g s IO -> - WhebT g s IO a ->- IO (Either WhebError a)-debugHandler opts h = debugHandlerT opts id defaultRequest h---- | Run a server with a function to run your inner Transformer to IO and --- generated options-runWhebServerT :: (Default s) => - (m EResponse -> IO EResponse) ->- WhebOptions g s m ->- IO ()-runWhebServerT runIO opts@(WhebOptions {..}) = do- putStrLn $ "Now running on port " ++ (show $ W.settingsPort $ warpSettings)- runSettings warpSettings $ - waiStack $ - optsToApplication opts runIO---- | Convenience wrapper for 'runWhebServerT' function in IO-runWhebServer :: (Default s) => - (WhebOptions g s IO) ->- IO ()-runWhebServer = runWhebServerT id
Wheb.cabal view
@@ -1,6 +1,6 @@ name: Wheb-version: 0.0.1.1-synopsis: The Batteries-Included Haskell WAI Framework+version: 0.1.0.0+synopsis: The frictionless WAI Framework license: BSD3 license-file: LICENSE author: Kyle Hanson@@ -8,21 +8,63 @@ maintainer: hanooter@gmail.com category: Web build-type: Simple-cabal-version: >=1.8+cabal-version: >=1.10 description: - Wheb aims at providing a simple simple and straightforward web server.+ Wheb's a framework for building robust high concurrency web applications simply and effectively. .+ * The core datatype will let you build anything from a read-only server to a fully interactive web application with basic Haskell.+ .+ * Minimal boilerplate to start your application.+ .+ * Named routes and URL generation (though it was a trade-off between named and type-safe urls).+ .+ * Easy to use for REST APIs+ .+ * Fully database and template agnostic+ .+ * Easy handler debugging.+ .+ * Middleware+ .+ * Fast. It deploys on warp.+ .+ /Plugins:/+ .+ Wheb makes it easy to write plugins. Plugins can add routes, middlware, settings and even handle resource cleanup on server shutdown.+ Named routes allow plugins to dynamically generate their routes at runtime based on settings.+ .+ Examples of plugins:+ .+ * Sessions+ .+ * Auth+ .+ * <http://hackage.haskell.org/package/wheb-mongo Wheb-Mongo>+ .+ /Wheb in action:/+ . > import Web.Wheb > import Data.Text.Lazy (pack) > > main :: IO () > main = do- > opts <- generateOptions $ addGET (pack ".") rootPat $ (text (pack "Hi!"))- > runWhebServer (opts :: MinOpts)+ > opts <- genMinOpts $ do+ > addGET "home" rootPat $ (text (pack "Hi!"))+ > addGET "about" ("about" </> "something") $ html (pack "<html><body><h1>About!</h1></body></html>")+ > runWhebServer opts+ .+ /Bigger example (Stateful.hs):/ . - Wheb makes it easy to share a global context and handle requests statefully+ Wheb makes it easy to share a global context and handle requests statefully. The Wheb monad+ is both a Reader and a State Monad allowing you to seperate thread-safe resources. .- >+ Below is an example of site that naively counts the non-unique hits across all pages. MyApp+ is our Reader's type and MyHandlerData is our State's type. MyApp is shared across requests+ while MyHandlerData is thread specific with a starting state given in options. We have a middleware+ that intercepts the request, safely increments the shared resource TVar and sets our MyHandlerData + to the correct count before it reaches our handler. We use a TVar in the Global context + because any state changes to the handler state will not affect other requests.+ . > import Control.Concurrent.STM > import Control.Monad.IO.Class > import Data.Monoid@@ -32,23 +74,22 @@ > data MyApp = MyApp Text (TVar Int) > data MyHandlerData = MyHandlerData Int >- > instance Default MyHandlerData where- > def = MyHandlerData 0- > > counterMw :: MonadIO m => WhebMiddleware MyApp MyHandlerData m > counterMw = do > (MyApp _ ctr) <- getApp- > number <- liftIO $ readTVarIO ctr- > liftIO $ atomically $ writeTVar ctr (succ number)- > putReqState (MyHandlerData number)+ > number <- liftIO $ atomically $ do+ > num <- readTVar ctr+ > writeTVar ctr (succ num)+ > return num+ > putHandlerState (MyHandlerData number) > return Nothing > > homePage :: WhebHandler MyApp MyHandlerData > homePage = do- > (MyApp appName _) <- getApp- > (MyHandlerData num) <- getReqState+ > (MyApp appName _) <- getApp+ > (MyHandlerData num) <- getHandlerState > html $ ("<h1>Welcome to" <> appName <> - > "</h1><h2>You are visitor #" <> (pack $ show num) <> "</h2>")+ > "</h1><h2>You are visitor #" <> (spack num) <> "</h2>") > > main :: IO () > main = do@@ -56,25 +97,55 @@ > startingCounter <- liftIO $ newTVarIO 0 > addWhebMiddleware counterMw > addGET (pack ".") rootPat $ homePage- > return $ MyApp "AwesomeApp" startingCounter+ > return $ (MyApp "AwesomeApp" startingCounter, MyHandlerData 0) > runWhebServer opts- .- Wheb allows you to write robust high concurrency web applications simply and effectively.- .- * The core datatype will allow you to build anything from a read-only server to a fully interactive web application with hundreds of routes without needing to define MonadTransformers.- .- * Minimal boilerplate to start your application.- .- * Plugin system- .+ source-repository head type: git location: git://github.com/hansonkd/Wheb-Framework.git library+ default-language: Haskell2010 exposed-modules: Web.Wheb, Web.Wheb.Cookie, Web.Wheb.InitM, Web.Wheb.Internal, Web.Wheb.Routes, Web.Wheb.Types, Web.Wheb.Utils, Web.Wheb.WhebT, Web.Wheb.Plugins.Auth, Web.Wheb.Plugins.Session, Web.Wheb.Plugins.Debug.MemoryBackend - build-depends: base ==4.6.*, text ==0.11.*, transformers ==0.3.*, data-default ==0.5.*, wai-extra ==2.0.*, time ==1.4.*, bytestring ==0.10.*, blaze-builder ==0.3.*, cookie ==0.4.*, mtl ==2.1.*, containers ==0.5.*, wai ==2.0.*, http-types ==0.8.*, warp ==2.0.*, conduit ==1.0.*, case-insensitive ==1.0.*, pwstore-fast ==2.4.*, uuid ==1.3.*, stm ==2.4.*+ build-depends: base ==4.6.*, + text ==0.11.*, + transformers ==0.3.*, + wai-extra ==2.0.*, + time ==1.4.*, + bytestring ==0.10.*, + blaze-builder ==0.3.*, + cookie ==0.4.*, + mtl ==2.1.*, + containers ==0.5.*, + wai ==2.0.*, + http-types ==0.8.*, + warp ==2.0.*, + conduit ==1.0.*, + case-insensitive ==1.0.*, + pwstore-fast ==2.4.*, + transformers ==0.3.*,+ uuid ==1.3.*, + stm ==2.4.*, + unix ==2.6.*+ hs-source-dirs: src GHC-options: -Wall -fno-warn-orphans++test-suite Main+ type: exitcode-stdio-1.0+ build-depends: Wheb ==0.1.*,+ base ==4.6.*, + HUnit >= 1.2 && < 2,+ QuickCheck >= 2.4,+ test-framework >= 0.4.1,+ test-framework-quickcheck2,+ test-framework-hunit,+ text ==0.11.*++ ghc-options: -Wall -rtsopts+ hs-source-dirs: tests+ default-language: Haskell2010+ hs-source-dirs: tests+ main-is: Main.hs
+ src/Web/Wheb.hs view
@@ -0,0 +1,115 @@+{-|++This module reexports Wheb modules. It should be the only thing you need to+import to get started.++@+import Web.Wheb+import Data.Text.Lazy (pack)++main :: IO ()+main = do+ opts <- generateOptions $ addGET (pack \".\") rootPat $ (text (pack \"Hi!\"))+ runWhebServer opts+@++-}+module Web.Wheb + (+ -- * Handlers+ -- ** ReaderT and StateT Functionality+ -- *** ReaderT+ getApp+ , getWithApp+ -- *** StateT+ , getHandlerState+ , putHandlerState+ , modifyHandlerState+ , modifyHandlerState'+ + -- ** Dealing with responses+ -- *** Creating a 'HandlerResponse'+ , html+ , text+ , file+ , builder+ -- *** Setting a header+ , setHeader+ , setRawHeader+ + -- * Settings+ , getSetting+ , getSetting'+ , getSetting''+ , getSettings+ + -- * Routes+ , getRouteParams+ , getRouteParam+ , getRoute+ , getRoute'+ + -- * Request reading+ , getRequest+ , getRequestHeader+ , getWithRequest+ , getQueryParams+ , getPOSTParam+ , getPOSTParams+ , getRawPOST+ + -- ** Running Wheb+ , runWhebServer+ , runWhebServerT+ , debugHandler+ , debugHandlerT+ + -- * Initialize+ -- ** Routes+ -- *** Named routes convenience functions+ , addGET+ , addPOST+ , addPUT+ , addDELETE+ -- *** Add raw routes+ , addRoute+ , addRoutes+ , catchAll+ -- ** Middlewares+ , addWAIMiddleware+ , addWhebMiddleware+ -- ** Settings+ , addSetting+ , addSetting'+ , addSettings+ , readSettingsFile+ -- ** Cleanup+ , addCleanupHook+ -- * Running+ , generateOptions+ , genMinOpts+ -- * Routes+ -- ** URL Patterns+ , compilePat+ , rootPat+ + -- ** URL building+ , (</>)+ , grabInt+ , grabText+ , pT+ , pS+ -- * Utilities+ , spack+ , MonadIO(..)+ -- * Types+ , module Web.Wheb.Types+ ) where+++import Web.Wheb.WhebT+import Web.Wheb.InitM+import Web.Wheb.Types+import Web.Wheb.Routes+import Web.Wheb.Utils+import Control.Monad.IO.Class
+ src/Web/Wheb/Cookie.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}++module Web.Wheb.Cookie+ ( setCookie+ , setCookie'+ , getCookie+ , getCookies+ , removeCookie+ ) where+ +import qualified Data.ByteString as BS+import qualified Blaze.ByteString.Builder as B+import Data.Maybe (fromMaybe)+import Data.Text.Lazy (Text)+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.Encoding as T+import Data.Time.Calendar+import Data.Time.Clock+import Web.Cookie++import Web.Wheb.Types+import Web.Wheb.Internal+import Web.Wheb.WhebT+import Web.Wheb.Utils++getDefaultCookie :: Monad m => WhebT g s m SetCookie+getDefaultCookie = return def -- Populate with settings...++setCookie :: Monad m => Text -> Text -> WhebT g s m ()+setCookie k v = getDefaultCookie >>= (setCookie' k v)++setCookie' :: Monad m => Text -> Text -> SetCookie -> WhebT g s m ()+setCookie' k v sc = setRawHeader ("Set-Cookie", cookieText)+ where cookie = sc { setCookieName = lazyTextToSBS k+ , setCookieValue = lazyTextToSBS v+ }+ cookieText = B.toByteString $ renderSetCookie cookie+ +getCookies :: Monad m => WhebT g s m CookiesText+getCookies = (getRequestHeader "Cookie") >>= + (return . parseFunc . (fromMaybe T.empty))+ where parseFunc = parseCookiesText . lazyTextToSBS++getCookie :: Monad m => Text -> WhebT g s m (Maybe Text)+getCookie k = getCookies >>= + (return . (fmap T.fromStrict) . (lookup (T.toStrict k)))++removeCookie :: Monad m => Text -> WhebT g s m ()+removeCookie k = do+ defCookie <- getDefaultCookie+ let utcLongAgo = UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)+ expiredCookie = defCookie {setCookieExpires = Just utcLongAgo}+ setCookie' k T.empty expiredCookie
+ src/Web/Wheb/InitM.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE RecordWildCards #-}++module Web.Wheb.InitM+ (+ -- * Routes+ -- ** Named routes convenience functions+ addGET+ , addPOST+ , addPUT+ , addDELETE+ -- ** Add raw routes+ , addRoute+ , addRoutes+ , catchAll+ -- * Middlewares+ , addWAIMiddleware+ , addWhebMiddleware+ -- * Settings+ , addSetting+ , addSetting'+ , addSettings+ , readSettingsFile+ -- * Cleanup+ , addCleanupHook+ -- * Running+ , generateOptions+ , genMinOpts+ ) where++import Control.Concurrent.STM+import Control.Monad.IO.Class+import Control.Monad.Writer+import Data.Char (isSpace)+import qualified Data.Map as M+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.IO as T+import Data.Typeable+import Network.Wai+import Network.Wai.Handler.Warp (defaultSettings+ , settingsOnOpen+ , settingsOnClose)+import Network.HTTP.Types.Method+import Text.Read (readMaybe)++import Web.Wheb.Internal+import Web.Wheb.Routes+import Web.Wheb.Types+import Web.Wheb.Utils++addGET :: T.Text -> UrlPat -> WhebHandlerT g s m -> InitM g s m ()+addGET n p h = addRoute $ patRoute (Just n) GET p h++addPOST :: T.Text -> UrlPat -> WhebHandlerT g s m -> InitM g s m ()+addPOST n p h = addRoute $ patRoute (Just n) POST p h++addPUT :: T.Text -> UrlPat -> WhebHandlerT g s m -> InitM g s m ()+addPUT n p h = addRoute $ patRoute (Just n) PUT p h++addDELETE :: T.Text -> UrlPat -> WhebHandlerT g s m -> InitM g s m ()+addDELETE n p h = addRoute $ patRoute (Just n) DELETE p h++addRoute :: Route g s m -> InitM g s m ()+addRoute r = addRoutes [r]++addRoutes :: [Route g s m] -> InitM g s m ()+addRoutes rs = InitM $ tell $ mempty { initRoutes = rs }++-- | Catch all requests regardless of method or path+catchAll :: WhebHandlerT g s m -> InitM g s m ()+catchAll h = addRoute $ Route Nothing (const True) parser h+ where parser = UrlParser (const (Just [])) (const (Right $ T.pack "/*"))++-- | Add generic "WAI" middleware+addWAIMiddleware :: Middleware -> InitM g s m ()+addWAIMiddleware m = InitM $ tell $ mempty { initWaiMw = m }++-- | Add "Wheb" specific middleware+addWhebMiddleware :: WhebMiddleware g s m -> InitM g s m ()+addWhebMiddleware m = InitM $ tell $ mempty { initWhebMw = [m] }++-- | Wrapped 'addSetting'' to help prevent monomorphism errors for simple settings.+addSetting :: T.Text -> T.Text -> InitM g s m ()+addSetting = addSetting'++-- | Adds a setting value, replacing it if its key already exists.+addSetting' :: Typeable a => T.Text -> a -> InitM g s m ()+addSetting' k v = addSettings $ M.fromList [(k, MkVal v)]++addSettings :: CSettings -> InitM g s m ()+addSettings settings = InitM $ tell $ mempty { initSettings = settings }++-- | Reads a file line by line and splits keys and values by \":\".+-- Uses default "Text.Read" to try to match 'Int', 'Bool' or 'Float' and will add+-- specific typed settings for those.+readSettingsFile :: FilePath -> InitM g s m ()+readSettingsFile fp = (liftIO $ liftM T.lines (T.readFile fp)) >>= (mapM_ parseLines)+ where parseLines line = + case T.splitOn (T.pack ":") line of + a:b:_ -> do+ let k = T.strip a+ v = T.strip b+ maybePutSetting k v (readText :: (T.Text -> Maybe Int))+ maybePutSetting k v (readText :: (T.Text -> Maybe Bool))+ maybePutSetting k v (readText :: (T.Text -> Maybe Float))+ addSetting k v+ _ -> return ()+ readText :: Read a => T.Text -> Maybe a+ readText = readMaybe . T.unpack+ maybePutSetting k t parse = maybe (return ()) (addSetting' k) (parse t)++-- | IO Actions to run after server has been stopped.+addCleanupHook :: IO () -> InitM g s m ()+addCleanupHook action = InitM $ tell $ mempty { initCleanup = [action] }++-- | Generate 'WhebOptions' from 'InitM' in 'IO'+generateOptions :: MonadIO m => InitM g s m (g, s) -> IO (WhebOptions g s m)+generateOptions m = do + ((g, s), InitOptions {..}) <- runWriterT (runInitM m)+ tv <- liftIO $ newTVarIO False+ ac <- liftIO $ newTVarIO 0+ let warpsettings = defaultSettings + { settingsOnOpen = atomically (addToTVar ac)+ , settingsOnClose = atomically (subFromTVar ac)}+ return $ WhebOptions { appRoutes = initRoutes+ , runTimeSettings = initSettings+ , warpSettings = warpsettings+ , startingCtx = g+ , startingState = InternalState s M.empty+ , waiStack = initWaiMw+ , whebMiddlewares = initWhebMw+ , defaultErrorHandler = defaultErr+ , shutdownTVar = tv+ , activeConnections = ac+ , cleanupActions = initCleanup }+ where addToTVar ac = ((readTVar ac) >>= (\cs -> writeTVar ac (succ cs)))+ subFromTVar ac = ((readTVar ac) >>= (\cs -> writeTVar ac (pred cs)))++-- | Generate options for an application without a context or state+genMinOpts :: InitM () () IO () -> IO MinOpts+genMinOpts m = generateOptions (m >> (return ((), ())))
+ src/Web/Wheb/Internal.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE RecordWildCards #-}++module Web.Wheb.Internal where++import Control.Monad.Error+import Control.Monad.IO.Class+import Control.Monad.Reader+import Control.Monad.State+import Control.Monad.Writer+import qualified Data.Map as M+import qualified Data.Text.Lazy as T++import Network.HTTP.Types.Method+import Network.Wai+import Network.Wai.Parse++import Web.Wheb.Routes+import Web.Wheb.Types+import Web.Wheb.Utils++-- * Converting to WAI application+ +-- | Convert 'WhebOptions' to 'Application' +optsToApplication :: WhebOptions g s m ->+ (m EResponse -> IO EResponse) ->+ Application+optsToApplication opts@(WhebOptions {..}) runIO r = do+ pData <- liftIO (parseRequestBody lbsBackEnd r)+ res <- runIO $ do+ let mwData = baseData { postData = pData }+ (mRes, st) <- runMiddlewares opts whebMiddlewares mwData+ case mRes of+ Just resp -> return $ Right resp+ Nothing -> case (findUrlMatch stdMthd pathChunks appRoutes) of+ Just (h, params) -> do+ let hData = mwData { routeParams = params }+ runWhebHandler opts h st hData + Nothing -> return $ Left Error404+ either handleError return res+ where baseData = HandlerData startingCtx r ([], []) [] opts+ pathChunks = fmap T.fromStrict $ pathInfo r+ stdMthd = either (\_-> GET) id $ parseMethod $ requestMethod r+ runErrorHandler eh = runWhebHandler opts eh startingState baseData+ handleError err = do+ errRes <- runIO $ runErrorHandler (defaultErrorHandler err)+ either (return . (const uhOh)) return errRes++-- * Running Handlers++-- | Run all inner wheb monads to the top level.+runWhebHandler :: Monad m =>+ WhebOptions g s m ->+ WhebHandlerT g s m ->+ InternalState s ->+ HandlerData g s m ->+ m EResponse+runWhebHandler opts@(WhebOptions {..}) handler st hd = do+ (resp, InternalState {..}) <- flip runStateT st $ do+ flip runReaderT hd $+ runErrorT $+ runWhebT handler+ return $ fmap (convertResponse respHeaders) resp + where convertResponse hds (HandlerResponse status resp) =+ toResponse status (M.toList hds) resp++-- | Same as above but returns arbitrary type for debugging.+runDebugHandler :: Monad m =>+ WhebOptions g s m ->+ WhebT g s m a ->+ HandlerData g s m ->+ m (Either WhebError a)+runDebugHandler opts@(WhebOptions {..}) handler hd = do+ flip evalStateT startingState $ do+ flip runReaderT hd $+ runErrorT $+ runWhebT handler+ where convertResponse hds (HandlerResponse status resp) =+ toResponse status (M.toList hds) resp+-- * Running Middlewares+ +-- | Runs middlewares in order, stopping if one returns a response+runMiddlewares :: Monad m =>+ WhebOptions g s m ->+ [WhebMiddleware g s m] ->+ HandlerData g s m ->+ m (Maybe Response, InternalState s)+runMiddlewares opts mWs hd = loop mWs (startingState opts)+ where loop [] st = return (Nothing, st)+ loop (mw:mws) st = do+ mwResult <- (runWhebMiddleware opts st hd mw)+ case mwResult of+ (Just resp, nst) -> return mwResult+ (Nothing, nst) -> loop mws nst++runWhebMiddleware :: Monad m =>+ WhebOptions g s m ->+ InternalState s ->+ HandlerData g s m ->+ WhebMiddleware g s m ->+ m (Maybe Response, InternalState s)+runWhebMiddleware opts@(WhebOptions {..}) st hd mW = do+ (eresp, is@InternalState {..}) <- flip runStateT st $ do+ flip runReaderT hd $+ runErrorT $+ runWhebT mW+ return $ (convertResponse respHeaders eresp, is)+ where convertResponse hds (Right (Just (HandlerResponse status resp))) =+ Just (toResponse status (M.toList hds) resp)+ convertResponse _ _ = Nothing
+ src/Web/Wheb/Plugins/Auth.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE RankNTypes #-}++module Web.Wheb.Plugins.Auth + ( + -- * Main functions+ login+ , logout+ , register+ , getCurrentUser+ , queryCurrentUser+ , loginRequired+ + -- * Middleware+ , authMiddleware + + -- * Types+ , AuthUser (..)+ , AuthContainer (..)+ , AuthApp (..)+ , AuthState (..)+ , AuthBackend (..)+ , AuthError (..)++ , UserKey+ , Password+ , PwHash+ + -- * Utils+ , makePwHash+ , verifyPw+ , getUserSessionKey+ ) where++import Control.Applicative ((<*>))+import Control.Monad (void, liftM)+import Control.Monad.Error+import Control.Monad.IO.Class+import Crypto.PasswordStore+import Data.Maybe+import Data.Text.Lazy (Text)+import Data.Text.Lazy as T+import Data.Text.Lazy.Encoding as T+import Data.Text.Encoding as ES++import Web.Wheb+import Web.Wheb.Types+import Web.Wheb.Plugins.Session+ +-- * Auth functions++-- | Register a user+register :: (AuthApp a, MonadIO m) => AuthUser -> Password -> WhebT a b m (Either AuthError AuthUser)+register un pw = runWithContainer $ backendRegister un pw++-- | Log a user in+login :: (AuthApp a, AuthState b, MonadIO m) => UserKey -> Password -> WhebT a b m (Either AuthError AuthUser)+login un pw = do+ loginResult <- (runWithContainer $ backendLogin un pw)+ case loginResult of+ Right au@(AuthUser userKey) -> do+ sessionKey <- getUserSessionKey+ deleteSessionValue sessionKey+ setSessionValue sessionKey userKey+ authSetUser (Just au)+ _ -> return ()+ return loginResult++-- | Log a user out+logout :: (AuthApp a, AuthState b, MonadIO m) => WhebT a b m ()+logout = (runWithContainer backendLogout) >> (authSetUser Nothing)++-- | Get the current user from the handler state (Needs to be populated first+-- with 'authMiddleware')+getCurrentUser :: (AuthState b, MonadIO m) => WhebT a b m (Maybe AuthUser)+getCurrentUser = liftM getAuthUser getHandlerState++-- | Explicitly query a user with the backend. Since this is an IO hit, it is+-- better to use the middleware and 'getCurrentUser'+queryCurrentUser :: (AuthApp a, MonadIO m) => WhebT a b m (Maybe AuthUser)+queryCurrentUser = getUserSessionKey >>= + getSessionValue' (T.pack "") >>=+ (\uid -> runWithContainer $ backendGetUser uid)++-- | Checks if a user is logged in with 'getCurrentUser' and throws a 500+-- if they aren't.+loginRequired :: (AuthState b, MonadIO m) =>+ WhebHandlerT a b m ->+ WhebHandlerT a b m+loginRequired action = getCurrentUser >>=+ (maybe (throwError Error403) (const action))++-- * Middleware++-- | Auto-populates the handler state with the current user.+authMiddleware :: (AuthApp a, AuthState b, MonadIO m) => WhebMiddleware a b m+authMiddleware = do+ cur <- queryCurrentUser+ authSetUser cur+ return Nothing+++type UserKey = Text+type Password = Text+type PwHash = Text++data AuthError = DuplicateUsername | UserDoesNotExist | InvalidPassword+ deriving (Show)++data AuthUser = AuthUser { uniqueUserKey :: UserKey } deriving (Show)++type PossibleUser = Maybe AuthUser++data AuthContainer = forall r. AuthBackend r => AuthContainer r++-- | Interface for creating Auth backends+class SessionApp a => AuthApp a where+ getAuthContainer :: a -> AuthContainer++-- | Minimal implementation for a +class AuthState a where+ getAuthUser :: a -> PossibleUser + modifyAuthUser :: (PossibleUser -> PossibleUser) -> a -> a++-- | Interface for creating Auth backends+class AuthBackend c where+ backendLogin :: (AuthApp a, MonadIO m) => SessionApp a => UserKey -> Password -> c -> WhebT a b m (Either AuthError AuthUser)+ backendRegister :: (AuthApp a, MonadIO m) => AuthUser -> Password -> c -> WhebT a b m (Either AuthError AuthUser)+ backendGetUser :: (AuthApp a, MonadIO m) => UserKey -> c -> WhebT a b m (Maybe AuthUser)+ backendLogout :: (AuthApp a, MonadIO m) => c -> WhebT a b m ()+ backendLogout _ = getUserSessionKey >>= deleteSessionValue+ +-- * Internal++runWithContainer :: (AuthApp a, MonadIO m) =>+ (forall r. AuthBackend r => r -> WhebT a s m b) -> + WhebT a s m b+runWithContainer f = do+ AuthContainer authStore <- getWithApp getAuthContainer+ f authStore++authSetUser :: (AuthApp a, AuthState b, MonadIO m) => PossibleUser -> WhebT a b m ()+authSetUser cur = modifyHandlerState' (modifyAuthUser (const cur))++getUserSessionKey :: (AuthApp a, MonadIO m) => WhebT a b m Text+getUserSessionKey = return $ T.pack "user-id" -- later read from settings.++makePwHash :: MonadIO m => Password -> WhebT a b m PwHash+makePwHash pw = liftM (T.fromStrict . ES.decodeUtf8) $ + liftIO $ makePassword (ES.encodeUtf8 $ T.toStrict pw) 14++verifyPw :: Text -> Text -> Bool+verifyPw pw hash = verifyPassword (ES.encodeUtf8 $ T.toStrict pw) + (ES.encodeUtf8 $ T.toStrict hash)
+ src/Web/Wheb/Plugins/Debug/MemoryBackend.hs view
@@ -0,0 +1,71 @@+module Web.Wheb.Plugins.Debug.MemoryBackend where++import Control.Monad (liftM)+import Control.Monad.IO.Class+import Control.Concurrent.STM+import Data.Text.Lazy (Text)+import Data.Map as M+import Data.Maybe (fromMaybe, fromJust)++import Web.Wheb+import Web.Wheb.Types+import Web.Wheb.Plugins.Auth+import Web.Wheb.Plugins.Session++data SessionData = SessionData + { sessionMemory :: TVar (M.Map Text (M.Map Text Text)) }+data UserData = UserData+ { userStorage :: TVar (M.Map UserKey PwHash) }++-- | In memory session backend. Session values +-- will not persist after server restart.+instance SessionBackend SessionData where+ backendSessionPut sessId key content (SessionData tv) =+ let insertFunc = (\sess -> + Just $ M.insert key content (fromMaybe M.empty sess)+ )+ tVarFunc = M.alter insertFunc sessId+ in liftIO $ atomically $ modifyTVar tv tVarFunc+ backendSessionGet sessId key (SessionData tv) = do+ curSessions <- liftIO $ readTVarIO tv+ return $ (M.lookup sessId curSessions) >>= (M.lookup key)+ backendSessionDelete sessId key (SessionData tv) =+ liftIO $ atomically $ modifyTVar tv (M.update (Just . (M.delete key)) sessId)+ backendSessionClear sessId (SessionData tv) =+ liftIO $ atomically $ modifyTVar tv (M.delete sessId)++-- | In memory auth backend. User values +-- will not persist after server restart.+instance AuthBackend UserData where+ backendGetUser name (UserData tv) = do+ possUser <- liftM (M.lookup name) $ liftIO $ readTVarIO tv+ case possUser of+ Nothing -> return Nothing+ Just _ -> return $ Just (AuthUser name)+ backendLogin name pw (UserData tv) = do+ users <- liftIO $ readTVarIO $ tv+ let possUser = M.lookup name users+ passCheck = fmap (verifyPw pw) possUser+ case passCheck of+ Just True -> return (Right $ AuthUser $ name)+ Just False -> return (Left InvalidPassword)+ Nothing -> return (Left UserDoesNotExist)+ backendRegister (AuthUser name) pw (UserData tv) = do+ users <- liftIO $ readTVarIO $ tv+ if M.member name users+ then return (Left DuplicateUsername)+ else do+ pwHash <- makePwHash pw+ liftIO $ atomically $ writeTVar tv (M.insert name pwHash users)+ return (Right $ AuthUser name)+ backendLogout _ = getUserSessionKey >>= deleteSessionValue+ +initSessionMemory :: InitM g s m SessionContainer+initSessionMemory = do+ tv <- liftIO $ newTVarIO $ M.empty+ return $! SessionContainer $ SessionData tv++initAuthMemory :: InitM g s m AuthContainer+initAuthMemory = do+ tv <- liftIO $ newTVarIO $ M.empty+ return $! AuthContainer $ UserData tv
+ src/Web/Wheb/Plugins/Session.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE RankNTypes #-}++module Web.Wheb.Plugins.Session + ( SessionContainer (..)+ , SessionApp (..)+ , SessionBackend (..)+ + , setSessionValue+ , getSessionValue+ , getSessionValue'+ , deleteSessionValue+ , generateSessionKey+ , getCurrentSessionKey+ , clearSessionKey+ ) where+ +import Control.Monad.IO.Class+import Control.Monad (liftM)+import Data.Maybe+import Data.Text.Lazy (Text, pack)+import Data.Text.Lazy.Encoding as T+import Data.UUID+import Data.UUID.V4++import Web.Wheb+import Web.Wheb.Cookie+import Web.Wheb.Types++-- | Initial pass on abstract plugin for Sessions.+-- Possibly add support for Typable to ease typecasting.+++session_cookie_key = pack "-session-"++data SessionContainer = forall r. SessionBackend r => SessionContainer r++class SessionApp a where+ getSessionContainer :: a -> SessionContainer++class SessionBackend c where+ backendSessionPut :: (SessionApp a, MonadIO m) => Text -> Text -> Text -> c -> WhebT a b m ()+ backendSessionGet :: (SessionApp a, MonadIO m) => Text -> Text -> c -> WhebT a b m (Maybe Text)+ backendSessionDelete :: (SessionApp a, MonadIO m) => Text -> Text -> c -> WhebT a b m ()+ backendSessionClear :: (SessionApp a, MonadIO m) => Text -> c -> WhebT a b m ()++runWithContainer :: (SessionApp a, MonadIO m) => (forall r. SessionBackend r => r -> WhebT a s m b) -> WhebT a s m b+runWithContainer f = do+ SessionContainer sessStore <- getWithApp getSessionContainer+ f sessStore++deleteSessionValue :: (SessionApp a, MonadIO m) => Text -> WhebT a b m ()+deleteSessionValue key= do+ sessId <- getCurrentSessionKey + runWithContainer $ backendSessionDelete sessId key++setSessionValue :: (SessionApp a, MonadIO m) => Text -> Text -> WhebT a b m ()+setSessionValue key content = do+ sessId <- getCurrentSessionKey + runWithContainer $ backendSessionPut sessId key content++getSessionValue :: (SessionApp a, MonadIO m) => Text -> WhebT a b m (Maybe Text)+getSessionValue key = do+ sessId <- getCurrentSessionKey+ runWithContainer $ backendSessionGet sessId key++getSessionValue' :: (SessionApp a, MonadIO m) => Text -> Text -> WhebT a b m Text+getSessionValue' def key = liftM (fromMaybe def) (getSessionValue key)+ +getSessionCookie :: (SessionApp a, MonadIO m) => WhebT a b m (Maybe Text)+getSessionCookie = getCookie session_cookie_key+ +generateSessionKey :: (SessionApp a, MonadIO m) => WhebT a b m Text+generateSessionKey = do+ newKey <- liftM (T.decodeUtf8 . toLazyASCIIBytes) (liftIO nextRandom)+ setCookie session_cookie_key newKey+ return newKey++getCurrentSessionKey :: (SessionApp a, MonadIO m) => WhebT a b m Text+getCurrentSessionKey = do+ curKey <- getSessionCookie+ case curKey of+ Just key -> return key+ Nothing -> generateSessionKey++clearSessionKey :: (SessionApp a, MonadIO m) => WhebT a b m Text+clearSessionKey = do+ curKey <- getSessionCookie+ newKey <- generateSessionKey+ case curKey of+ Nothing -> return newKey+ Just oldKey -> do+ runWithContainer $ backendSessionClear oldKey+ return newKey
+ src/Web/Wheb/Routes.hs view
@@ -0,0 +1,168 @@+module Web.Wheb.Routes+ (+ -- * URL building+ (</>)+ , grabInt+ , grabText+ , pT+ , pS+ -- * Convenience constructors+ , patRoute+ -- * URL Patterns+ , compilePat+ , rootPat+ + -- * Working with URLs+ , getParam+ , matchUrl+ , generateUrl+ , findUrlMatch++ -- * Utilities+ , testUrlParser+ ) where+ +import qualified Data.Text.Lazy as T+import Data.Text.Lazy.Read+import Data.Maybe (isJust)+import Data.Typeable+import Network.HTTP.Types.Method+import Network.HTTP.Types.URI++import Data.Maybe (fromJust)+import Data.Monoid ((<>))++import Web.Wheb.Types+import Web.Wheb.Utils++-- | Build a 'Route' from a 'UrlPat'+patRoute :: (Maybe T.Text) -> + StdMethod -> + UrlPat -> + WhebHandlerT g s m -> + Route g s m+patRoute n m p = Route n (==m) (compilePat p)++-- | Convert a 'UrlPat' to a 'UrlParser'+compilePat :: UrlPat -> UrlParser+compilePat (Composed a) = UrlParser (matchPat a) (buildPat a)+compilePat a = UrlParser (matchPat [a]) (buildPat [a])++-- | Represents root path @/@+rootPat :: UrlPat+rootPat = Chunk $ T.pack ""++-- | Allows for easier building of URL patterns+-- This should be the primary URL constructor.+-- +-- > (\"blog\" '</>' ('grabInt' \"pk\") '</>' \"edit\" '</>' ('grabText' \"verb\"))+-- +(</>) :: UrlPat -> UrlPat -> UrlPat+(Composed a) </> (Composed b) = Composed (a ++ b)+a </> (Composed b) = Composed (a:b)+(Composed a) </> b = Composed (a ++ [b])+a </> b = Composed [a, b]++-- | Parses URL parameter and matches on 'Int'+grabInt :: T.Text -> UrlPat+grabInt key = FuncChunk key f IntChunk+ where rInt = decimal :: Reader Int+ f = ((either (const Nothing) (Just . MkChunk . fst)) . rInt)++-- | Parses URL parameter and matches on 'Text'+grabText :: T.Text -> UrlPat+grabText key = FuncChunk key (Just . MkChunk) TextChunk++-- | Constructors to use w/o OverloadedStrings+pT :: T.Text -> UrlPat+pT = Chunk++pS :: String -> UrlPat+pS = pT . T.pack++-- | Lookup and cast a URL parameter to its expected type if it exists.+getParam :: Typeable a => T.Text -> RouteParamList -> Maybe a+getParam k l = (lookup k l) >>= unwrap+ where unwrap :: Typeable a => ParsedChunk -> Maybe a+ unwrap (MkChunk a) = cast a++-- | Convert URL chunks (split on /) +matchUrl :: [T.Text] -> UrlParser -> Maybe RouteParamList+matchUrl url (UrlParser f _) = f url++-- | Runs a 'UrlParser' with 'RouteParamList' to a URL path+generateUrl :: UrlParser -> RouteParamList -> Either UrlBuildError T.Text+generateUrl (UrlParser _ f) = f++-- | Sort through a list of routes to find a Handler and 'RouteParamList'+findUrlMatch :: StdMethod ->+ [T.Text] ->+ [Route g s m] ->+ Maybe (WhebHandlerT g s m, RouteParamList)+findUrlMatch _ _ [] = Nothing+findUrlMatch rmtd path ((Route _ methodMatch (UrlParser f _) h):rs) + | not (methodMatch rmtd) = findUrlMatch rmtd path rs+ | otherwise = case f path of+ Just params -> Just (h, params)+ Nothing -> findUrlMatch rmtd path rs++-- | Test a parser to make sure it can match what it produces and vice versa+testUrlParser :: UrlParser -> RouteParamList -> Bool+testUrlParser up rpl = + case generateUrl up rpl of+ Left _ -> False+ Right t -> case (matchUrl (fmap T.fromStrict $ decodeUrl t) up) of+ Just params -> either (const False) (==t) (generateUrl up params)+ Nothing -> False+ where decodeUrl = decodePathSegments . lazyTextToSBS++-- | Implementation for a 'UrlParser' using pseudo-typed URL composition.+-- Pattern will match path when the pattern is as long as the path, matching+-- on a trailing slash. If the path is longer or shorter than the pattern, it+-- should not match.+-- Example:+-- Given a url = "blog" </> (grabInt "pk") </> "edit"+-- This will match on "/blog/1/edit" and /blog/9999/edit/"+-- But not "/blog/1/", "/blog/1", "blog/foo/edit/", "/blog/9/edit/d",+-- nor "/blog/9/edit//"+matchPat :: [UrlPat] -> [T.Text] -> Maybe RouteParamList+matchPat chunks [] = matchPat chunks [T.pack ""]+matchPat chunks t = parse t chunks []+ where parse [] [] params = Just params+ parse [] c params = Nothing+ parse (u:[]) [] params | T.null u = Just params+ | otherwise = Nothing+ parse (u:us) [] _ = Nothing+ parse (u:us) ((Chunk c):cs) params | T.null c = parse (u:us) cs params+ | u == c = parse us cs params+ | otherwise = Nothing+ parse (u:us) ((FuncChunk k f _):cs) params = do+ val <- f u+ parse us cs ((k, val):params)+ parse us ((Composed xs):cs) params = parse us (xs ++ cs) params++buildPat :: [UrlPat] -> RouteParamList -> Either UrlBuildError T.Text+buildPat pats params = fmap addSlashes $ build [] pats+ where build acc [] = Right acc+ build acc ((Chunk c):[]) = build (acc <> [c]) []+ build acc ((Chunk c):cs) | T.null c = build acc cs+ | otherwise = build (acc <> [c]) cs+ build acc ((Composed xs):cs) = build acc (xs <> cs)+ build acc ((FuncChunk k _ t):cs) = + case (showParam t k params) of+ (Right v) -> build (acc <> [v]) cs+ (Left err) -> Left err+ addSlashes [] = T.pack "/"+ addSlashes list = builderToText $+ encodePathSegments (fmap T.toStrict list)++showParam :: ChunkType -> T.Text -> RouteParamList -> Either UrlBuildError T.Text+showParam chunkType k l = + case (lookup k l) of+ Just (MkChunk v) -> case chunkType of+ IntChunk -> toEither $ fmap spack (cast v :: Maybe Int)+ TextChunk -> toEither (cast v :: Maybe T.Text)+ Nothing -> Left NoParam+ where toEither v = case v of+ Just b -> Right b+ Nothing -> Left $ ParamTypeMismatch k
+ src/Web/Wheb/Types.hs view
@@ -0,0 +1,174 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE MultiParamTypeClasses #-}++module Web.Wheb.Types where++import Blaze.ByteString.Builder (Builder, fromLazyByteString)+import Control.Concurrent.STM+import Control.Applicative+import Control.Monad.Error+import Control.Monad.Trans+import Control.Monad.IO.Class+import Control.Monad.State+import Control.Monad.Reader+import Control.Monad.Writer+import Data.Monoid ((<>))++import qualified Data.ByteString.Lazy as LBS+import Data.List (intercalate)+import Data.Map as M+import Data.String (IsString(..))+import qualified Data.Text.Lazy as T+import Data.Typeable++import Network.Wai (Request, Response, Middleware, responseBuilder)+import Network.Wai.Handler.Warp as Warp+import Network.Wai.Parse+import Network.HTTP.Types.Method+import Network.HTTP.Types.Status+import Network.HTTP.Types.Header++import Data.ByteString (ByteString)+++-- | WhebT g s m+--+-- * g -> The global confirgured context (Read-only data shared between threads)+-- +-- * s -> Handler state for each request.+--+-- * m -> Monad we are transforming+newtype WhebT g s m a = WhebT + { runWhebT :: ErrorT WhebError + (ReaderT (HandlerData g s m) (StateT (InternalState s) m)) a + } deriving ( Functor, Applicative, Monad, MonadIO )++instance MonadTrans (WhebT g s) where+ lift = WhebT . lift . lift . lift++instance (Monad m) => MonadError WhebError (WhebT g s m) where+ throwError = WhebT . throwError+ catchError (WhebT m) f = WhebT (catchError m (runWhebT . f))++-- | Writer Monad to build options.+newtype InitM g s m a = InitM { runInitM :: WriterT (InitOptions g s m) IO a}+ deriving (Functor, Applicative, Monad, MonadIO)++-- | Converts a type to a WAI 'Response'+class WhebContent a where+ toResponse :: Status -> ResponseHeaders -> a -> Response++-- | A Wheb response that represents a file.+data WhebFile = WhebFile T.Text++data HandlerResponse = forall a . WhebContent a => HandlerResponse Status a++-- | Our 'ReaderT' portion of 'WhebT' uses this.+data HandlerData g s m = + HandlerData { globalCtx :: g+ , request :: Request+ , postData :: ([Param], [File LBS.ByteString])+ , routeParams :: RouteParamList+ , globalSettings :: WhebOptions g s m }++-- | Our 'StateT' portion of 'WhebT' uses this.+data InternalState s =+ InternalState { reqState :: s+ , respHeaders :: M.Map HeaderName ByteString } + +data SettingsValue = forall a. (Typeable a) => MkVal a++data WhebError = Error500 String + | Error404+ | Error403+ | RouteParamDoesNotExist+ | URLError T.Text UrlBuildError+ deriving (Show)++instance Error WhebError where + strMsg = Error500++-- | Monoid to use in InitM's WriterT+data InitOptions g s m =+ InitOptions { initRoutes :: [ Route g s m ]+ , initSettings :: CSettings+ , initWaiMw :: Middleware+ , initWhebMw :: [ WhebMiddleware g s m ]+ , initCleanup :: [ IO () ] }++instance Monoid (InitOptions g s m) where+ mappend (InitOptions a1 b1 c1 d1 e1) (InitOptions a2 b2 c2 d2 e2) = + InitOptions (a1 <> a2) (b1 <> b2) (c2 . c1) (d1 <> d2) (e1 <> e2)+ mempty = InitOptions mempty mempty id mempty mempty++-- | The main option datatype for Wheb+data WhebOptions g s m = MonadIO m => + WhebOptions { appRoutes :: [ Route g s m ]+ , runTimeSettings :: CSettings+ , warpSettings :: Warp.Settings+ , startingCtx :: g -- ^ Global ctx shared between requests+ , startingState :: InternalState s -- ^ Handler state given each request+ , waiStack :: Middleware+ , whebMiddlewares :: [ WhebMiddleware g s m ]+ , defaultErrorHandler :: WhebError -> WhebHandlerT g s m+ , shutdownTVar :: TVar Bool+ , activeConnections :: TVar Int+ , cleanupActions :: [ IO () ] }++type EResponse = Either WhebError Response++type CSettings = M.Map T.Text SettingsValue+ +type WhebHandler g s = WhebT g s IO HandlerResponse+type WhebHandlerT g s m = WhebT g s m HandlerResponse+type WhebMiddleware g s m = WhebT g s m (Maybe HandlerResponse)++-- | A minimal type for WhebT+type MinWheb a = WhebT () () IO a+type MinHandler = MinWheb HandlerResponse+-- | A minimal type for WhebOptions+type MinOpts = WhebOptions () () IO++-- * Routes++type RouteParamList = [(T.Text, ParsedChunk)]+type MethodMatch = StdMethod -> Bool++data ParsedChunk = forall a. (Typeable a, Show a) => MkChunk a++instance Show ParsedChunk where+ show (MkChunk a) = show a++data UrlBuildError = NoParam | ParamTypeMismatch T.Text | UrlNameNotFound+ deriving (Show) ++-- | A Parser should be able to extract params and regenerate URL from params.+data UrlParser = UrlParser + { parseFunc :: ([T.Text] -> Maybe RouteParamList)+ , genFunc :: (RouteParamList -> Either UrlBuildError T.Text) }++data Route g s m = Route + { routeName :: (Maybe T.Text)+ , routeMethod :: MethodMatch+ , routeParser :: UrlParser+ , routeHandler :: (WhebHandlerT g s m) }++data ChunkType = IntChunk | TextChunk+ deriving (Show)++data UrlPat = Chunk T.Text+ | Composed [UrlPat]+ | FuncChunk + { chunkParamName :: T.Text+ , chunkFunc :: (T.Text -> Maybe ParsedChunk)+ , chunkType :: ChunkType }++instance Show UrlPat where+ show (Chunk a) = "(Chunk " ++ (T.unpack a) ++ ")"+ show (Composed a) = intercalate "/" $ fmap show a+ show (FuncChunk pn _ ct) = "(FuncChunk " ++ (T.unpack pn) ++ " | " ++ (show ct) ++ ")"++instance IsString UrlPat where+ fromString = Chunk . T.pack
+ src/Web/Wheb/Utils.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}++module Web.Wheb.Utils where++import Blaze.ByteString.Builder (Builder+ ,fromLazyByteString+ ,toLazyByteString)+import Control.Monad+import Data.Monoid+import Data.Conduit as C+import Data.Conduit.List (fold)+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.Encoding as T+import qualified Data.Text.Encoding as TS+import Network.HTTP.Types.Status+import Network.Wai++import Web.Wheb.Types++lazyTextToSBS = TS.encodeUtf8 . T.toStrict+sbsToLazyText = T.fromStrict . TS.decodeUtf8+builderToText = T.decodeUtf8 . toLazyByteString++-- | Show and pack into 'Text'+spack :: Show a => a -> T.Text+spack = T.pack . show++-- | See a 'HandlerResponse's as 'Text'+showResponseBody :: HandlerResponse -> IO T.Text+showResponseBody (HandlerResponse s r) = + liftM (T.decodeUtf8 . toLazyByteString) builderBody+ where chunkFlatAppend m (C.Chunk more) = m `mappend` more+ chunkFlatAppend m _ = m+ builderBody = body' (C.$$ fold chunkFlatAppend mempty)+ (_, _, body') = responseToSource $ toResponse s [] r++----------------------- Instances ------------------------+instance WhebContent Builder where+ toResponse = responseBuilder++instance WhebContent T.Text where+ toResponse s hds = responseBuilder s hds . fromLazyByteString . T.encodeUtf8++instance WhebContent WhebFile where+ toResponse s hds (WhebFile fp) = responseFile s hds (show fp) Nothing++----------------------- Some defaults -----------------------+defaultErr :: Monad m => WhebError -> WhebHandlerT g s m+defaultErr err = return $ HandlerResponse status500 $ + ("<h1>Error: " <> (T.pack $ show err) <> ".</h1>")++uhOh :: Response+uhOh = responseLBS status500 [("Content-Type", "text/html")]+ "Something went wrong on the server."
+ src/Web/Wheb/WhebT.hs view
@@ -0,0 +1,302 @@+{-# LANGUAGE RecordWildCards #-}++module Web.Wheb.WhebT+ (+ -- * ReaderT and StateT Functionality+ -- ** ReaderT+ getApp+ , getWithApp+ -- ** StateT+ , getHandlerState+ , putHandlerState+ , modifyHandlerState+ , modifyHandlerState'+ + -- * Responses+ , setHeader+ , setRawHeader+ , html+ , text+ , file+ , builder+ + -- * Settings+ , getSetting+ , getSetting'+ , getSetting''+ , getSettings+ + -- * Routes+ , getRouteParams+ , getRouteParam+ , getRoute+ , getRoute'+ , getRawRoute+ + -- * Request reading+ , getRequest+ , getRequestHeader+ , getWithRequest+ , getQueryParams+ , getPOSTParam+ , getPOSTParams+ , getRawPOST+ + -- * Running Wheb+ , runWhebServer+ , runWhebServerT+ , debugHandler+ , debugHandlerT+ ) where++import Blaze.ByteString.Builder (Builder)+import Control.Concurrent+import Control.Concurrent.STM+import Control.Exception as E+import Control.Monad.Error+import Control.Monad.IO.Class+import Control.Monad.Reader+import Control.Monad.State++import qualified Data.ByteString.Lazy as LBS+import Data.CaseInsensitive (mk)+import qualified Data.Map as M+import Data.Maybe (fromMaybe)+import qualified Data.Text.Lazy as T+import Data.Typeable (Typeable, cast)+import Data.List (find)++import Network.HTTP.Types.Header+import Network.HTTP.Types.Status+import Network.HTTP.Types.URI+import Network.Wai+import Network.Wai.Handler.Warp as W+import Network.Wai.Parse++import System.Posix.Signals (installHandler, Handler(Catch), sigINT, sigTERM)++import Web.Wheb.Internal+import Web.Wheb.Routes+import Web.Wheb.Types+import Web.Wheb.Utils++-- * ReaderT and StateT Functionality++-- ** ReaderT++-- | Get the 'g' in @WhebT g s m g@. This is a read-only state so only+-- thread-safe resources such as DB connections should go in here.+getApp :: Monad m => WhebT g s m g+getApp = WhebT $ liftM globalCtx ask++getWithApp :: Monad m => (g -> a) -> WhebT g s m a+getWithApp = flip liftM getApp++-- ** StateT++-- | Get the 's' in @WhebT g s m g@. This is a read and writable state+-- so you can get and put information in your state. Each request gets its own+-- fresh state duplicated from our options 'startingState'+getHandlerState :: Monad m => WhebT g s m s+getHandlerState = WhebT $ liftM reqState get++putHandlerState :: Monad m => s -> WhebT g s m ()+putHandlerState s = WhebT $ modify (\is -> is {reqState = s})++modifyHandlerState :: Monad m => (s -> s) -> WhebT g s m s+modifyHandlerState f = do+ s <- liftM f getHandlerState+ putHandlerState s+ return s++modifyHandlerState' :: Monad m => (s -> s) -> WhebT g s m ()+modifyHandlerState' f = modifyHandlerState f >> (return ())++-- * Settings++-- | Help prevent monomorphism errors for simple settings.+getSetting :: Monad m => T.Text -> WhebT g s m (Maybe T.Text)+getSetting = getSetting'++-- | Open up underlying support for polymorphic global settings+getSetting' :: (Monad m, Typeable a) => T.Text -> WhebT g s m (Maybe a)+getSetting' k = liftM (\cs -> (M.lookup k cs) >>= unwrap) getSettings+ where unwrap :: Typeable a => SettingsValue -> Maybe a+ unwrap (MkVal a) = cast a++-- | Get a setting or a default+getSetting'' :: (Monad m, Typeable a) => T.Text -> a -> WhebT g s m a+getSetting'' k d = liftM (fromMaybe d) (getSetting' k)++-- | Get all settings.+getSettings :: Monad m => WhebT g s m CSettings+getSettings = WhebT $ liftM (runTimeSettings . globalSettings) ask++-- * Routes++-- | Get all route params.+getRouteParams :: Monad m => WhebT g s m RouteParamList+getRouteParams = WhebT $ liftM routeParams ask++-- | Cast a route param into its type.+getRouteParam :: (Typeable a, Monad m) => T.Text -> WhebT g s m a+getRouteParam t = do+ p <- getRouteParam' t+ maybe (throwError RouteParamDoesNotExist) return p++-- | Cast a route param into its type.+getRouteParam' :: (Typeable a, Monad m) => T.Text -> WhebT g s m (Maybe a)+getRouteParam' t = liftM (getParam t) getRouteParams++-- | Convert 'Either' from 'getRoute'' into an error in the Monad+getRoute :: Monad m => T.Text -> RouteParamList -> WhebT g s m T.Text+getRoute t l = do+ res <- getRoute' t l+ case res of+ Right t -> return t+ Left err -> throwError $ URLError t err++-- | Generate a route from a name and param list.+getRoute' :: Monad m => T.Text -> + RouteParamList -> + WhebT g s m (Either UrlBuildError T.Text)+getRoute' n l = liftM buildRoute (getRawRoute n l)+ where buildRoute (Just (Route {..})) = generateUrl routeParser l+ buildRoute (Nothing) = Left UrlNameNotFound++-- | Generate the raw route+getRawRoute :: Monad m => T.Text -> + RouteParamList -> + WhebT g s m (Maybe (Route g s m))+getRawRoute n l = WhebT $ liftM f ask + where findRoute (Route {..}) = fromMaybe False (fmap (==n) routeName) + f = ((find findRoute) . appRoutes . globalSettings) ++-- * Request reading++-- | Access the request+getRequest :: Monad m => WhebT g s m Request+getRequest = WhebT $ liftM request ask++getWithRequest :: Monad m => (Request -> a) -> WhebT g s m a+getWithRequest = flip liftM getRequest++-- | Get the raw parsed POST data including files.+getRawPOST :: MonadIO m => WhebT g s m ([Param], [File LBS.ByteString])+getRawPOST = WhebT $ liftM postData ask++-- | Get POST params as 'Text'+getPOSTParams :: MonadIO m => WhebT g s m [(T.Text, T.Text)]+getPOSTParams = liftM (fmap f . fst) getRawPOST+ where f (a, b) = (sbsToLazyText a, sbsToLazyText b)++-- | Maybe get one param if it exists.+getPOSTParam :: MonadIO m => T.Text -> WhebT g s m (Maybe T.Text)+getPOSTParam k = liftM (lookup k) getPOSTParams ++-- | Get params from URL (e.g. from '/foo/?q=4')+getQueryParams :: Monad m => WhebT g s m Query+getQueryParams = getWithRequest queryString++-- | Get a request header+getRequestHeader :: Monad m => T.Text -> WhebT g s m (Maybe T.Text)+getRequestHeader k = getRequest >>= f+ where hk = mk $ lazyTextToSBS k+ f = (return . (fmap sbsToLazyText) . (lookup hk) . requestHeaders)++-- * Responses++-- | Set a Strict ByteString header for the response+setRawHeader :: Monad m => Header -> WhebT g s m ()+setRawHeader (hn, hc) = WhebT $ modify insertHeader + where insertHeader is@(InternalState {..}) = + is { respHeaders = M.insert hn hc respHeaders }+ +-- | Set a header for the response+setHeader :: Monad m => T.Text -> T.Text -> WhebT g s m ()+setHeader hn hc = setRawHeader (mk $ lazyTextToSBS hn, lazyTextToSBS hc)++-- | Give filepath and content type to serve a file from disk.+file :: Monad m => T.Text -> T.Text -> WhebHandlerT g s m+file fp ct = do+ setHeader (T.pack "Content-Type") (ct) + return $ HandlerResponse status200 (WhebFile fp)++-- | Return simple HTML from Text+html :: Monad m => T.Text -> WhebHandlerT g s m+html c = do+ setHeader (T.pack "Content-Type") (T.pack "text/html") + return $ HandlerResponse status200 c++-- | Return simple Text +text :: Monad m => T.Text -> WhebHandlerT g s m+text c = do+ setHeader (T.pack "Content-Type") (T.pack "text/plain") + return $ HandlerResponse status200 c++-- | Give content type and Blaze Builder+builder :: Monad m => T.Text -> Builder -> WhebHandlerT g s m+builder c b = do+ setHeader (T.pack "Content-Type") c + return $ HandlerResponse status200 b+ +-- * Running a Wheb Application++-- | Running a Handler with a custom Transformer+debugHandlerT :: WhebOptions g s m ->+ (m (Either WhebError a) -> IO (Either WhebError a)) ->+ Request ->+ WhebT g s m a ->+ IO (Either WhebError a)+debugHandlerT opts@(WhebOptions {..}) runIO r h = + runIO $ runDebugHandler opts h baseData+ where baseData = HandlerData startingCtx r ([], []) [] opts++-- | Convenience wrapper for 'debugHandlerT' function in 'IO'+debugHandler :: WhebOptions g s IO -> + WhebT g s IO a ->+ IO (Either WhebError a)+debugHandler opts h = debugHandlerT opts id defaultRequest h++-- | Run a server with a function to run your inner Transformer to IO and +-- generated options+runWhebServerT :: (m EResponse -> IO EResponse) ->+ WhebOptions g s m ->+ IO ()+runWhebServerT runIO opts@(WhebOptions {..}) = do+ putStrLn $ "Now running on port " ++ (show $ port)++ installHandler sigINT catchSig Nothing+ installHandler sigTERM catchSig Nothing++ forkIO $ runSettings rtSettings $+ gracefulExit $+ waiStack $ + optsToApplication opts runIO++ loop+ waitForConnections+ putStrLn $ "Shutting down server..."+ sequence_ cleanupActions++ where catchSig = (Catch (atomically $ writeTVar shutdownTVar True))+ loop = do+ shutDown <- atomically $ readTVar shutdownTVar+ if shutDown then return () else (threadDelay 100000) >> loop+ gracefulExit app r = do+ isExit <- atomically $ readTVar shutdownTVar+ case isExit of+ False -> app r+ True -> return $ responseLBS serviceUnavailable503 [] LBS.empty+ waitForConnections = do+ openConnections <- atomically $ readTVar activeConnections+ if (openConnections > 0)+ then waitForConnections+ else return ()+ port = fromMaybe 3000 $ + (M.lookup (T.pack "port") runTimeSettings) >>= (\(MkVal m) -> cast m)+ rtSettings = warpSettings { W.settingsPort = port }++-- | Convenience wrapper for 'runWhebServerT' function in IO+runWhebServer :: (WhebOptions g s IO) -> IO ()+runWhebServer = runWhebServerT id
+ tests/Main.hs view
@@ -0,0 +1,10 @@+module Main where++import Test.QuickCheck+import TestRoutes (checkURL)+import System.Exit (exitFailure)++main :: IO ()+main = do+ putStrLn "Running route tests:"+ quickCheck checkURL