Replaced UrlRewriter with HandlerParser

This commit is contained in:
Michael Snoyman 2009-08-04 15:52:21 +03:00
parent 543b15d768
commit c4eb5e1ee7
8 changed files with 178 additions and 68 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
dist dist
*.swp *.swp
*.hi
*.o

View File

@ -49,6 +49,11 @@ clearly do not want to write code twice to process these requests. Instead,
convert the article name into a URL parameter and then articles will have its convert the article name into a URL parameter and then articles will have its
own ResourceName. own ResourceName.
### ResourceParser
A ResourceParser converts a Resource (ie, a URL) to a ResourceName and URL
parameters.
## RawRequest ## RawRequest
The parsed data sent from the client. Has, for example, GET and POST The parsed data sent from the client. Has, for example, GET and POST
@ -83,6 +88,15 @@ in different ways.
There is a single Handler for each combination of ResourceName and Verb. A There is a single Handler for each combination of ResourceName and Verb. A
Handler takes some instance of Request and returns a Response. Handler takes some instance of Request and returns a Response.
### HandlerMap
Maps a ResourceName/Verb pair to a Handler.
## Application
An application is essentially a ResourceParser and HandlerMap. It also has some
settings involved.
# Static files # Static files
All static files should go under the /static/ path. A typical application will All static files should go under the /static/ path. A typical application will
@ -94,3 +108,8 @@ Restful API.
Search engines nad older clients should not be ignored. However, it is quite Search engines nad older clients should not be ignored. However, it is quite
tedious to write view code twice. Hopefully, in the future there will be a view tedious to write view code twice. Hopefully, in the future there will be a view
component to this framework which can automate some of that process. component to this framework which can automate some of that process.
# Passing global data
You should use function currying to pass around global information (the list of
entries in a blog, a database connection, etc).

View File

@ -17,9 +17,11 @@ module Web.Restful
, module Web.Restful.Request , module Web.Restful.Request
, module Web.Restful.Response , module Web.Restful.Response
, module Web.Restful.Application , module Web.Restful.Application
, module Web.Restful.Definitions
) where ) where
import Data.Object import Data.Object
import Web.Restful.Request import Web.Restful.Request
import Web.Restful.Response import Web.Restful.Response
import Web.Restful.Application import Web.Restful.Application
import Web.Restful.Definitions

View File

@ -1,5 +1,6 @@
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables #-}
--------------------------------------------------------- ---------------------------------------------------------
-- --
-- Module : Web.Restful.Application -- Module : Web.Restful.Application
@ -22,10 +23,12 @@ module Web.Restful.Application
-- ** Settings -- ** Settings
, setHandler , setHandler
, setRpxnowApiKey , setRpxnowApiKey
, setUrlRewriter , setResourceParser
, setHtmlWrapper , setHtmlWrapper
-- ** Engage -- ** Engage
, run , run
-- * FIXME
, Application (..)
) where ) where
-- hideously long import list -- hideously long import list
@ -56,31 +59,33 @@ import Web.Restful.Request
import Web.Restful.Response import Web.Restful.Response
import Web.Restful.Constants import Web.Restful.Constants
import Web.Restful.Utils import Web.Restful.Utils
import Web.Restful.Handler
import Web.Restful.Definitions
import Data.Object import Data.Object
-- | Contains settings and a list of resources. -- | Contains settings and a list of resources.
type ApplicationMonad = StateT ApplicationSettings (Writer [Resource]) type ApplicationMonad a = StateT (ApplicationSettings a) (Writer (HandlerMap a))
instance Applicative ApplicationMonad where instance Applicative (ApplicationMonad a) where
pure = return pure = return
f <*> a = do f <*> a = do
f' <- f f' <- f
a' <- a a' <- a
return $! f' a' return $! f' a'
data ApplicationSettings = ApplicationSettings data ApplicationSettings rn = ApplicationSettings
{ hackHandler :: Hack.Application -> IO () { hackHandler :: Hack.Application -> IO ()
, rpxnowApiKey :: Maybe String , rpxnowApiKey :: Maybe String
, encryptKey :: Either FilePath Word256 , encryptKey :: Either FilePath Word256
, urlRewriter :: UrlRewriter , appResourceParser :: ResourceParser rn
, hackMiddleware :: [Hack.Middleware] , hackMiddleware :: [Hack.Middleware]
, response404 :: Hack.Env -> IO Hack.Response , response404 :: Hack.Env -> IO Hack.Response
, htmlWrapper :: BS.ByteString -> BS.ByteString , htmlWrapper :: BS.ByteString -> BS.ByteString
} }
instance Default ApplicationSettings where instance ResourceName a => Default (ApplicationSettings a) where
def = ApplicationSettings def = ApplicationSettings
{ hackHandler = Hack.Handler.CGI.run { hackHandler = Hack.Handler.CGI.run
, rpxnowApiKey = Nothing , rpxnowApiKey = Nothing
, encryptKey = Left defaultKeyFile , encryptKey = Left defaultKeyFile
, urlRewriter = \s -> (s, []) , appResourceParser = \s -> ParsedResource (toResourceName s) []
, hackMiddleware = , hackMiddleware =
[ gzip [ gzip
, cleanPath , cleanPath
@ -98,44 +103,32 @@ default404 env = return $
[("Content-Type", "text/plain")] [("Content-Type", "text/plain")]
$ toLazyByteString $ "Not found: " ++ Hack.pathInfo env $ toLazyByteString $ "Not found: " ++ Hack.pathInfo env
data Handler = forall req res. (Request req, Response res)
=> Handler (req -> IO res)
type LiftedHandler = RawRequest -> IO ResponseWrapper
liftHandler ::
Handler
-> RawRequest
-> IO ResponseWrapper
liftHandler (Handler h) rr = do
case runRequestParser parseRequest rr of
Left errors -> return $ ResponseWrapper
$ ErrorResponse
$ unlines errors
Right req -> ResponseWrapper `fmap` h req
data Resource = Resource [Hack.RequestMethod] PathInfo LiftedHandler
-- FIXME document below here -- FIXME document below here
addResource :: (Request req, Response res) addResource :: (Request req, Response res, ResourceName rn)
=> [Hack.RequestMethod] => Verb
-> PathInfo -> rn
-> (req -> IO res) -> (req -> IO res)
-> ApplicationMonad () -> ApplicationMonad rn ()
addResource methods path f = addResource verb resourceName' f = do
tell [Resource methods path $ liftHandler $ Handler f] let handler :: Handler
handler = Handler $ (fmap ResponseWrapper) . f
handlerDesc = HandlerDesc resourceName' verb handler
tell [handlerDesc]
setUrlRewriter :: UrlRewriter -> ApplicationMonad () setResourceParser :: ResourceName rn
setUrlRewriter newUrlRewriter = do => ResourceParser rn
-> ApplicationMonad rn ()
setResourceParser newRP = do
s <- get s <- get
put $ s { urlRewriter = newUrlRewriter } put $ s { appResourceParser = newRP }
setHtmlWrapper :: (BS.ByteString -> BS.ByteString) -> ApplicationMonad () setHtmlWrapper :: (BS.ByteString -> BS.ByteString) -> ApplicationMonad a ()
setHtmlWrapper f = do setHtmlWrapper f = do
s <- get s <- get
put $ s { htmlWrapper = f } put $ s { htmlWrapper = f }
run :: ApplicationMonad () -> IO () run :: ResourceName a => ApplicationMonad a () -> IO ()
run m = do run m = do
let (settings, resources') = runWriter $ execStateT m def let (settings, resources') = runWriter $ execStateT m def
key <- case encryptKey settings of key <- case encryptKey settings of
@ -143,36 +136,38 @@ run m = do
Right k -> return k Right k -> return k
let defApp = defaultResources settings let defApp = defaultResources settings
defResources = execWriter $ execStateT defApp def defResources = execWriter $ execStateT defApp def
resources = resources' ++ defResources resources = resources' ++ defResources -- FIXME rename HandlerDescs
app' :: Hack.Application app' :: Hack.Application
app' = makeApplication' resources settings app' = toHackApplication $ Application resources settings
clientsession' :: Hack.Middleware clientsession' :: Hack.Middleware
clientsession' = clientsession [authCookieName] key clientsession' = clientsession [authCookieName] key
app :: Hack.Application app :: Hack.Application
app = foldr ($) app' $ hackMiddleware settings ++ [clientsession'] app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
hackHandler settings app hackHandler settings app
setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad () setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad a ()
setHandler h = do setHandler h = do
settings <- get settings <- get
put $ settings { hackHandler = h } put $ settings { hackHandler = h }
setRpxnowApiKey :: String -> ApplicationMonad () setRpxnowApiKey :: String -> ApplicationMonad a ()
setRpxnowApiKey k = do setRpxnowApiKey k = do
settings <- get settings <- get
put $ settings { rpxnowApiKey = Just k } put $ settings { rpxnowApiKey = Just k }
defaultResources :: ApplicationSettings -> ApplicationMonad () defaultResources :: ResourceName rn
=> ApplicationSettings rn
-> ApplicationMonad rn ()
defaultResources settings = do defaultResources settings = do
addResource [Hack.GET] ["auth", "check"] authCheck addResource Get (toResourceName ["auth", "check"]) authCheck
addResource [Hack.GET] ["auth", "logout"] authLogout addResource Get (toResourceName ["auth", "logout"]) authLogout
addResource [Hack.GET] ["auth", "openid"] authOpenidForm addResource Get (toResourceName ["auth", "openid"]) authOpenidForm
addResource [Hack.GET] ["auth", "openid", "forward"] authOpenidForward addResource Get (toResourceName ["auth", "openid", "forward"]) authOpenidForward
addResource [Hack.GET] ["auth", "openid", "complete"] authOpenidComplete addResource Get (toResourceName ["auth", "openid", "complete"]) authOpenidComplete
case rpxnowApiKey settings of case rpxnowApiKey settings of
Nothing -> return () Nothing -> return ()
Just key -> do Just key -> do
addResource [Hack.GET] ["auth", "login", "rpxnow"] $ addResource Get (toResourceName ["auth", "login", "rpxnow"]) $
rpxnowLogin key rpxnowLogin key
data OIDFormReq = OIDFormReq (Maybe String) (Maybe String) data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
@ -314,22 +309,24 @@ instance Response LogoutResponse where
Hack.Response s (h':h) c Hack.Response s (h':h) c
h' = resetCookie authCookieName h' = resetCookie authCookieName
makeApplication' :: [Resource] toHackApplication :: Eq resourceName
-> ApplicationSettings => Application resourceName
-> Hack.Env -> Hack.Application
-> IO Hack.Response toHackApplication (Application hm settings) env = do
makeApplication' resources settings env = do let (Right resource) = splitPath $ Hack.pathInfo env
let method = Hack.requestMethod env (ParsedResource rn urlParams') = (appResourceParser settings) resource
rr = envToRawRequest (urlRewriter settings) env verb :: Verb
path' = rawPathInfo rr verb = toVerb $ Hack.requestMethod env
isValid :: Resource -> Bool rr :: RawRequest
isValid (Resource methods path _) = method `elem` methods rr = envToRawRequest urlParams' env
&& path == path' matchingHandler (HandlerDesc resourceName' verb' _) =
case filter isValid resources of rn == resourceName' &&
[Resource _ _ handler] -> do verb == verb'
case filter matchingHandler hm of
[HandlerDesc _ _ handler] -> do
let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
ctypes' = parseHttpAccept rawHttpAccept ctypes' = parseHttpAccept rawHttpAccept
body <- handler rr body <- runHandler handler rr
let reps' = reps body let reps' = reps body
ctypes = filter (\c -> isJust $ lookup c reps') ctypes' ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
let handlerPair = let handlerPair =
@ -351,13 +348,11 @@ makeApplication' resources settings env = do
(("Content-Type", ctype) : headers) (("Content-Type", ctype) : headers)
$ toLazyByteString $ wrapper content $ toLazyByteString $ wrapper content
[] -> response404 settings $ env [] -> response404 settings $ env
_ -> fail "Overlapping handlers" _ -> fail $ "Overlapping handlers for: " ++ show env
type UrlRewriter = PathInfo -> (PathInfo, [(String, String)]) envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
envToRawRequest :: UrlRewriter -> Hack.Env -> RawRequest envToRawRequest urlParams' env =
envToRawRequest rewriter env =
let (Right rawPieces) = splitPath $ Hack.pathInfo env let (Right rawPieces) = splitPath $ Hack.pathInfo env
(pi', urls) = rewriter rawPieces
gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)] gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)]
clength = tryLookup "0" "Content-Length" $ Hack.http env clength = tryLookup "0" "Content-Length" $ Hack.http env
ctype = tryLookup "" "Content-Type" $ Hack.http env ctype = tryLookup "" "Content-Type" $ Hack.http env
@ -365,4 +360,9 @@ envToRawRequest rewriter env =
$ Hack.hackInput env $ Hack.hackInput env
rawCookie = tryLookup "" "Cookie" $ Hack.http env rawCookie = tryLookup "" "Cookie" $ Hack.http env
cookies' = decodeCookies rawCookie :: [(String, String)] cookies' = decodeCookies rawCookie :: [(String, String)]
in RawRequest pi' urls gets' posts cookies' files env in RawRequest rawPieces urlParams' gets' posts cookies' files env
data Application a = Application
{ handlerMap :: HandlerMap a
, applicationSettings :: ApplicationSettings a
}

View File

@ -11,7 +11,9 @@
-- Constants used throughout Restful. -- Constants used throughout Restful.
-- --
--------------------------------------------------------- ---------------------------------------------------------
module Web.Restful.Constants where module Web.Restful.Constants
( authCookieName
) where
authCookieName :: String authCookieName :: String
authCookieName = "IDENTIFIER" authCookieName = "IDENTIFIER"

View File

@ -0,0 +1,47 @@
{-# LANGUAGE FlexibleInstances #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Definitions
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : Stable
-- Portability : portable
--
-- Definitions throughout Restful.
--
---------------------------------------------------------
module Web.Restful.Definitions
( Verb (..)
, toVerb
, Resource
, ParsedResource (..)
, ResourceParser
, ResourceName (..)
) where
import qualified Hack
data Verb = Get | Put | Delete | Post
deriving (Eq, Show)
toVerb :: Hack.RequestMethod -> Verb
toVerb Hack.PUT = Put
toVerb Hack.DELETE = Delete
toVerb Hack.POST = Post
toVerb _ = Get
type Resource = [String]
class Eq a => ResourceName a where
toResourceName :: [String] -> a
instance ResourceName [String] where
toResourceName = id
data ParsedResource a = ParsedResource
{ resourceName :: a
, urlParameters :: [(String, String)]
}
type ResourceParser a = Resource -> ParsedResource a

36
Web/Restful/Handler.hs Normal file
View File

@ -0,0 +1,36 @@
{-# LANGUAGE ExistentialQuantification #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Handler
-- Copyright : Michael Snoyman
-- License : BSD3
--
-- Maintainer : Michael Snoyman <michael@snoyman.com>
-- Stability : unstable
-- Portability : portable
--
-- Define Handler stuff.
--
---------------------------------------------------------
module Web.Restful.Handler
( Handler (..)
, runHandler
, HandlerMap
, HandlerDesc (..)
) where
import Web.Restful.Definitions
import Web.Restful.Request
import Web.Restful.Response
data Handler = forall req. Request req => Handler (req -> IO ResponseWrapper)
runHandler :: Handler -> RawRequest -> IO ResponseWrapper
runHandler (Handler f) rreq = do
let rparser = parseRequest
case runRequestParser rparser rreq of
Left errors -> fail $ unlines errors -- FIXME
Right req -> f req
data HandlerDesc a = HandlerDesc a Verb Handler
type HandlerMap a = [HandlerDesc a]

View File

@ -1,5 +1,5 @@
name: restful name: restful
version: 0.0.0 version: 0.1.0
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -35,5 +35,7 @@ library
Web.Restful.Request, Web.Restful.Request,
Web.Restful.Response, Web.Restful.Response,
Web.Restful.Utils, Web.Restful.Utils,
Web.Restful.Definitions,
Web.Restful.Handler,
Web.Restful.Application Web.Restful.Application
ghc-options: -Wall ghc-options: -Wall