From a02230ab74a5bd57c3cfe8810c50ececb0d4fad5 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Thu, 25 Jun 2009 20:45:43 +0300
Subject: [PATCH 001/624] initial commit
---
README | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 README
diff --git a/README b/README
new file mode 100644
index 00000000..e69de29b
From 4ad1b2956e0846c4b76839b5279d21bf2d97f48a Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Sun, 28 Jun 2009 01:55:14 +0300
Subject: [PATCH 002/624] Imported existing code, documentation incomplete
---
.gitignore | 2 +
LICENSE | 25 ++
README | 1 +
Setup.lhs | 7 +
Web/Restful.hs | 888 +++++++++++++++++++++++++++++++++++++++++++++++++
restful.cabal | 17 +
6 files changed, 940 insertions(+)
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100755 Setup.lhs
create mode 100644 Web/Restful.hs
create mode 100644 restful.cabal
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..39b806f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+dist
+*.swp
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..11dc17a1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+The following license covers this documentation, and the source code, except
+where otherwise indicated.
+
+Copyright 2008, Michael Snoyman. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README b/README
index e69de29b..4ccea4d9 100644
--- a/README
+++ b/README
@@ -0,0 +1 @@
+A Restful front controller built on Hack.
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100755
index 00000000..06e2708f
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,7 @@
+#!/usr/bin/env runhaskell
+
+> module Main where
+> import Distribution.Simple
+
+> main :: IO ()
+> main = defaultMain
diff --git a/Web/Restful.hs b/Web/Restful.hs
new file mode 100644
index 00000000..3e26bad6
--- /dev/null
+++ b/Web/Restful.hs
@@ -0,0 +1,888 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE OverlappingInstances #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Lightweight framework for designing RESTful APIs.
+--
+---------------------------------------------------------
+module Web.Restful
+ (
+ -- * Request parsing
+ -- $param_overview
+
+ -- ** Types
+ ParamError
+ , ParamName
+ , ParamValue
+ -- ** Parameter type class
+ , Parameter (..)
+ -- ** RequestParser helpers
+ , getParam
+ , postParam
+ , urlParam
+ , anyParam
+ , cookieParam
+ , identifier
+ , acceptedLanguages
+ , requestPath
+ -- ** Building actual request
+ , Request (..)
+ , Hack.RequestMethod (..)
+ , rawFiles
+ -- * Response construction
+ , Response (..)
+ , response
+ -- ** Helper 'Response' instances
+ -- *** Generic hierarchichal text
+ , Tree (..)
+ , IsTree (..)
+ -- *** Atom news feed
+ , AtomFeed (..)
+ , AtomFeedEntry (..)
+ -- *** Sitemap
+ , sitemap
+ , SitemapUrl (..)
+ , SitemapLoc (..)
+ , SitemapChangeFreq (..)
+ -- *** Generics
+ -- **** List/detail
+ , ListDetail (..)
+ , ItemList (..)
+ , ItemDetail (..)
+ , -- **** Multiple response types.
+ GenResponse (..)
+ -- * Defining an application
+ , ApplicationMonad
+ -- ** Routing
+ , addResource
+ -- ** Settings
+ , setHandler
+ , setRpxnowApiKey
+ , setUrlRewriter
+ , setHtmlWrapper
+ -- ** Engage
+ , run
+ ) where
+
+-- hideously long import list
+import qualified Hack
+import qualified Hack.Handler.CGI
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Control.Monad.State hiding (gets)
+import Data.List (intercalate)
+import Web.Encodings
+import Data.Maybe (isJust)
+import Data.ByteString.Class
+import qualified Data.ByteString.Lazy as BS
+import Data.Function.Predicate (equals)
+import Data.Default
+import qualified Web.Authenticate.Rpxnow as Rpxnow
+import qualified Web.Authenticate.OpenId as OpenId
+import Data.List.Split (splitOneOf)
+
+import Hack.Middleware.Gzip
+import Hack.Middleware.CleanPath
+import Hack.Middleware.Jsonp
+import Hack.Middleware.ClientSession
+
+import Data.Time.Format
+import Data.Time.Clock
+import System.Locale
+import Control.Applicative ((<$>), Applicative (..))
+import Control.Arrow (second)
+
+-- $param_overview
+-- In Restful, all of the underlying parameter values are strings. They can
+-- come from multiple sources: GET parameters, URL rewriting (FIXME: link),
+-- cookies, etc. However, most applications eventually want to convert
+-- those strings into something else, like 'Int's. Additionally, it is
+-- often desirable to allow multiple values, or no value at all.
+--
+-- That is what the parameter concept is for. A 'Parameter' is any value
+-- which can be converted from a 'String', or list of 'String's.
+
+-- | Any kind of error message generated in the parsing stage.
+type ParamError = String
+
+-- | In GET parameters, the key. In cookies, the cookie name. So on and so
+-- forth.
+type ParamName = String
+
+-- | The 'String' value of a parameter, such as cookie content.
+type ParamValue = String
+
+-- | Anything which can be converted from a 'String' or list of 'String's.
+--
+-- The default implementation of 'readParams' will error out if given
+-- anything but 1 'ParamValue'. This is usually what you want.
+--
+-- Minimal complete definition: either 'readParam' or 'readParams'.
+class Parameter a where
+ -- | Convert a string into the desired value, or explain why that can't
+ -- happen.
+ readParam :: ParamValue -> Either ParamError a
+ readParam = readParams . return
+
+ -- | Convert a list of strings into the desired value, or explain why
+ -- that can't happen.
+ readParams :: [ParamValue] -> Either ParamError a
+ readParams [x] = readParam x
+ readParams [] = Left "Missing parameter"
+ readParams xs = Left $ "Given " ++ show (length xs) ++
+ " values, expecting 1"
+
+-- | Attempt to parse a list of param values using 'readParams'.
+-- If that fails, return an error message and an undefined value. This way,
+-- we can process all of the parameters and get all of the error messages.
+-- Be careful not to use the value inside until you can be certain the
+-- reading succeeded.
+tryReadParams:: Parameter a
+ => ParamName
+ -> [ParamValue]
+ -> RequestParser a
+tryReadParams name params =
+ case readParams params of
+ Left s -> do
+ tell [name ++ ": " ++ s]
+ return $
+ error $
+ "Trying to evaluate nonpresent parameter " ++
+ name
+ Right x -> return x
+
+-- | Helper function for generating 'RequestParser's from various
+-- 'ParamValue' lists.
+genParam :: Parameter a
+ => (RawRequest -> ParamName -> [ParamValue])
+ -> ParamName
+ -> RequestParser a
+genParam f name = do
+ req <- ask
+ tryReadParams name $ f req name
+
+-- | Parse a value passed as a GET parameter.
+getParam :: Parameter a => ParamName -> RequestParser a
+getParam = genParam getParams
+
+-- | Parse a value passed as a POST parameter.
+postParam :: Parameter a => ParamName -> RequestParser a
+postParam = genParam postParams
+
+-- | Parse a value passed in the URL and extracted using rewrite.
+-- (FIXME: link to rewrite section.)
+urlParam :: Parameter a => ParamName -> RequestParser a
+urlParam = genParam urlParams
+
+-- | Parse a value passed as a GET, POST or URL parameter.
+anyParam :: Parameter a => ParamName -> RequestParser a
+anyParam = genParam anyParams
+
+-- | Parse a value passed as a raw cookie.
+cookieParam :: Parameter a => ParamName -> RequestParser a
+cookieParam = genParam cookies
+
+-- | Parse a value in the hackHeader field.
+hackHeaderParam :: Parameter a => ParamName -> RequestParser a
+hackHeaderParam name = do
+ env <- parseEnv
+ let vals' = lookup name $ Hack.hackHeaders env
+ vals = case vals' of
+ Nothing -> []
+ Just x -> [x]
+ tryReadParams name vals
+
+-- | Extract the cookie which specifies the identifier for a logged in
+-- user.
+identifier :: Parameter a => RequestParser a
+identifier = hackHeaderParam authCookieName
+
+-- | Get the raw 'Hack.Env' value.
+parseEnv :: RequestParser Hack.Env
+parseEnv = rawEnv `fmap` ask
+
+-- | Determine the ordered list of language preferences.
+--
+-- FIXME: Future versions should account for some cookie.
+acceptedLanguages :: RequestParser [String]
+acceptedLanguages = do
+ env <- parseEnv
+ let rawLang = tryLookup "" "Accept-Language" $ Hack.http env
+ return $! parseHttpAccept rawLang
+
+-- | Determinge the path requested by the user (ie, the path info).
+requestPath :: RequestParser String
+requestPath = do
+ env <- parseEnv
+ let q = case Hack.queryString env of
+ "" -> ""
+ q'@('?':_) -> q'
+ q' -> q'
+ return $! Hack.pathInfo env ++ q
+
+type RequestParser a = WriterT [ParamError] (Reader RawRequest) a
+instance Applicative (WriterT [ParamError] (Reader RawRequest)) where
+ pure = return
+ f <*> a = do
+ f' <- f
+ a' <- a
+ return $! f' a'
+
+-- | Parse a request into either the desired 'Request' or a list of errors.
+runRequestParser :: RequestParser a -> RawRequest -> Either [ParamError] a
+runRequestParser p req =
+ let (val, errors) = (runReader (runWriterT p)) req
+ in case errors of
+ [] -> Right val
+ x -> Left x
+
+-- | The raw information passed through Hack, cleaned up a bit.
+data RawRequest = RawRequest
+ { rawPathInfo :: PathInfo
+ , rawUrlParams :: [(ParamName, ParamValue)]
+ , rawGetParams :: [(ParamName, ParamValue)]
+ , rawPostParams :: [(ParamName, ParamValue)]
+ , rawCookies :: [(ParamName, ParamValue)]
+ , rawFiles :: [(ParamName, FileInfo)]
+ , rawEnv :: Hack.Env
+ }
+
+-- | All GET paramater values with the given name.
+getParams :: RawRequest -> ParamName -> [ParamValue]
+getParams rr name = map snd
+ . filter (\x -> name == fst x)
+ . rawGetParams
+ $ rr
+
+-- | All POST paramater values with the given name.
+postParams :: RawRequest -> ParamName -> [ParamValue]
+postParams rr name = map snd
+ . filter (\x -> name == fst x)
+ . rawPostParams
+ $ rr
+
+-- | All URL paramater values (see rewriting) with the given name.
+urlParams :: RawRequest -> ParamName -> [ParamValue]
+urlParams rr name = map snd
+ . filter (\x -> name == fst x)
+ . rawUrlParams
+ $ rr
+
+-- | All GET, POST and URL paramater values (see rewriting) with the given name.
+anyParams :: RawRequest -> ParamName -> [ParamValue]
+anyParams req name = urlParams req name ++
+ getParams req name ++
+ postParams req name
+
+-- | All cookies with the given name.
+cookies :: RawRequest -> ParamName -> [ParamValue]
+cookies rr name = map snd . filter (fst `equals` name) . rawCookies $ rr
+
+instance Parameter a => Parameter (Maybe a) where
+ readParams [] = Right Nothing
+ readParams [x] = readParam x >>= return . Just
+ readParams xs = Left $ "Given " ++ show (length xs) ++
+ " values, expecting 0 or 1"
+
+instance Parameter a => Parameter [a] where
+ readParams = mapM readParam
+
+instance Parameter String where
+ readParam = Right
+
+instance Parameter Int where
+ readParam s = case reads s of
+ ((x, _):_) -> Right x
+ _ -> Left $ "Invalid integer: " ++ s
+
+-- | The input for a resource.
+--
+-- Each resource can define its own instance of 'Request' and then more
+-- easily ensure that it received the correct input (ie, correct variables,
+-- properly typed).
+class Request a where
+ parseRequest :: RequestParser a
+
+instance Request () where
+ parseRequest = return ()
+
+type ContentType = String
+
+-- | The output for a resource.
+class Response a where
+ -- | Provide an ordered list of possible responses, depending on content
+ -- type. If the user asked for a specific response type (like
+ -- text/html), then that will get priority. If not, then the first
+ -- element in this list will be used.
+ reps :: a -> [(ContentType, Hack.Response)]
+
+-- | Wrapper around 'Hack.Response' to allow arbitrary pieces of data to be
+-- used for the body.
+response :: LazyByteString lbs
+ => Int
+ -> [(String, String)]
+ -> lbs
+ -> Hack.Response
+response a b c = Hack.Response a b $ toLazyByteString c
+
+instance Response () where
+ reps _ = [("text/plain", response 200 [] "")]
+
+newtype ErrorResponse = ErrorResponse String
+instance Response ErrorResponse where
+ reps (ErrorResponse s) = [("text/plain", response 500 [] s)]
+
+data ResponseWrapper = forall res. Response res => ResponseWrapper res
+instance Response ResponseWrapper where
+ reps (ResponseWrapper res) = reps res
+
+-- | Contains settings and a list of resources.
+type ApplicationMonad = StateT ApplicationSettings (Writer [Resource])
+instance Applicative ApplicationMonad where
+ pure = return
+ f <*> a = do
+ f' <- f
+ a' <- a
+ return $! f' a'
+data ApplicationSettings = ApplicationSettings
+ { hackHandler :: Hack.Application -> IO ()
+ , rpxnowApiKey :: Maybe String
+ , encryptKey :: Either FilePath Word256
+ , urlRewriter :: UrlRewriter
+ , hackMiddleware :: [Hack.Middleware]
+ , response404 :: Hack.Env -> IO Hack.Response
+ , htmlWrapper :: BS.ByteString -> BS.ByteString
+ }
+instance Default ApplicationSettings where
+ def = ApplicationSettings
+ { hackHandler = Hack.Handler.CGI.run
+ , rpxnowApiKey = Nothing
+ , encryptKey = Left defaultKeyFile
+ , urlRewriter = \s -> (s, [])
+ , hackMiddleware = [gzip, cleanPath, jsonp]
+ , response404 = default404
+ , htmlWrapper = id
+ }
+
+default404 :: Hack.Env -> IO Hack.Response
+default404 env = return $
+ Hack.Response
+ 404
+ [("Content-Type", "text/plain")]
+ $ 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
+
+type PathInfo = [String]
+data Resource = Resource [Hack.RequestMethod] PathInfo LiftedHandler
+
+-- FIXME document below here
+
+addResource :: (Request req, Response res)
+ => [Hack.RequestMethod]
+ -> PathInfo
+ -> (req -> IO res)
+ -> ApplicationMonad ()
+addResource methods path f =
+ tell [Resource methods path $ liftHandler $ Handler f]
+
+setUrlRewriter :: UrlRewriter -> ApplicationMonad ()
+setUrlRewriter newUrlRewriter = do
+ s <- get
+ put $ s { urlRewriter = newUrlRewriter }
+
+setHtmlWrapper :: (BS.ByteString -> BS.ByteString) -> ApplicationMonad ()
+setHtmlWrapper f = do
+ s <- get
+ put $ s { htmlWrapper = f }
+
+run :: ApplicationMonad () -> IO ()
+run m = do
+ let (settings, resources') = runWriter $ execStateT m def
+ key <- case encryptKey settings of
+ Left f -> getKey f
+ Right k -> return k
+ let defApp = defaultResources settings
+ defResources = execWriter $ execStateT defApp def
+ resources = resources' ++ defResources
+ app' :: Hack.Application
+ app' = makeApplication' resources settings
+ clientsession' :: Hack.Middleware
+ clientsession' = clientsession [authCookieName] key
+ app :: Hack.Application
+ app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
+ hackHandler settings app
+
+setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad ()
+setHandler h = do
+ settings <- get
+ put $ settings { hackHandler = h }
+
+setRpxnowApiKey :: String -> ApplicationMonad ()
+setRpxnowApiKey k = do
+ settings <- get
+ put $ settings { rpxnowApiKey = Just k }
+
+defaultResources :: ApplicationSettings -> ApplicationMonad ()
+defaultResources settings = do
+ addResource [Hack.GET] ["auth", "check"] authCheck
+ addResource [Hack.GET] ["auth", "logout"] authLogout
+ addResource [Hack.GET] ["auth", "openid"] authOpenidForm
+ addResource [Hack.GET] ["auth", "openid", "forward"] authOpenidForward
+ addResource [Hack.GET] ["auth", "openid", "complete"] authOpenidComplete
+ case rpxnowApiKey settings of
+ Nothing -> return ()
+ Just key -> do
+ addResource [Hack.GET] ["auth", "login", "rpxnow"] $
+ rpxnowLogin key
+
+data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
+instance Request OIDFormReq where
+ parseRequest = OIDFormReq <$> getParam "message" <*> getParam "dest"
+instance Show OIDFormReq where
+ show (OIDFormReq Nothing _) = ""
+ show (OIDFormReq (Just s) _) = "" ++ encodeHtml s ++
+ "
"
+data OIDFormRes = OIDFormRes String (Maybe String)
+instance Response OIDFormRes where
+ reps (OIDFormRes s dest) = [("text/html", response 200 heads s)]
+ where
+ heads =
+ case dest of
+ Nothing -> []
+ Just dest' ->
+ [("Set-Cookie", "DEST=" ++ dest' ++ "; path=/")]
+authOpenidForm :: OIDFormReq -> IO OIDFormRes
+authOpenidForm m@(OIDFormReq _ dest) =
+ let html =
+ show m ++
+ ""
+ in return $! OIDFormRes html dest
+data OIDFReq = OIDFReq String String
+instance Request OIDFReq where
+ parseRequest = do
+ oid <- getParam "openid"
+ env <- parseEnv
+ let complete = "http://" ++ Hack.serverName env ++ ":" ++
+ show (Hack.serverPort env) ++
+ "/auth/openid/complete/"
+ return $! OIDFReq oid complete
+authOpenidForward :: OIDFReq -> IO GenResponse
+authOpenidForward (OIDFReq oid complete) = do
+ res <- OpenId.getForwardUrl oid complete :: IO (Either String String)
+ return $
+ case res of
+ Left err -> RedirectResponse $ "/auth/openid/?message=" ++
+ encodeUrl err
+ Right url -> RedirectResponse url
+
+data OIDComp = OIDComp [(String, String)] (Maybe String)
+instance Request OIDComp where
+ parseRequest = do
+ rr <- ask
+ let gets = rawGetParams rr
+ dest <- cookieParam "DEST"
+ return $! OIDComp gets dest
+data OIDCompRes = OIDCompResErr String
+ | OIDCompResGood String (Maybe String)
+instance Response OIDCompRes where
+ reps (OIDCompResErr err) =
+ reps $ RedirectResponse
+ $ "/auth/openid/?message=" ++
+ encodeUrl err
+ reps (OIDCompResGood ident Nothing) =
+ reps $ OIDCompResGood ident (Just "/")
+ reps (OIDCompResGood ident (Just dest)) =
+ [("text/plain", response 303 heads "")] where
+ heads =
+ [ (authCookieName, ident)
+ , resetCookie "DEST"
+ , ("Location", dest)
+ ]
+
+resetCookie :: String -> (String, String)
+resetCookie name =
+ ("Set-Cookie",
+ name ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
+
+authOpenidComplete :: OIDComp -> IO OIDCompRes
+authOpenidComplete (OIDComp gets' dest) = do
+ res <- OpenId.authenticate gets' :: IO (Either String OpenId.Identifier)
+ return $
+ case res of
+ Left err -> OIDCompResErr err
+ Right (OpenId.Identifier ident) -> OIDCompResGood ident dest
+
+-- | token dest
+data RpxnowRequest = RpxnowRequest String (Maybe String)
+instance Request RpxnowRequest where
+ parseRequest = do
+ token <- getParam "token"
+ dest <- getParam "dest"
+ return $! RpxnowRequest token $ chopHash `fmap` dest
+
+chopHash :: String -> String
+chopHash ('#':rest) = rest
+chopHash x = x
+
+-- | dest identifier
+data RpxnowResponse = RpxnowResponse String (Maybe String)
+instance Response RpxnowResponse where
+ reps (RpxnowResponse dest Nothing) =
+ [("text/html", response 303 [("Location", dest)] "")]
+ reps (RpxnowResponse dest (Just ident)) =
+ [("text/html", response 303
+ [ ("Location", dest)
+ , (authCookieName, ident)
+ ]
+ "")]
+
+rpxnowLogin :: String -- ^ api key
+ -> RpxnowRequest
+ -> IO RpxnowResponse
+rpxnowLogin apiKey (RpxnowRequest token dest') = do
+ let dest = case dest' of
+ Nothing -> "/"
+ Just "" -> "/"
+ Just s -> s
+ ident' <- Rpxnow.authenticate apiKey token
+ return $ RpxnowResponse dest (Rpxnow.identifier `fmap` ident')
+
+authCookieName :: String
+authCookieName = "IDENTIFIER"
+
+data AuthRequest = AuthRequest (Maybe String)
+instance Request AuthRequest where
+ parseRequest = AuthRequest `fmap` identifier
+
+authCheck :: AuthRequest -> IO Tree
+authCheck (AuthRequest Nothing) =
+ return $ TreeMap [("status", TreeScalar "notloggedin")]
+authCheck (AuthRequest (Just i)) =
+ return $ TreeMap $
+ [ ("status", TreeScalar "loggedin")
+ , ("ident", TreeScalar i)
+ ]
+
+authLogout :: () -> IO LogoutResponse
+authLogout _ = return LogoutResponse
+
+data LogoutResponse = LogoutResponse
+instance Response LogoutResponse where
+ reps _ = map (second addCookie) $ reps tree where
+ tree = TreeMap [("status", TreeScalar "loggedout")]
+ addCookie (Hack.Response s h c) =
+ Hack.Response s (h':h) c
+ h' = resetCookie authCookieName
+
+makeApplication' :: [Resource]
+ -> ApplicationSettings
+ -> Hack.Env
+ -> IO Hack.Response
+makeApplication' resources settings env = do
+ let method = Hack.requestMethod env
+ rr = envToRawRequest (urlRewriter settings) env
+ path' = rawPathInfo rr
+ isValid :: Resource -> Bool
+ isValid (Resource methods path _) = method `elem` methods
+ && path == path'
+ case filter isValid resources of
+ [Resource _ _ handler] -> do
+ let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
+ ctypes' = parseHttpAccept rawHttpAccept
+ body <- handler rr
+ let reps' = reps body
+ ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
+ let handlerPair =
+ case ctypes of
+ [] -> Just $ head reps'
+ (c:_) ->
+ case filter (fst `equals` c) reps' of
+ [pair] -> Just pair
+ [] -> Nothing
+ _ -> error "Overlapping reps"
+ case handlerPair of
+ Nothing -> response404 settings $ env
+ Just (ctype, Hack.Response status headers content) -> do
+ let wrapper =
+ case ctype of
+ "text/html" -> htmlWrapper settings
+ _ -> id
+ return $ Hack.Response status
+ (("Content-Type", ctype) : headers)
+ $ toLazyByteString $ wrapper content
+ [] -> response404 settings $ env
+ _ -> fail "Overlapping handlers"
+
+type UrlRewriter = PathInfo -> (PathInfo, [(String, String)])
+envToRawRequest :: UrlRewriter -> Hack.Env -> RawRequest
+envToRawRequest rewriter env =
+ let (Right rawPieces) = splitPath $ Hack.pathInfo env
+ (pi', urls) = rewriter rawPieces
+ gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)]
+ clength = tryLookup "0" "Content-Length" $ Hack.http env
+ ctype = tryLookup "" "Content-Type" $ Hack.http env
+ (posts, files) = parsePost ctype clength
+ $ Hack.hackInput env
+ rawCookie = tryLookup "" "Cookie" $ Hack.http env
+ cookies' = decodeCookies rawCookie :: [(String, String)]
+ in RawRequest pi' urls gets' posts cookies' files env
+
+data Tree = TreeScalar String
+ | TreeList [Tree]
+ | TreeMap [(String, Tree)]
+class IsTree a where
+ toTree :: a -> Tree
+
+treeToJson :: Tree -> String
+treeToJson (TreeScalar s) = '"' : encodeJson s ++ "\""
+treeToJson (TreeList l) =
+ "[" ++ intercalate "," (map treeToJson l) ++ "]"
+treeToJson (TreeMap m) =
+ "{" ++ intercalate "," (map helper m) ++ "}" where
+ helper (k, v) =
+ treeToJson (TreeScalar k) ++
+ ":" ++
+ treeToJson v
+
+treeToHtml :: Tree -> String
+treeToHtml (TreeScalar s) = encodeHtml s
+treeToHtml (TreeList l) =
+ "" ++ concatMap (\e -> "" ++ treeToHtml e ++ " ") l ++
+ " "
+treeToHtml (TreeMap m) =
+ "" ++
+ concatMap (\(k, v) -> "" ++ encodeHtml k ++ " " ++
+ "" ++ treeToHtml v ++ " ") m ++
+ " "
+
+instance Response Tree where
+ reps tree =
+ [ ("text/html", response 200 [] $ treeToHtml tree)
+ , ("application/json", response 200 [] $ treeToJson tree)
+ ]
+
+parseHttpAccept :: String -> [String]
+parseHttpAccept = filter (not . specialHttpAccept) . splitOneOf ";,"
+
+specialHttpAccept :: String -> Bool
+specialHttpAccept ('q':'=':_) = True
+specialHttpAccept ('*':_) = True
+specialHttpAccept _ = False
+
+data AtomFeed = AtomFeed
+ { atomTitle :: String
+ , atomLinkSelf :: String
+ , atomLinkHome :: String
+ , atomUpdated :: UTCTime
+ , atomEntries :: [AtomFeedEntry]
+ }
+instance Response AtomFeed where
+ reps e =
+ [ ("application/atom+xml", response 200 [] $ show e)
+ ]
+
+data AtomFeedEntry = AtomFeedEntry
+ { atomEntryLink :: String
+ , atomEntryUpdated :: UTCTime
+ , atomEntryTitle :: String
+ , atomEntryContent :: String
+ }
+
+instance Show AtomFeed where
+ show f = concat
+ [ "\n"
+ , ""
+ , ""
+ , encodeHtml $ atomTitle f
+ , " "
+ , " "
+ , " "
+ , ""
+ , formatW3 $ atomUpdated f
+ , " "
+ , ""
+ , encodeHtml $ atomLinkHome f
+ , " "
+ , concatMap show $ atomEntries f
+ , " "
+ ]
+
+instance Show AtomFeedEntry where
+ show e = concat
+ [ ""
+ , ""
+ , encodeHtml $ atomEntryLink e
+ , " "
+ , " "
+ , ""
+ , formatW3 $ atomEntryUpdated e
+ , " "
+ , ""
+ , encodeHtml $ atomEntryTitle e
+ , " "
+ , " "
+ , " "
+ ]
+
+formatW3 :: UTCTime -> String
+formatW3 = formatTime defaultTimeLocale "%FT%X-08:00"
+
+class IsTree a => ListDetail a where
+ htmlDetail :: a -> String
+ htmlDetail = treeToHtml . toTree
+ detailTitle :: a -> String
+ detailUrl :: a -> String
+ htmlList :: [a] -> String
+ htmlList l = "" ++ concatMap helper l ++ " "
+ where
+ helper i = "" ++ encodeHtml (detailTitle i) ++
+ " "
+ -- | Often times for the JSON response of the list, we don't need all
+ -- the information.
+ treeList :: [a] -> Tree
+ treeList = TreeList . map treeListSingle
+ treeListSingle :: a -> Tree
+ treeListSingle = toTree
+
+newtype ItemList a = ItemList [a]
+instance ListDetail a => Response (ItemList a) where
+ reps (ItemList l) =
+ [ ("text/html", response 200 [] $ htmlList l)
+ , ("application/json", response 200 [] $ treeToJson $ treeList l)
+ ]
+newtype ItemDetail a = ItemDetail a
+instance ListDetail a => Response (ItemDetail a) where
+ reps (ItemDetail i) =
+ [ ("text/html", response 200 [] $ htmlDetail i)
+ , ("application/json", response 200 [] $ treeToJson $ toTree i)
+ ]
+
+-- sitemaps
+data SitemapLoc = AbsLoc String | RelLoc String
+data SitemapChangeFreq = Always
+ | Hourly
+ | Daily
+ | Weekly
+ | Monthly
+ | Yearly
+ | Never
+instance Show SitemapChangeFreq where
+ show Always = "always"
+ show Hourly = "hourly"
+ show Daily = "daily"
+ show Weekly = "weekly"
+ show Monthly = "monthly"
+ show Yearly = "yearly"
+ show Never = "never"
+
+data SitemapUrl = SitemapUrl
+ { sitemapLoc :: SitemapLoc
+ , sitemapLastMod :: UTCTime
+ , sitemapChangeFreq :: SitemapChangeFreq
+ , priority :: Double
+ }
+data SitemapRequest = SitemapRequest String Int
+instance Request SitemapRequest where
+ parseRequest = do
+ env <- parseEnv
+ return $! SitemapRequest (Hack.serverName env)
+ (Hack.serverPort env)
+data SitemapResponse = SitemapResponse SitemapRequest [SitemapUrl]
+instance Show SitemapResponse where
+ show (SitemapResponse (SitemapRequest host port) urls) =
+ "\n" ++
+ "" ++
+ concatMap helper urls ++
+ " "
+ where
+ prefix = "http://" ++ host ++
+ case port of
+ 80 -> ""
+ _ -> ":" ++ show port
+ helper (SitemapUrl loc modTime freq pri) = concat
+ [ ""
+ , encodeHtml $ showLoc loc
+ , " "
+ , formatW3 modTime
+ , " "
+ , show freq
+ , " "
+ , show pri
+ , " "
+ ]
+ showLoc (AbsLoc s) = s
+ showLoc (RelLoc s) = prefix ++ s
+
+instance Response SitemapResponse where
+ reps res =
+ [ ("text/xml", response 200 [] $ show res)
+ ]
+
+sitemap :: IO [SitemapUrl] -> SitemapRequest -> IO SitemapResponse
+sitemap urls' req = do
+ urls <- urls'
+ return $ SitemapResponse req urls
+
+-- misc helper functions
+tryLookup :: Eq k => v -> k -> [(k, v)] -> v
+tryLookup v _ [] = v
+tryLookup v k ((k', v'):rest)
+ | k == k' = v'
+ | otherwise = tryLookup v k rest
+
+data GenResponse = HtmlResponse String
+ | TreeResponse Tree
+ | HtmlOrTreeResponse String Tree
+ | RedirectResponse String
+ | PermissionDeniedResult String
+ | NotFoundResponse String
+instance Response GenResponse where
+ reps (HtmlResponse h) = [("text/html", response 200 [] h)]
+ reps (TreeResponse t) = reps t
+ reps (HtmlOrTreeResponse h t) =
+ ("text/html", response 200 [] h) : reps t
+ reps (RedirectResponse url) = [("text/html", response 303 heads body)]
+ where
+ heads = [("Location", url)]
+ body = "Redirecting to " ++ encodeHtml url ++ "
"
+ reps (PermissionDeniedResult s) = [("text/plain", response 403 [] s)]
+ reps (NotFoundResponse s) = [("text/plain", response 404 [] s)]
diff --git a/restful.cabal b/restful.cabal
new file mode 100644
index 00000000..d48ec210
--- /dev/null
+++ b/restful.cabal
@@ -0,0 +1,17 @@
+name: restful
+version: 0.0.0
+license: BSD3
+license-file: LICENSE
+author: Michael Snoyman
+maintainer: Michael Snoyman
+synopsis: A Restful front controller built on Hack.
+category: Web
+stability: unstable
+cabal-version: >= 1.2
+build-type: Simple
+homepage: http://github.com/snoyberg/restful/tree/master
+
+library
+ build-depends: base
+ exposed-modules: Web.Restful
+ ghc-options: -Wall
From d99fa2d8bc5282afdcde16accc97340bbaa41714 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Thu, 2 Jul 2009 01:35:07 +0300
Subject: [PATCH 003/624] Updated build-depends list
---
restful.cabal | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/restful.cabal b/restful.cabal
index d48ec210..26910f74 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -12,6 +12,22 @@ build-type: Simple
homepage: http://github.com/snoyberg/restful/tree/master
library
- build-depends: base
+ build-depends: base >= 4 && < 5,
+ old-locale >= 1.0.0.1,
+ time >= 1.1.3,
+ hack-middleware-clientsession,
+ hack-middleware-jsonp >= 0.0.1,
+ hack-middleware-cleanpath >= 0.0.1,
+ hack-middleware-gzip,
+ hack-handler-cgi >= 0.0.2,
+ hack >= 2009.5.19,
+ split >= 0.1.1,
+ authenticate >= 0.0.1,
+ data-default >= 0.2,
+ predicates >= 0.1,
+ bytestring >= 0.9.1.4,
+ bytestring-class,
+ web-encodings,
+ mtl >= 1.1.0.2
exposed-modules: Web.Restful
ghc-options: -Wall
From 019dca9968ec475688fc0a51683caa6941b33a94 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Tue, 4 Aug 2009 09:23:30 +0300
Subject: [PATCH 004/624] Split into multiple modules
---
Web/Restful.hs | 879 +------------------------------------
Web/Restful/Application.hs | 362 +++++++++++++++
Web/Restful/Constants.hs | 17 +
Web/Restful/Request.hs | 271 ++++++++++++
Web/Restful/Response.hs | 293 +++++++++++++
Web/Restful/Utils.hs | 33 ++
restful.cabal | 10 +-
7 files changed, 992 insertions(+), 873 deletions(-)
create mode 100644 Web/Restful/Application.hs
create mode 100644 Web/Restful/Constants.hs
create mode 100644 Web/Restful/Request.hs
create mode 100644 Web/Restful/Response.hs
create mode 100644 Web/Restful/Utils.hs
diff --git a/Web/Restful.hs b/Web/Restful.hs
index 3e26bad6..f4925cad 100644
--- a/Web/Restful.hs
+++ b/Web/Restful.hs
@@ -1,9 +1,3 @@
-{-# LANGUAGE FlexibleInstances #-}
-{-# LANGUAGE TypeSynonymInstances #-}
-{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE RankNTypes #-}
-{-# LANGUAGE ExistentialQuantification #-}
-{-# LANGUAGE OverlappingInstances #-}
---------------------------------------------------------
--
-- Module : Web.Restful
@@ -19,870 +13,13 @@
---------------------------------------------------------
module Web.Restful
(
- -- * Request parsing
- -- $param_overview
-
- -- ** Types
- ParamError
- , ParamName
- , ParamValue
- -- ** Parameter type class
- , Parameter (..)
- -- ** RequestParser helpers
- , getParam
- , postParam
- , urlParam
- , anyParam
- , cookieParam
- , identifier
- , acceptedLanguages
- , requestPath
- -- ** Building actual request
- , Request (..)
- , Hack.RequestMethod (..)
- , rawFiles
- -- * Response construction
- , Response (..)
- , response
- -- ** Helper 'Response' instances
- -- *** Generic hierarchichal text
- , Tree (..)
- , IsTree (..)
- -- *** Atom news feed
- , AtomFeed (..)
- , AtomFeedEntry (..)
- -- *** Sitemap
- , sitemap
- , SitemapUrl (..)
- , SitemapLoc (..)
- , SitemapChangeFreq (..)
- -- *** Generics
- -- **** List/detail
- , ListDetail (..)
- , ItemList (..)
- , ItemDetail (..)
- , -- **** Multiple response types.
- GenResponse (..)
- -- * Defining an application
- , ApplicationMonad
- -- ** Routing
- , addResource
- -- ** Settings
- , setHandler
- , setRpxnowApiKey
- , setUrlRewriter
- , setHtmlWrapper
- -- ** Engage
- , run
+ module Data.Object
+ , module Web.Restful.Request
+ , module Web.Restful.Response
+ , module Web.Restful.Application
) where
--- hideously long import list
-import qualified Hack
-import qualified Hack.Handler.CGI
-import Control.Monad.Reader
-import Control.Monad.Writer
-import Control.Monad.State hiding (gets)
-import Data.List (intercalate)
-import Web.Encodings
-import Data.Maybe (isJust)
-import Data.ByteString.Class
-import qualified Data.ByteString.Lazy as BS
-import Data.Function.Predicate (equals)
-import Data.Default
-import qualified Web.Authenticate.Rpxnow as Rpxnow
-import qualified Web.Authenticate.OpenId as OpenId
-import Data.List.Split (splitOneOf)
-
-import Hack.Middleware.Gzip
-import Hack.Middleware.CleanPath
-import Hack.Middleware.Jsonp
-import Hack.Middleware.ClientSession
-
-import Data.Time.Format
-import Data.Time.Clock
-import System.Locale
-import Control.Applicative ((<$>), Applicative (..))
-import Control.Arrow (second)
-
--- $param_overview
--- In Restful, all of the underlying parameter values are strings. They can
--- come from multiple sources: GET parameters, URL rewriting (FIXME: link),
--- cookies, etc. However, most applications eventually want to convert
--- those strings into something else, like 'Int's. Additionally, it is
--- often desirable to allow multiple values, or no value at all.
---
--- That is what the parameter concept is for. A 'Parameter' is any value
--- which can be converted from a 'String', or list of 'String's.
-
--- | Any kind of error message generated in the parsing stage.
-type ParamError = String
-
--- | In GET parameters, the key. In cookies, the cookie name. So on and so
--- forth.
-type ParamName = String
-
--- | The 'String' value of a parameter, such as cookie content.
-type ParamValue = String
-
--- | Anything which can be converted from a 'String' or list of 'String's.
---
--- The default implementation of 'readParams' will error out if given
--- anything but 1 'ParamValue'. This is usually what you want.
---
--- Minimal complete definition: either 'readParam' or 'readParams'.
-class Parameter a where
- -- | Convert a string into the desired value, or explain why that can't
- -- happen.
- readParam :: ParamValue -> Either ParamError a
- readParam = readParams . return
-
- -- | Convert a list of strings into the desired value, or explain why
- -- that can't happen.
- readParams :: [ParamValue] -> Either ParamError a
- readParams [x] = readParam x
- readParams [] = Left "Missing parameter"
- readParams xs = Left $ "Given " ++ show (length xs) ++
- " values, expecting 1"
-
--- | Attempt to parse a list of param values using 'readParams'.
--- If that fails, return an error message and an undefined value. This way,
--- we can process all of the parameters and get all of the error messages.
--- Be careful not to use the value inside until you can be certain the
--- reading succeeded.
-tryReadParams:: Parameter a
- => ParamName
- -> [ParamValue]
- -> RequestParser a
-tryReadParams name params =
- case readParams params of
- Left s -> do
- tell [name ++ ": " ++ s]
- return $
- error $
- "Trying to evaluate nonpresent parameter " ++
- name
- Right x -> return x
-
--- | Helper function for generating 'RequestParser's from various
--- 'ParamValue' lists.
-genParam :: Parameter a
- => (RawRequest -> ParamName -> [ParamValue])
- -> ParamName
- -> RequestParser a
-genParam f name = do
- req <- ask
- tryReadParams name $ f req name
-
--- | Parse a value passed as a GET parameter.
-getParam :: Parameter a => ParamName -> RequestParser a
-getParam = genParam getParams
-
--- | Parse a value passed as a POST parameter.
-postParam :: Parameter a => ParamName -> RequestParser a
-postParam = genParam postParams
-
--- | Parse a value passed in the URL and extracted using rewrite.
--- (FIXME: link to rewrite section.)
-urlParam :: Parameter a => ParamName -> RequestParser a
-urlParam = genParam urlParams
-
--- | Parse a value passed as a GET, POST or URL parameter.
-anyParam :: Parameter a => ParamName -> RequestParser a
-anyParam = genParam anyParams
-
--- | Parse a value passed as a raw cookie.
-cookieParam :: Parameter a => ParamName -> RequestParser a
-cookieParam = genParam cookies
-
--- | Parse a value in the hackHeader field.
-hackHeaderParam :: Parameter a => ParamName -> RequestParser a
-hackHeaderParam name = do
- env <- parseEnv
- let vals' = lookup name $ Hack.hackHeaders env
- vals = case vals' of
- Nothing -> []
- Just x -> [x]
- tryReadParams name vals
-
--- | Extract the cookie which specifies the identifier for a logged in
--- user.
-identifier :: Parameter a => RequestParser a
-identifier = hackHeaderParam authCookieName
-
--- | Get the raw 'Hack.Env' value.
-parseEnv :: RequestParser Hack.Env
-parseEnv = rawEnv `fmap` ask
-
--- | Determine the ordered list of language preferences.
---
--- FIXME: Future versions should account for some cookie.
-acceptedLanguages :: RequestParser [String]
-acceptedLanguages = do
- env <- parseEnv
- let rawLang = tryLookup "" "Accept-Language" $ Hack.http env
- return $! parseHttpAccept rawLang
-
--- | Determinge the path requested by the user (ie, the path info).
-requestPath :: RequestParser String
-requestPath = do
- env <- parseEnv
- let q = case Hack.queryString env of
- "" -> ""
- q'@('?':_) -> q'
- q' -> q'
- return $! Hack.pathInfo env ++ q
-
-type RequestParser a = WriterT [ParamError] (Reader RawRequest) a
-instance Applicative (WriterT [ParamError] (Reader RawRequest)) where
- pure = return
- f <*> a = do
- f' <- f
- a' <- a
- return $! f' a'
-
--- | Parse a request into either the desired 'Request' or a list of errors.
-runRequestParser :: RequestParser a -> RawRequest -> Either [ParamError] a
-runRequestParser p req =
- let (val, errors) = (runReader (runWriterT p)) req
- in case errors of
- [] -> Right val
- x -> Left x
-
--- | The raw information passed through Hack, cleaned up a bit.
-data RawRequest = RawRequest
- { rawPathInfo :: PathInfo
- , rawUrlParams :: [(ParamName, ParamValue)]
- , rawGetParams :: [(ParamName, ParamValue)]
- , rawPostParams :: [(ParamName, ParamValue)]
- , rawCookies :: [(ParamName, ParamValue)]
- , rawFiles :: [(ParamName, FileInfo)]
- , rawEnv :: Hack.Env
- }
-
--- | All GET paramater values with the given name.
-getParams :: RawRequest -> ParamName -> [ParamValue]
-getParams rr name = map snd
- . filter (\x -> name == fst x)
- . rawGetParams
- $ rr
-
--- | All POST paramater values with the given name.
-postParams :: RawRequest -> ParamName -> [ParamValue]
-postParams rr name = map snd
- . filter (\x -> name == fst x)
- . rawPostParams
- $ rr
-
--- | All URL paramater values (see rewriting) with the given name.
-urlParams :: RawRequest -> ParamName -> [ParamValue]
-urlParams rr name = map snd
- . filter (\x -> name == fst x)
- . rawUrlParams
- $ rr
-
--- | All GET, POST and URL paramater values (see rewriting) with the given name.
-anyParams :: RawRequest -> ParamName -> [ParamValue]
-anyParams req name = urlParams req name ++
- getParams req name ++
- postParams req name
-
--- | All cookies with the given name.
-cookies :: RawRequest -> ParamName -> [ParamValue]
-cookies rr name = map snd . filter (fst `equals` name) . rawCookies $ rr
-
-instance Parameter a => Parameter (Maybe a) where
- readParams [] = Right Nothing
- readParams [x] = readParam x >>= return . Just
- readParams xs = Left $ "Given " ++ show (length xs) ++
- " values, expecting 0 or 1"
-
-instance Parameter a => Parameter [a] where
- readParams = mapM readParam
-
-instance Parameter String where
- readParam = Right
-
-instance Parameter Int where
- readParam s = case reads s of
- ((x, _):_) -> Right x
- _ -> Left $ "Invalid integer: " ++ s
-
--- | The input for a resource.
---
--- Each resource can define its own instance of 'Request' and then more
--- easily ensure that it received the correct input (ie, correct variables,
--- properly typed).
-class Request a where
- parseRequest :: RequestParser a
-
-instance Request () where
- parseRequest = return ()
-
-type ContentType = String
-
--- | The output for a resource.
-class Response a where
- -- | Provide an ordered list of possible responses, depending on content
- -- type. If the user asked for a specific response type (like
- -- text/html), then that will get priority. If not, then the first
- -- element in this list will be used.
- reps :: a -> [(ContentType, Hack.Response)]
-
--- | Wrapper around 'Hack.Response' to allow arbitrary pieces of data to be
--- used for the body.
-response :: LazyByteString lbs
- => Int
- -> [(String, String)]
- -> lbs
- -> Hack.Response
-response a b c = Hack.Response a b $ toLazyByteString c
-
-instance Response () where
- reps _ = [("text/plain", response 200 [] "")]
-
-newtype ErrorResponse = ErrorResponse String
-instance Response ErrorResponse where
- reps (ErrorResponse s) = [("text/plain", response 500 [] s)]
-
-data ResponseWrapper = forall res. Response res => ResponseWrapper res
-instance Response ResponseWrapper where
- reps (ResponseWrapper res) = reps res
-
--- | Contains settings and a list of resources.
-type ApplicationMonad = StateT ApplicationSettings (Writer [Resource])
-instance Applicative ApplicationMonad where
- pure = return
- f <*> a = do
- f' <- f
- a' <- a
- return $! f' a'
-data ApplicationSettings = ApplicationSettings
- { hackHandler :: Hack.Application -> IO ()
- , rpxnowApiKey :: Maybe String
- , encryptKey :: Either FilePath Word256
- , urlRewriter :: UrlRewriter
- , hackMiddleware :: [Hack.Middleware]
- , response404 :: Hack.Env -> IO Hack.Response
- , htmlWrapper :: BS.ByteString -> BS.ByteString
- }
-instance Default ApplicationSettings where
- def = ApplicationSettings
- { hackHandler = Hack.Handler.CGI.run
- , rpxnowApiKey = Nothing
- , encryptKey = Left defaultKeyFile
- , urlRewriter = \s -> (s, [])
- , hackMiddleware = [gzip, cleanPath, jsonp]
- , response404 = default404
- , htmlWrapper = id
- }
-
-default404 :: Hack.Env -> IO Hack.Response
-default404 env = return $
- Hack.Response
- 404
- [("Content-Type", "text/plain")]
- $ 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
-
-type PathInfo = [String]
-data Resource = Resource [Hack.RequestMethod] PathInfo LiftedHandler
-
--- FIXME document below here
-
-addResource :: (Request req, Response res)
- => [Hack.RequestMethod]
- -> PathInfo
- -> (req -> IO res)
- -> ApplicationMonad ()
-addResource methods path f =
- tell [Resource methods path $ liftHandler $ Handler f]
-
-setUrlRewriter :: UrlRewriter -> ApplicationMonad ()
-setUrlRewriter newUrlRewriter = do
- s <- get
- put $ s { urlRewriter = newUrlRewriter }
-
-setHtmlWrapper :: (BS.ByteString -> BS.ByteString) -> ApplicationMonad ()
-setHtmlWrapper f = do
- s <- get
- put $ s { htmlWrapper = f }
-
-run :: ApplicationMonad () -> IO ()
-run m = do
- let (settings, resources') = runWriter $ execStateT m def
- key <- case encryptKey settings of
- Left f -> getKey f
- Right k -> return k
- let defApp = defaultResources settings
- defResources = execWriter $ execStateT defApp def
- resources = resources' ++ defResources
- app' :: Hack.Application
- app' = makeApplication' resources settings
- clientsession' :: Hack.Middleware
- clientsession' = clientsession [authCookieName] key
- app :: Hack.Application
- app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
- hackHandler settings app
-
-setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad ()
-setHandler h = do
- settings <- get
- put $ settings { hackHandler = h }
-
-setRpxnowApiKey :: String -> ApplicationMonad ()
-setRpxnowApiKey k = do
- settings <- get
- put $ settings { rpxnowApiKey = Just k }
-
-defaultResources :: ApplicationSettings -> ApplicationMonad ()
-defaultResources settings = do
- addResource [Hack.GET] ["auth", "check"] authCheck
- addResource [Hack.GET] ["auth", "logout"] authLogout
- addResource [Hack.GET] ["auth", "openid"] authOpenidForm
- addResource [Hack.GET] ["auth", "openid", "forward"] authOpenidForward
- addResource [Hack.GET] ["auth", "openid", "complete"] authOpenidComplete
- case rpxnowApiKey settings of
- Nothing -> return ()
- Just key -> do
- addResource [Hack.GET] ["auth", "login", "rpxnow"] $
- rpxnowLogin key
-
-data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
-instance Request OIDFormReq where
- parseRequest = OIDFormReq <$> getParam "message" <*> getParam "dest"
-instance Show OIDFormReq where
- show (OIDFormReq Nothing _) = ""
- show (OIDFormReq (Just s) _) = "" ++ encodeHtml s ++
- "
"
-data OIDFormRes = OIDFormRes String (Maybe String)
-instance Response OIDFormRes where
- reps (OIDFormRes s dest) = [("text/html", response 200 heads s)]
- where
- heads =
- case dest of
- Nothing -> []
- Just dest' ->
- [("Set-Cookie", "DEST=" ++ dest' ++ "; path=/")]
-authOpenidForm :: OIDFormReq -> IO OIDFormRes
-authOpenidForm m@(OIDFormReq _ dest) =
- let html =
- show m ++
- ""
- in return $! OIDFormRes html dest
-data OIDFReq = OIDFReq String String
-instance Request OIDFReq where
- parseRequest = do
- oid <- getParam "openid"
- env <- parseEnv
- let complete = "http://" ++ Hack.serverName env ++ ":" ++
- show (Hack.serverPort env) ++
- "/auth/openid/complete/"
- return $! OIDFReq oid complete
-authOpenidForward :: OIDFReq -> IO GenResponse
-authOpenidForward (OIDFReq oid complete) = do
- res <- OpenId.getForwardUrl oid complete :: IO (Either String String)
- return $
- case res of
- Left err -> RedirectResponse $ "/auth/openid/?message=" ++
- encodeUrl err
- Right url -> RedirectResponse url
-
-data OIDComp = OIDComp [(String, String)] (Maybe String)
-instance Request OIDComp where
- parseRequest = do
- rr <- ask
- let gets = rawGetParams rr
- dest <- cookieParam "DEST"
- return $! OIDComp gets dest
-data OIDCompRes = OIDCompResErr String
- | OIDCompResGood String (Maybe String)
-instance Response OIDCompRes where
- reps (OIDCompResErr err) =
- reps $ RedirectResponse
- $ "/auth/openid/?message=" ++
- encodeUrl err
- reps (OIDCompResGood ident Nothing) =
- reps $ OIDCompResGood ident (Just "/")
- reps (OIDCompResGood ident (Just dest)) =
- [("text/plain", response 303 heads "")] where
- heads =
- [ (authCookieName, ident)
- , resetCookie "DEST"
- , ("Location", dest)
- ]
-
-resetCookie :: String -> (String, String)
-resetCookie name =
- ("Set-Cookie",
- name ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
-
-authOpenidComplete :: OIDComp -> IO OIDCompRes
-authOpenidComplete (OIDComp gets' dest) = do
- res <- OpenId.authenticate gets' :: IO (Either String OpenId.Identifier)
- return $
- case res of
- Left err -> OIDCompResErr err
- Right (OpenId.Identifier ident) -> OIDCompResGood ident dest
-
--- | token dest
-data RpxnowRequest = RpxnowRequest String (Maybe String)
-instance Request RpxnowRequest where
- parseRequest = do
- token <- getParam "token"
- dest <- getParam "dest"
- return $! RpxnowRequest token $ chopHash `fmap` dest
-
-chopHash :: String -> String
-chopHash ('#':rest) = rest
-chopHash x = x
-
--- | dest identifier
-data RpxnowResponse = RpxnowResponse String (Maybe String)
-instance Response RpxnowResponse where
- reps (RpxnowResponse dest Nothing) =
- [("text/html", response 303 [("Location", dest)] "")]
- reps (RpxnowResponse dest (Just ident)) =
- [("text/html", response 303
- [ ("Location", dest)
- , (authCookieName, ident)
- ]
- "")]
-
-rpxnowLogin :: String -- ^ api key
- -> RpxnowRequest
- -> IO RpxnowResponse
-rpxnowLogin apiKey (RpxnowRequest token dest') = do
- let dest = case dest' of
- Nothing -> "/"
- Just "" -> "/"
- Just s -> s
- ident' <- Rpxnow.authenticate apiKey token
- return $ RpxnowResponse dest (Rpxnow.identifier `fmap` ident')
-
-authCookieName :: String
-authCookieName = "IDENTIFIER"
-
-data AuthRequest = AuthRequest (Maybe String)
-instance Request AuthRequest where
- parseRequest = AuthRequest `fmap` identifier
-
-authCheck :: AuthRequest -> IO Tree
-authCheck (AuthRequest Nothing) =
- return $ TreeMap [("status", TreeScalar "notloggedin")]
-authCheck (AuthRequest (Just i)) =
- return $ TreeMap $
- [ ("status", TreeScalar "loggedin")
- , ("ident", TreeScalar i)
- ]
-
-authLogout :: () -> IO LogoutResponse
-authLogout _ = return LogoutResponse
-
-data LogoutResponse = LogoutResponse
-instance Response LogoutResponse where
- reps _ = map (second addCookie) $ reps tree where
- tree = TreeMap [("status", TreeScalar "loggedout")]
- addCookie (Hack.Response s h c) =
- Hack.Response s (h':h) c
- h' = resetCookie authCookieName
-
-makeApplication' :: [Resource]
- -> ApplicationSettings
- -> Hack.Env
- -> IO Hack.Response
-makeApplication' resources settings env = do
- let method = Hack.requestMethod env
- rr = envToRawRequest (urlRewriter settings) env
- path' = rawPathInfo rr
- isValid :: Resource -> Bool
- isValid (Resource methods path _) = method `elem` methods
- && path == path'
- case filter isValid resources of
- [Resource _ _ handler] -> do
- let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
- ctypes' = parseHttpAccept rawHttpAccept
- body <- handler rr
- let reps' = reps body
- ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
- let handlerPair =
- case ctypes of
- [] -> Just $ head reps'
- (c:_) ->
- case filter (fst `equals` c) reps' of
- [pair] -> Just pair
- [] -> Nothing
- _ -> error "Overlapping reps"
- case handlerPair of
- Nothing -> response404 settings $ env
- Just (ctype, Hack.Response status headers content) -> do
- let wrapper =
- case ctype of
- "text/html" -> htmlWrapper settings
- _ -> id
- return $ Hack.Response status
- (("Content-Type", ctype) : headers)
- $ toLazyByteString $ wrapper content
- [] -> response404 settings $ env
- _ -> fail "Overlapping handlers"
-
-type UrlRewriter = PathInfo -> (PathInfo, [(String, String)])
-envToRawRequest :: UrlRewriter -> Hack.Env -> RawRequest
-envToRawRequest rewriter env =
- let (Right rawPieces) = splitPath $ Hack.pathInfo env
- (pi', urls) = rewriter rawPieces
- gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)]
- clength = tryLookup "0" "Content-Length" $ Hack.http env
- ctype = tryLookup "" "Content-Type" $ Hack.http env
- (posts, files) = parsePost ctype clength
- $ Hack.hackInput env
- rawCookie = tryLookup "" "Cookie" $ Hack.http env
- cookies' = decodeCookies rawCookie :: [(String, String)]
- in RawRequest pi' urls gets' posts cookies' files env
-
-data Tree = TreeScalar String
- | TreeList [Tree]
- | TreeMap [(String, Tree)]
-class IsTree a where
- toTree :: a -> Tree
-
-treeToJson :: Tree -> String
-treeToJson (TreeScalar s) = '"' : encodeJson s ++ "\""
-treeToJson (TreeList l) =
- "[" ++ intercalate "," (map treeToJson l) ++ "]"
-treeToJson (TreeMap m) =
- "{" ++ intercalate "," (map helper m) ++ "}" where
- helper (k, v) =
- treeToJson (TreeScalar k) ++
- ":" ++
- treeToJson v
-
-treeToHtml :: Tree -> String
-treeToHtml (TreeScalar s) = encodeHtml s
-treeToHtml (TreeList l) =
- "" ++ concatMap (\e -> "" ++ treeToHtml e ++ " ") l ++
- " "
-treeToHtml (TreeMap m) =
- "" ++
- concatMap (\(k, v) -> "" ++ encodeHtml k ++ " " ++
- "" ++ treeToHtml v ++ " ") m ++
- " "
-
-instance Response Tree where
- reps tree =
- [ ("text/html", response 200 [] $ treeToHtml tree)
- , ("application/json", response 200 [] $ treeToJson tree)
- ]
-
-parseHttpAccept :: String -> [String]
-parseHttpAccept = filter (not . specialHttpAccept) . splitOneOf ";,"
-
-specialHttpAccept :: String -> Bool
-specialHttpAccept ('q':'=':_) = True
-specialHttpAccept ('*':_) = True
-specialHttpAccept _ = False
-
-data AtomFeed = AtomFeed
- { atomTitle :: String
- , atomLinkSelf :: String
- , atomLinkHome :: String
- , atomUpdated :: UTCTime
- , atomEntries :: [AtomFeedEntry]
- }
-instance Response AtomFeed where
- reps e =
- [ ("application/atom+xml", response 200 [] $ show e)
- ]
-
-data AtomFeedEntry = AtomFeedEntry
- { atomEntryLink :: String
- , atomEntryUpdated :: UTCTime
- , atomEntryTitle :: String
- , atomEntryContent :: String
- }
-
-instance Show AtomFeed where
- show f = concat
- [ "\n"
- , ""
- , ""
- , encodeHtml $ atomTitle f
- , " "
- , " "
- , " "
- , ""
- , formatW3 $ atomUpdated f
- , " "
- , ""
- , encodeHtml $ atomLinkHome f
- , " "
- , concatMap show $ atomEntries f
- , " "
- ]
-
-instance Show AtomFeedEntry where
- show e = concat
- [ ""
- , ""
- , encodeHtml $ atomEntryLink e
- , " "
- , " "
- , ""
- , formatW3 $ atomEntryUpdated e
- , " "
- , ""
- , encodeHtml $ atomEntryTitle e
- , " "
- , " "
- , " "
- ]
-
-formatW3 :: UTCTime -> String
-formatW3 = formatTime defaultTimeLocale "%FT%X-08:00"
-
-class IsTree a => ListDetail a where
- htmlDetail :: a -> String
- htmlDetail = treeToHtml . toTree
- detailTitle :: a -> String
- detailUrl :: a -> String
- htmlList :: [a] -> String
- htmlList l = "" ++ concatMap helper l ++ " "
- where
- helper i = "" ++ encodeHtml (detailTitle i) ++
- " "
- -- | Often times for the JSON response of the list, we don't need all
- -- the information.
- treeList :: [a] -> Tree
- treeList = TreeList . map treeListSingle
- treeListSingle :: a -> Tree
- treeListSingle = toTree
-
-newtype ItemList a = ItemList [a]
-instance ListDetail a => Response (ItemList a) where
- reps (ItemList l) =
- [ ("text/html", response 200 [] $ htmlList l)
- , ("application/json", response 200 [] $ treeToJson $ treeList l)
- ]
-newtype ItemDetail a = ItemDetail a
-instance ListDetail a => Response (ItemDetail a) where
- reps (ItemDetail i) =
- [ ("text/html", response 200 [] $ htmlDetail i)
- , ("application/json", response 200 [] $ treeToJson $ toTree i)
- ]
-
--- sitemaps
-data SitemapLoc = AbsLoc String | RelLoc String
-data SitemapChangeFreq = Always
- | Hourly
- | Daily
- | Weekly
- | Monthly
- | Yearly
- | Never
-instance Show SitemapChangeFreq where
- show Always = "always"
- show Hourly = "hourly"
- show Daily = "daily"
- show Weekly = "weekly"
- show Monthly = "monthly"
- show Yearly = "yearly"
- show Never = "never"
-
-data SitemapUrl = SitemapUrl
- { sitemapLoc :: SitemapLoc
- , sitemapLastMod :: UTCTime
- , sitemapChangeFreq :: SitemapChangeFreq
- , priority :: Double
- }
-data SitemapRequest = SitemapRequest String Int
-instance Request SitemapRequest where
- parseRequest = do
- env <- parseEnv
- return $! SitemapRequest (Hack.serverName env)
- (Hack.serverPort env)
-data SitemapResponse = SitemapResponse SitemapRequest [SitemapUrl]
-instance Show SitemapResponse where
- show (SitemapResponse (SitemapRequest host port) urls) =
- "\n" ++
- "" ++
- concatMap helper urls ++
- " "
- where
- prefix = "http://" ++ host ++
- case port of
- 80 -> ""
- _ -> ":" ++ show port
- helper (SitemapUrl loc modTime freq pri) = concat
- [ ""
- , encodeHtml $ showLoc loc
- , " "
- , formatW3 modTime
- , " "
- , show freq
- , " "
- , show pri
- , " "
- ]
- showLoc (AbsLoc s) = s
- showLoc (RelLoc s) = prefix ++ s
-
-instance Response SitemapResponse where
- reps res =
- [ ("text/xml", response 200 [] $ show res)
- ]
-
-sitemap :: IO [SitemapUrl] -> SitemapRequest -> IO SitemapResponse
-sitemap urls' req = do
- urls <- urls'
- return $ SitemapResponse req urls
-
--- misc helper functions
-tryLookup :: Eq k => v -> k -> [(k, v)] -> v
-tryLookup v _ [] = v
-tryLookup v k ((k', v'):rest)
- | k == k' = v'
- | otherwise = tryLookup v k rest
-
-data GenResponse = HtmlResponse String
- | TreeResponse Tree
- | HtmlOrTreeResponse String Tree
- | RedirectResponse String
- | PermissionDeniedResult String
- | NotFoundResponse String
-instance Response GenResponse where
- reps (HtmlResponse h) = [("text/html", response 200 [] h)]
- reps (TreeResponse t) = reps t
- reps (HtmlOrTreeResponse h t) =
- ("text/html", response 200 [] h) : reps t
- reps (RedirectResponse url) = [("text/html", response 303 heads body)]
- where
- heads = [("Location", url)]
- body = "Redirecting to " ++ encodeHtml url ++ "
"
- reps (PermissionDeniedResult s) = [("text/plain", response 403 [] s)]
- reps (NotFoundResponse s) = [("text/plain", response 404 [] s)]
+import Data.Object
+import Web.Restful.Request
+import Web.Restful.Response
+import Web.Restful.Application
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
new file mode 100644
index 00000000..84c30672
--- /dev/null
+++ b/Web/Restful/Application.hs
@@ -0,0 +1,362 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE ExistentialQuantification #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Application
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Defining the application.
+--
+---------------------------------------------------------
+module Web.Restful.Application
+ (
+ -- * Defining an application
+ ApplicationMonad
+ -- ** Routing
+ , addResource
+ -- ** Settings
+ , setHandler
+ , setRpxnowApiKey
+ , setUrlRewriter
+ , setHtmlWrapper
+ -- ** Engage
+ , run
+ ) where
+
+-- hideously long import list
+import qualified Hack
+import qualified Hack.Handler.CGI
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Control.Monad.State hiding (gets)
+import Web.Encodings
+import Data.Maybe (isJust)
+import Data.ByteString.Class
+import qualified Data.ByteString.Lazy as BS
+import Data.Function.Predicate (equals)
+import Data.Default
+import qualified Web.Authenticate.Rpxnow as Rpxnow
+import qualified Web.Authenticate.OpenId as OpenId
+
+import Hack.Middleware.Gzip
+import Hack.Middleware.CleanPath
+import Hack.Middleware.Jsonp
+import Hack.Middleware.ClientSession
+
+import Control.Applicative ((<$>), Applicative (..))
+import Control.Arrow (second)
+
+import Web.Restful.Request
+import Web.Restful.Response
+import Web.Restful.Constants
+import Web.Restful.Utils
+import Data.Object
+
+-- | Contains settings and a list of resources.
+type ApplicationMonad = StateT ApplicationSettings (Writer [Resource])
+instance Applicative ApplicationMonad where
+ pure = return
+ f <*> a = do
+ f' <- f
+ a' <- a
+ return $! f' a'
+data ApplicationSettings = ApplicationSettings
+ { hackHandler :: Hack.Application -> IO ()
+ , rpxnowApiKey :: Maybe String
+ , encryptKey :: Either FilePath Word256
+ , urlRewriter :: UrlRewriter
+ , hackMiddleware :: [Hack.Middleware]
+ , response404 :: Hack.Env -> IO Hack.Response
+ , htmlWrapper :: BS.ByteString -> BS.ByteString
+ }
+instance Default ApplicationSettings where
+ def = ApplicationSettings
+ { hackHandler = Hack.Handler.CGI.run
+ , rpxnowApiKey = Nothing
+ , encryptKey = Left defaultKeyFile
+ , urlRewriter = \s -> (s, [])
+ , hackMiddleware = [gzip, cleanPath, jsonp]
+ , response404 = default404
+ , htmlWrapper = id
+ }
+
+default404 :: Hack.Env -> IO Hack.Response
+default404 env = return $
+ Hack.Response
+ 404
+ [("Content-Type", "text/plain")]
+ $ 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
+
+addResource :: (Request req, Response res)
+ => [Hack.RequestMethod]
+ -> PathInfo
+ -> (req -> IO res)
+ -> ApplicationMonad ()
+addResource methods path f =
+ tell [Resource methods path $ liftHandler $ Handler f]
+
+setUrlRewriter :: UrlRewriter -> ApplicationMonad ()
+setUrlRewriter newUrlRewriter = do
+ s <- get
+ put $ s { urlRewriter = newUrlRewriter }
+
+setHtmlWrapper :: (BS.ByteString -> BS.ByteString) -> ApplicationMonad ()
+setHtmlWrapper f = do
+ s <- get
+ put $ s { htmlWrapper = f }
+
+run :: ApplicationMonad () -> IO ()
+run m = do
+ let (settings, resources') = runWriter $ execStateT m def
+ key <- case encryptKey settings of
+ Left f -> getKey f
+ Right k -> return k
+ let defApp = defaultResources settings
+ defResources = execWriter $ execStateT defApp def
+ resources = resources' ++ defResources
+ app' :: Hack.Application
+ app' = makeApplication' resources settings
+ clientsession' :: Hack.Middleware
+ clientsession' = clientsession [authCookieName] key
+ app :: Hack.Application
+ app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
+ hackHandler settings app
+
+setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad ()
+setHandler h = do
+ settings <- get
+ put $ settings { hackHandler = h }
+
+setRpxnowApiKey :: String -> ApplicationMonad ()
+setRpxnowApiKey k = do
+ settings <- get
+ put $ settings { rpxnowApiKey = Just k }
+
+defaultResources :: ApplicationSettings -> ApplicationMonad ()
+defaultResources settings = do
+ addResource [Hack.GET] ["auth", "check"] authCheck
+ addResource [Hack.GET] ["auth", "logout"] authLogout
+ addResource [Hack.GET] ["auth", "openid"] authOpenidForm
+ addResource [Hack.GET] ["auth", "openid", "forward"] authOpenidForward
+ addResource [Hack.GET] ["auth", "openid", "complete"] authOpenidComplete
+ case rpxnowApiKey settings of
+ Nothing -> return ()
+ Just key -> do
+ addResource [Hack.GET] ["auth", "login", "rpxnow"] $
+ rpxnowLogin key
+
+data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
+instance Request OIDFormReq where
+ parseRequest = OIDFormReq <$> getParam "message" <*> getParam "dest"
+instance Show OIDFormReq where
+ show (OIDFormReq Nothing _) = ""
+ show (OIDFormReq (Just s) _) = "" ++ encodeHtml s ++
+ "
"
+data OIDFormRes = OIDFormRes String (Maybe String)
+instance Response OIDFormRes where
+ reps (OIDFormRes s dest) = [("text/html", response 200 heads s)]
+ where
+ heads =
+ case dest of
+ Nothing -> []
+ Just dest' ->
+ [("Set-Cookie", "DEST=" ++ dest' ++ "; path=/")]
+authOpenidForm :: OIDFormReq -> IO OIDFormRes
+authOpenidForm m@(OIDFormReq _ dest) =
+ let html =
+ show m ++
+ ""
+ in return $! OIDFormRes html dest
+data OIDFReq = OIDFReq String String
+instance Request OIDFReq where
+ parseRequest = do
+ oid <- getParam "openid"
+ env <- parseEnv
+ let complete = "http://" ++ Hack.serverName env ++ ":" ++
+ show (Hack.serverPort env) ++
+ "/auth/openid/complete/"
+ return $! OIDFReq oid complete
+authOpenidForward :: OIDFReq -> IO GenResponse
+authOpenidForward (OIDFReq oid complete) = do
+ res <- OpenId.getForwardUrl oid complete :: IO (Either String String)
+ return $
+ case res of
+ Left err -> RedirectResponse $ "/auth/openid/?message=" ++
+ encodeUrl err
+ Right url -> RedirectResponse url
+
+data OIDComp = OIDComp [(String, String)] (Maybe String)
+instance Request OIDComp where
+ parseRequest = do
+ rr <- ask
+ let gets = rawGetParams rr
+ dest <- cookieParam "DEST"
+ return $! OIDComp gets dest
+data OIDCompRes = OIDCompResErr String
+ | OIDCompResGood String (Maybe String)
+instance Response OIDCompRes where
+ reps (OIDCompResErr err) =
+ reps $ RedirectResponse
+ $ "/auth/openid/?message=" ++
+ encodeUrl err
+ reps (OIDCompResGood ident Nothing) =
+ reps $ OIDCompResGood ident (Just "/")
+ reps (OIDCompResGood ident (Just dest)) =
+ [("text/plain", response 303 heads "")] where
+ heads =
+ [ (authCookieName, ident)
+ , resetCookie "DEST"
+ , ("Location", dest)
+ ]
+
+resetCookie :: String -> (String, String)
+resetCookie name =
+ ("Set-Cookie",
+ name ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
+
+authOpenidComplete :: OIDComp -> IO OIDCompRes
+authOpenidComplete (OIDComp gets' dest) = do
+ res <- OpenId.authenticate gets' :: IO (Either String OpenId.Identifier)
+ return $
+ case res of
+ Left err -> OIDCompResErr err
+ Right (OpenId.Identifier ident) -> OIDCompResGood ident dest
+
+-- | token dest
+data RpxnowRequest = RpxnowRequest String (Maybe String)
+instance Request RpxnowRequest where
+ parseRequest = do
+ token <- getParam "token"
+ dest <- getParam "dest"
+ return $! RpxnowRequest token $ chopHash `fmap` dest
+
+chopHash :: String -> String
+chopHash ('#':rest) = rest
+chopHash x = x
+
+-- | dest identifier
+data RpxnowResponse = RpxnowResponse String (Maybe String)
+instance Response RpxnowResponse where
+ reps (RpxnowResponse dest Nothing) =
+ [("text/html", response 303 [("Location", dest)] "")]
+ reps (RpxnowResponse dest (Just ident)) =
+ [("text/html", response 303
+ [ ("Location", dest)
+ , (authCookieName, ident)
+ ]
+ "")]
+
+rpxnowLogin :: String -- ^ api key
+ -> RpxnowRequest
+ -> IO RpxnowResponse
+rpxnowLogin apiKey (RpxnowRequest token dest') = do
+ let dest = case dest' of
+ Nothing -> "/"
+ Just "" -> "/"
+ Just s -> s
+ ident' <- Rpxnow.authenticate apiKey token
+ return $ RpxnowResponse dest (Rpxnow.identifier `fmap` ident')
+
+data AuthRequest = AuthRequest (Maybe String)
+instance Request AuthRequest where
+ parseRequest = AuthRequest `fmap` identifier
+
+authCheck :: AuthRequest -> IO Object
+authCheck (AuthRequest Nothing) =
+ return $ toObject [("status", "notloggedin")]
+authCheck (AuthRequest (Just i)) =
+ return $ toObject
+ [ ("status", "loggedin")
+ , ("ident", i)
+ ]
+
+authLogout :: () -> IO LogoutResponse
+authLogout _ = return LogoutResponse
+
+data LogoutResponse = LogoutResponse
+instance Response LogoutResponse where
+ reps _ = map (second addCookie) $ reps tree where
+ tree = toObject [("status", "loggedout")]
+ addCookie (Hack.Response s h c) =
+ Hack.Response s (h':h) c
+ h' = resetCookie authCookieName
+
+makeApplication' :: [Resource]
+ -> ApplicationSettings
+ -> Hack.Env
+ -> IO Hack.Response
+makeApplication' resources settings env = do
+ let method = Hack.requestMethod env
+ rr = envToRawRequest (urlRewriter settings) env
+ path' = rawPathInfo rr
+ isValid :: Resource -> Bool
+ isValid (Resource methods path _) = method `elem` methods
+ && path == path'
+ case filter isValid resources of
+ [Resource _ _ handler] -> do
+ let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
+ ctypes' = parseHttpAccept rawHttpAccept
+ body <- handler rr
+ let reps' = reps body
+ ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
+ let handlerPair =
+ case ctypes of
+ [] -> Just $ head reps'
+ (c:_) ->
+ case filter (fst `equals` c) reps' of
+ [pair] -> Just pair
+ [] -> Nothing
+ _ -> error "Overlapping reps"
+ case handlerPair of
+ Nothing -> response404 settings $ env
+ Just (ctype, Hack.Response status headers content) -> do
+ let wrapper =
+ case ctype of
+ "text/html" -> htmlWrapper settings
+ _ -> id
+ return $ Hack.Response status
+ (("Content-Type", ctype) : headers)
+ $ toLazyByteString $ wrapper content
+ [] -> response404 settings $ env
+ _ -> fail "Overlapping handlers"
+
+type UrlRewriter = PathInfo -> (PathInfo, [(String, String)])
+envToRawRequest :: UrlRewriter -> Hack.Env -> RawRequest
+envToRawRequest rewriter env =
+ let (Right rawPieces) = splitPath $ Hack.pathInfo env
+ (pi', urls) = rewriter rawPieces
+ gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)]
+ clength = tryLookup "0" "Content-Length" $ Hack.http env
+ ctype = tryLookup "" "Content-Type" $ Hack.http env
+ (posts, files) = parsePost ctype clength
+ $ Hack.hackInput env
+ rawCookie = tryLookup "" "Cookie" $ Hack.http env
+ cookies' = decodeCookies rawCookie :: [(String, String)]
+ in RawRequest pi' urls gets' posts cookies' files env
diff --git a/Web/Restful/Constants.hs b/Web/Restful/Constants.hs
new file mode 100644
index 00000000..c39aa532
--- /dev/null
+++ b/Web/Restful/Constants.hs
@@ -0,0 +1,17 @@
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Constants
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Constants used throughout Restful.
+--
+---------------------------------------------------------
+module Web.Restful.Constants where
+
+authCookieName :: String
+authCookieName = "IDENTIFIER"
diff --git a/Web/Restful/Request.hs b/Web/Restful/Request.hs
new file mode 100644
index 00000000..9f98849b
--- /dev/null
+++ b/Web/Restful/Request.hs
@@ -0,0 +1,271 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE OverlappingInstances #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Request
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Code for extracting parameters from requests.
+--
+---------------------------------------------------------
+module Web.Restful.Request
+ (
+ -- * Request parsing
+ -- $param_overview
+
+ -- ** Types
+ ParamError
+ , ParamName
+ , ParamValue
+ -- ** Parameter type class
+ , Parameter (..)
+ -- ** RequestParser helpers
+ , getParam
+ , postParam
+ , urlParam
+ , anyParam
+ , cookieParam
+ , identifier
+ , acceptedLanguages
+ , requestPath
+ -- ** Building actual request
+ , Request (..)
+ , Hack.RequestMethod (..)
+ -- ** FIXME
+ , parseEnv
+ , RawRequest (..)
+ , PathInfo
+ , runRequestParser
+ ) where
+
+import qualified Hack
+import Data.Function.Predicate (equals)
+import Control.Monad.Reader
+import Control.Monad.Writer
+import Control.Monad.Error ()
+import Web.Restful.Constants
+import Web.Restful.Utils
+import Control.Applicative (Applicative (..))
+import Web.Encodings
+
+-- $param_overview
+-- In Restful, all of the underlying parameter values are strings. They can
+-- come from multiple sources: GET parameters, URL rewriting (FIXME: link),
+-- cookies, etc. However, most applications eventually want to convert
+-- those strings into something else, like 'Int's. Additionally, it is
+-- often desirable to allow multiple values, or no value at all.
+--
+-- That is what the parameter concept is for. A 'Parameter' is any value
+-- which can be converted from a 'String', or list of 'String's.
+
+-- | Any kind of error message generated in the parsing stage.
+type ParamError = String
+
+-- | In GET parameters, the key. In cookies, the cookie name. So on and so
+-- forth.
+type ParamName = String
+
+-- | The 'String' value of a parameter, such as cookie content.
+type ParamValue = String
+
+-- | Anything which can be converted from a 'String' or list of 'String's.
+--
+-- The default implementation of 'readParams' will error out if given
+-- anything but 1 'ParamValue'. This is usually what you want.
+--
+-- Minimal complete definition: either 'readParam' or 'readParams'.
+class Parameter a where
+ -- | Convert a string into the desired value, or explain why that can't
+ -- happen.
+ readParam :: ParamValue -> Either ParamError a
+ readParam = readParams . return
+
+ -- | Convert a list of strings into the desired value, or explain why
+ -- that can't happen.
+ readParams :: [ParamValue] -> Either ParamError a
+ readParams [x] = readParam x
+ readParams [] = Left "Missing parameter"
+ readParams xs = Left $ "Given " ++ show (length xs) ++
+ " values, expecting 1"
+
+-- | Attempt to parse a list of param values using 'readParams'.
+-- If that fails, return an error message and an undefined value. This way,
+-- we can process all of the parameters and get all of the error messages.
+-- Be careful not to use the value inside until you can be certain the
+-- reading succeeded.
+tryReadParams:: Parameter a
+ => ParamName
+ -> [ParamValue]
+ -> RequestParser a
+tryReadParams name params =
+ case readParams params of
+ Left s -> do
+ tell [name ++ ": " ++ s]
+ return $
+ error $
+ "Trying to evaluate nonpresent parameter " ++
+ name
+ Right x -> return x
+
+-- | Helper function for generating 'RequestParser's from various
+-- 'ParamValue' lists.
+genParam :: Parameter a
+ => (RawRequest -> ParamName -> [ParamValue])
+ -> ParamName
+ -> RequestParser a
+genParam f name = do
+ req <- ask
+ tryReadParams name $ f req name
+
+-- | Parse a value passed as a GET parameter.
+getParam :: Parameter a => ParamName -> RequestParser a
+getParam = genParam getParams
+
+-- | Parse a value passed as a POST parameter.
+postParam :: Parameter a => ParamName -> RequestParser a
+postParam = genParam postParams
+
+-- | Parse a value passed in the URL and extracted using rewrite.
+-- (FIXME: link to rewrite section.)
+urlParam :: Parameter a => ParamName -> RequestParser a
+urlParam = genParam urlParams
+
+-- | Parse a value passed as a GET, POST or URL parameter.
+anyParam :: Parameter a => ParamName -> RequestParser a
+anyParam = genParam anyParams
+
+-- | Parse a value passed as a raw cookie.
+cookieParam :: Parameter a => ParamName -> RequestParser a
+cookieParam = genParam cookies
+
+-- | Parse a value in the hackHeader field.
+hackHeaderParam :: Parameter a => ParamName -> RequestParser a
+hackHeaderParam name = do
+ env <- parseEnv
+ let vals' = lookup name $ Hack.hackHeaders env
+ vals = case vals' of
+ Nothing -> []
+ Just x -> [x]
+ tryReadParams name vals
+
+-- | Extract the cookie which specifies the identifier for a logged in
+-- user.
+identifier :: Parameter a => RequestParser a
+identifier = hackHeaderParam authCookieName
+
+-- | Get the raw 'Hack.Env' value.
+parseEnv :: RequestParser Hack.Env
+parseEnv = rawEnv `fmap` ask
+
+-- | Determine the ordered list of language preferences.
+--
+-- FIXME: Future versions should account for some cookie.
+acceptedLanguages :: RequestParser [String]
+acceptedLanguages = do
+ env <- parseEnv
+ let rawLang = tryLookup "" "Accept-Language" $ Hack.http env
+ return $! parseHttpAccept rawLang
+
+-- | Determinge the path requested by the user (ie, the path info).
+requestPath :: RequestParser String
+requestPath = do
+ env <- parseEnv
+ let q = case Hack.queryString env of
+ "" -> ""
+ q'@('?':_) -> q'
+ q' -> q'
+ return $! Hack.pathInfo env ++ q
+
+type RequestParser a = WriterT [ParamError] (Reader RawRequest) a
+instance Applicative (WriterT [ParamError] (Reader RawRequest)) where
+ pure = return
+ f <*> a = do
+ f' <- f
+ a' <- a
+ return $! f' a'
+
+-- | Parse a request into either the desired 'Request' or a list of errors.
+runRequestParser :: RequestParser a -> RawRequest -> Either [ParamError] a
+runRequestParser p req =
+ let (val, errors) = (runReader (runWriterT p)) req
+ in case errors of
+ [] -> Right val
+ x -> Left x
+
+-- | The raw information passed through Hack, cleaned up a bit.
+data RawRequest = RawRequest
+ { rawPathInfo :: PathInfo
+ , rawUrlParams :: [(ParamName, ParamValue)]
+ , rawGetParams :: [(ParamName, ParamValue)]
+ , rawPostParams :: [(ParamName, ParamValue)]
+ , rawCookies :: [(ParamName, ParamValue)]
+ , rawFiles :: [(ParamName, FileInfo)]
+ , rawEnv :: Hack.Env
+ }
+
+-- | All GET paramater values with the given name.
+getParams :: RawRequest -> ParamName -> [ParamValue]
+getParams rr name = map snd
+ . filter (\x -> name == fst x)
+ . rawGetParams
+ $ rr
+
+-- | All POST paramater values with the given name.
+postParams :: RawRequest -> ParamName -> [ParamValue]
+postParams rr name = map snd
+ . filter (\x -> name == fst x)
+ . rawPostParams
+ $ rr
+
+-- | All URL paramater values (see rewriting) with the given name.
+urlParams :: RawRequest -> ParamName -> [ParamValue]
+urlParams rr name = map snd
+ . filter (\x -> name == fst x)
+ . rawUrlParams
+ $ rr
+
+-- | All GET, POST and URL paramater values (see rewriting) with the given name.
+anyParams :: RawRequest -> ParamName -> [ParamValue]
+anyParams req name = urlParams req name ++
+ getParams req name ++
+ postParams req name
+
+-- | All cookies with the given name.
+cookies :: RawRequest -> ParamName -> [ParamValue]
+cookies rr name = map snd . filter (fst `equals` name) . rawCookies $ rr
+
+instance Parameter a => Parameter (Maybe a) where
+ readParams [] = Right Nothing
+ readParams [x] = readParam x >>= return . Just
+ readParams xs = Left $ "Given " ++ show (length xs) ++
+ " values, expecting 0 or 1"
+
+instance Parameter a => Parameter [a] where
+ readParams = mapM readParam
+
+instance Parameter String where
+ readParam = Right
+
+instance Parameter Int where
+ readParam s = case reads s of
+ ((x, _):_) -> Right x
+ _ -> Left $ "Invalid integer: " ++ s
+
+-- | The input for a resource.
+--
+-- Each resource can define its own instance of 'Request' and then more
+-- easily ensure that it received the correct input (ie, correct variables,
+-- properly typed).
+class Request a where
+ parseRequest :: RequestParser a
+
+instance Request () where
+ parseRequest = return ()
+
+type PathInfo = [String]
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
new file mode 100644
index 00000000..8fd46e7c
--- /dev/null
+++ b/Web/Restful/Response.hs
@@ -0,0 +1,293 @@
+{-# LANGUAGE ExistentialQuantification #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Response
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Generating responses.
+--
+---------------------------------------------------------
+module Web.Restful.Response
+ (
+ -- * Response construction
+ Response (..)
+ , response
+ -- ** Helper 'Response' instances
+ -- *** Atom news feed
+ , AtomFeed (..)
+ , AtomFeedEntry (..)
+ -- *** Sitemap
+ , sitemap
+ , SitemapUrl (..)
+ , SitemapLoc (..)
+ , SitemapChangeFreq (..)
+ -- *** Generics
+ -- **** List/detail
+ , ListDetail (..)
+ , ItemList (..)
+ , ItemDetail (..)
+ -- **** Multiple response types.
+ , GenResponse (..)
+ -- * FIXME
+ , ResponseWrapper (..)
+ , ErrorResponse (..)
+ ) where
+
+import Data.ByteString.Class
+import qualified Hack
+import Data.Time.Format
+import Data.Time.Clock
+import Web.Encodings
+import System.Locale
+import Web.Restful.Request -- FIXME ultimately remove
+import Data.Object
+import Data.List (intercalate)
+
+type ContentType = String
+
+-- | The output for a resource.
+class Response a where
+ -- | Provide an ordered list of possible responses, depending on content
+ -- type. If the user asked for a specific response type (like
+ -- text/html), then that will get priority. If not, then the first
+ -- element in this list will be used.
+ reps :: a -> [(ContentType, Hack.Response)]
+
+-- | Wrapper around 'Hack.Response' to allow arbitrary pieces of data to be
+-- used for the body.
+response :: LazyByteString lbs
+ => Int
+ -> [(String, String)]
+ -> lbs
+ -> Hack.Response
+response a b c = Hack.Response a b $ toLazyByteString c
+
+instance Response () where
+ reps _ = [("text/plain", response 200 [] "")]
+
+newtype ErrorResponse = ErrorResponse String
+instance Response ErrorResponse where
+ reps (ErrorResponse s) = [("text/plain", response 500 [] s)]
+
+data ResponseWrapper = forall res. Response res => ResponseWrapper res
+instance Response ResponseWrapper where
+ reps (ResponseWrapper res) = reps res
+
+data AtomFeed = AtomFeed
+ { atomTitle :: String
+ , atomLinkSelf :: String
+ , atomLinkHome :: String
+ , atomUpdated :: UTCTime
+ , atomEntries :: [AtomFeedEntry]
+ }
+instance Response AtomFeed where
+ reps e =
+ [ ("application/atom+xml", response 200 [] $ show e)
+ ]
+
+data AtomFeedEntry = AtomFeedEntry
+ { atomEntryLink :: String
+ , atomEntryUpdated :: UTCTime
+ , atomEntryTitle :: String
+ , atomEntryContent :: String
+ }
+
+instance Show AtomFeed where
+ show f = concat
+ [ "\n"
+ , ""
+ , ""
+ , encodeHtml $ atomTitle f
+ , " "
+ , " "
+ , " "
+ , ""
+ , formatW3 $ atomUpdated f
+ , " "
+ , ""
+ , encodeHtml $ atomLinkHome f
+ , " "
+ , concatMap show $ atomEntries f
+ , " "
+ ]
+
+instance Show AtomFeedEntry where
+ show e = concat
+ [ ""
+ , ""
+ , encodeHtml $ atomEntryLink e
+ , " "
+ , " "
+ , ""
+ , formatW3 $ atomEntryUpdated e
+ , " "
+ , ""
+ , encodeHtml $ atomEntryTitle e
+ , " "
+ , " "
+ , " "
+ ]
+
+formatW3 :: UTCTime -> String
+formatW3 = formatTime defaultTimeLocale "%FT%X-08:00"
+
+-- sitemaps
+data SitemapLoc = AbsLoc String | RelLoc String
+data SitemapChangeFreq = Always
+ | Hourly
+ | Daily
+ | Weekly
+ | Monthly
+ | Yearly
+ | Never
+instance Show SitemapChangeFreq where
+ show Always = "always"
+ show Hourly = "hourly"
+ show Daily = "daily"
+ show Weekly = "weekly"
+ show Monthly = "monthly"
+ show Yearly = "yearly"
+ show Never = "never"
+
+data SitemapUrl = SitemapUrl
+ { sitemapLoc :: SitemapLoc
+ , sitemapLastMod :: UTCTime
+ , sitemapChangeFreq :: SitemapChangeFreq
+ , priority :: Double
+ }
+data SitemapRequest = SitemapRequest String Int
+instance Request SitemapRequest where
+ parseRequest = do
+ env <- parseEnv
+ return $! SitemapRequest (Hack.serverName env)
+ (Hack.serverPort env)
+data SitemapResponse = SitemapResponse SitemapRequest [SitemapUrl]
+instance Show SitemapResponse where
+ show (SitemapResponse (SitemapRequest host port) urls) =
+ "\n" ++
+ "" ++
+ concatMap helper urls ++
+ " "
+ where
+ prefix = "http://" ++ host ++
+ case port of
+ 80 -> ""
+ _ -> ":" ++ show port
+ helper (SitemapUrl loc modTime freq pri) = concat
+ [ ""
+ , encodeHtml $ showLoc loc
+ , " "
+ , formatW3 modTime
+ , " "
+ , show freq
+ , " "
+ , show pri
+ , " "
+ ]
+ showLoc (AbsLoc s) = s
+ showLoc (RelLoc s) = prefix ++ s
+
+instance Response SitemapResponse where
+ reps res =
+ [ ("text/xml", response 200 [] $ show res)
+ ]
+
+sitemap :: IO [SitemapUrl] -> SitemapRequest -> IO SitemapResponse
+sitemap urls' req = do
+ urls <- urls'
+ return $ SitemapResponse req urls
+
+data GenResponse = HtmlResponse String
+ | ObjectResponse Object
+ | HtmlOrObjectResponse String Object
+ | RedirectResponse String
+ | PermissionDeniedResult String
+ | NotFoundResponse String
+instance Response GenResponse where
+ reps (HtmlResponse h) = [("text/html", response 200 [] h)]
+ reps (ObjectResponse t) = reps t
+ reps (HtmlOrObjectResponse h t) =
+ ("text/html", response 200 [] h) : reps t
+ reps (RedirectResponse url) = [("text/html", response 303 heads body)]
+ where
+ heads = [("Location", url)]
+ body = "Redirecting to " ++ encodeHtml url ++ "
"
+ reps (PermissionDeniedResult s) = [("text/plain", response 403 [] s)]
+ reps (NotFoundResponse s) = [("text/plain", response 404 [] s)]
+class ToObject a => ListDetail a where
+ htmlDetail :: a -> String
+ htmlDetail = treeToHtml . toObject
+ detailTitle :: a -> String
+ detailUrl :: a -> String
+ htmlList :: [a] -> String
+ htmlList l = "" ++ concatMap helper l ++ " "
+ where
+ helper i = "" ++ encodeHtml (detailTitle i) ++
+ " "
+ -- | Often times for the JSON response of the list, we don't need all
+ -- the information.
+ treeList :: [a] -> Object -- FIXME
+ treeList = Sequence . map treeListSingle
+ treeListSingle :: a -> Object
+ treeListSingle = toObject
+
+newtype ItemList a = ItemList [a]
+instance ListDetail a => Response (ItemList a) where
+ reps (ItemList l) =
+ [ ("text/html", response 200 [] $ htmlList l)
+ , ("application/json", response 200 [] $ treeToJson $ treeList l)
+ ]
+newtype ItemDetail a = ItemDetail a
+instance ListDetail a => Response (ItemDetail a) where
+ reps (ItemDetail i) =
+ [ ("text/html", response 200 [] $ htmlDetail i)
+ , ("application/json", response 200 [] $ treeToJson $ toObject i)
+ ]
+
+-- FIXME remove treeTo functions, replace with Object instances
+treeToJson :: Object -> String
+treeToJson (Scalar s) = '"' : encodeJson (fromStrictByteString s) ++ "\""
+treeToJson (Sequence l) =
+ "[" ++ intercalate "," (map treeToJson l) ++ "]"
+treeToJson (Mapping m) =
+ "{" ++ intercalate "," (map helper m) ++ "}" where
+ helper (k, v) =
+ treeToJson (Scalar k) ++
+ ":" ++
+ treeToJson v
+
+treeToHtml :: Object -> String
+treeToHtml (Scalar s) = encodeHtml $ fromStrictByteString s
+treeToHtml (Sequence l) =
+ "" ++ concatMap (\e -> "" ++ treeToHtml e ++ " ") l ++
+ " "
+treeToHtml (Mapping m) =
+ "" ++
+ concatMap (\(k, v) -> "" ++
+ encodeHtml (fromStrictByteString k) ++
+ " " ++
+ "" ++
+ treeToHtml v ++
+ " ") m ++
+ " "
+
+instance Response Object where
+ reps tree =
+ [ ("text/html", response 200 [] $ treeToHtml tree)
+ , ("application/json", response 200 [] $ treeToJson tree)
+ ]
diff --git a/Web/Restful/Utils.hs b/Web/Restful/Utils.hs
new file mode 100644
index 00000000..605a9c99
--- /dev/null
+++ b/Web/Restful/Utils.hs
@@ -0,0 +1,33 @@
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Utils
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Utility functions for Restful.
+--
+---------------------------------------------------------
+module Web.Restful.Utils
+ ( parseHttpAccept
+ , tryLookup
+ ) where
+
+import Data.List.Split (splitOneOf)
+
+parseHttpAccept :: String -> [String]
+parseHttpAccept = filter (not . specialHttpAccept) . splitOneOf ";,"
+
+specialHttpAccept :: String -> Bool
+specialHttpAccept ('q':'=':_) = True
+specialHttpAccept ('*':_) = True
+specialHttpAccept _ = False
+
+tryLookup :: Eq k => v -> k -> [(k, v)] -> v
+tryLookup v _ [] = v
+tryLookup v k ((k', v'):rest)
+ | k == k' = v'
+ | otherwise = tryLookup v k rest
diff --git a/restful.cabal b/restful.cabal
index 26910f74..dacf32c7 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -28,6 +28,12 @@ library
bytestring >= 0.9.1.4,
bytestring-class,
web-encodings,
- mtl >= 1.1.0.2
- exposed-modules: Web.Restful
+ mtl >= 1.1.0.2,
+ data-object
+ exposed-modules: Web.Restful,
+ Web.Restful.Constants,
+ Web.Restful.Request,
+ Web.Restful.Response,
+ Web.Restful.Utils,
+ Web.Restful.Application
ghc-options: -Wall
From 24520b9b16b21de3da84fd1a4f87bbd8c53bf545 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Tue, 4 Aug 2009 10:02:58 +0300
Subject: [PATCH 005/624] Updated README to show future refactor goals.
---
README | 1 -
README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 93 insertions(+), 1 deletion(-)
delete mode 100644 README
create mode 100644 README.md
diff --git a/README b/README
deleted file mode 100644
index 4ccea4d9..00000000
--- a/README
+++ /dev/null
@@ -1 +0,0 @@
-A Restful front controller built on Hack.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..e84ad30e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,93 @@
+A Restful front controller built on Hack.
+
+Below is an overview of how the refactored code will hopefully turn out.
+
+# Terminology
+
+## Verb
+
+HTTP request methods. Possible values: GET, POST, PUT, DELETE. Please see
+http://rest.blueoxen.net/cgi-bin/wiki.pl?MinimumMethods. In sum:
+
+GET: Read only.
+PUT: Replace data on server.
+DELETE: Remove data from server.
+POST: Some form of update.
+
+FIXME Note: not all clients support PUT and DELETE. Therefore, we need a
+workaround. I will implement two fixes:
+
+1. X-HTTP-Method-Override header.
+2. Get parameter (ie, in the query string). This will be more useful for web forms.
+
+## Resource
+
+A Resource is a single URL. Each Resource can be addressed with the four verbs,
+though does not necesarily support all of them. There are a few different ways
+of passing parameters:
+
+1. URL parameter. For example, you can set up your application so that
+/articles/my_title/ will hand off to the article resource with a parameter of
+my_title. The idea here is to avoid the regexs present in most other
+frameworks and provide a uniform interface for all parameters.
+
+2. Get parameter, ie query string.
+
+3. Post parameter, ie content body. This is *not* available for GET requests.
+However, it *is* available for PUT and DELETE.
+
+4. Headers, including cookies.
+
+### ResourceName
+
+As a side point to the URL parameter mentioned above: let us say you have
+multiple resources like /articles/my_title/ and /articles/other_title/. You
+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
+own ResourceName.
+
+## RawRequest
+
+The parsed data sent from the client. Has, for example, GET and POST
+parameters, not QUERY_STRING and CONTENT_BODY.
+
+## Request
+
+Request is actually a **type class** for parsing a RawRequest. This allows
+putting all form handling code and the like into a single place. Your handler
+code (see below) need only deal with your data type.
+
+## Representation
+
+Some method of showing data to the client. These are identified by MIME types.
+For the most part, you should be using hierarchichal data (see Data.Object).
+There are some exceptions, such as:
+
+* HTML for non-Ajax clients (search engines, old browsers).
+* Atom/RSS feeds.
+* Sitemaps.
+
+Whenever possible, use Data.Object, as this will automatically handle some
+basic representations (JSON, JSONP, Yaml, XML, undecorated HTML).
+
+## Response
+
+Contains basic HTTP response information and something which can be represented
+in different ways.
+
+## Handler
+
+There is a single Handler for each combination of ResourceName and Verb. A
+Handler takes some instance of Request and returns a Response.
+
+# Static files
+
+All static files should go under the /static/ path. A typical application will
+probably have a Javascript file in there which deals directly with the entire
+Restful API.
+
+# Non-Ajax clients
+
+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
+component to this framework which can automate some of that process.
From 543b15d7688ff98bcf134cdc01604f4b82d81292 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Tue, 4 Aug 2009 10:05:02 +0300
Subject: [PATCH 006/624] Added method override middleware
---
Hack/Middleware/MethodOverride.hs | 37 +++++++++++++++++++++++++++++++
README.md | 9 +++++---
Web/Restful/Application.hs | 8 ++++++-
3 files changed, 50 insertions(+), 4 deletions(-)
create mode 100644 Hack/Middleware/MethodOverride.hs
diff --git a/Hack/Middleware/MethodOverride.hs b/Hack/Middleware/MethodOverride.hs
new file mode 100644
index 00000000..940c168d
--- /dev/null
+++ b/Hack/Middleware/MethodOverride.hs
@@ -0,0 +1,37 @@
+---------------------------------------------------------
+-- |
+-- Module : Hack.Middleware.MethodOverride
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Unstable
+-- Portability : portable
+--
+-- Override the HTTP method based on either:
+-- The X-HTTP-Method-Override header.
+-- The _method_override GET parameter.
+--
+---------------------------------------------------------
+module Hack.Middleware.MethodOverride (methodOverride) where
+
+import Hack
+import Web.Encodings (decodeUrlPairs)
+import Data.Monoid (mappend)
+
+methodOverride :: Middleware
+methodOverride app env = do
+ let mo1 = lookup "X-HTTP-Method-Override" $ http env
+ gets = decodeUrlPairs $ queryString env
+ mo2 = lookup "_method_override" gets
+ cm = requestMethod env
+ app $
+ case mo1 `mappend` mo2 of
+ Nothing -> env
+ Just nm -> env { requestMethod = safeRead cm nm }
+
+safeRead :: Read a => a -> String -> a
+safeRead d s =
+ case reads s of
+ ((x, _):_) -> x
+ [] -> d
diff --git a/README.md b/README.md
index e84ad30e..21c4cc8d 100644
--- a/README.md
+++ b/README.md
@@ -14,11 +14,14 @@ PUT: Replace data on server.
DELETE: Remove data from server.
POST: Some form of update.
-FIXME Note: not all clients support PUT and DELETE. Therefore, we need a
-workaround. I will implement two fixes:
+Note: not all clients support PUT and DELETE. Therefore, we need a
+workaround. There are two fixes:
1. X-HTTP-Method-Override header.
-2. Get parameter (ie, in the query string). This will be more useful for web forms.
+2. Get parameter _method_override (ie, in the query string). This will be more
+useful for web forms.
+
+See MethodOverride middleware.
## Resource
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index 84c30672..dc9679fa 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -47,6 +47,7 @@ import Hack.Middleware.Gzip
import Hack.Middleware.CleanPath
import Hack.Middleware.Jsonp
import Hack.Middleware.ClientSession
+import Hack.Middleware.MethodOverride
import Control.Applicative ((<$>), Applicative (..))
import Control.Arrow (second)
@@ -80,7 +81,12 @@ instance Default ApplicationSettings where
, rpxnowApiKey = Nothing
, encryptKey = Left defaultKeyFile
, urlRewriter = \s -> (s, [])
- , hackMiddleware = [gzip, cleanPath, jsonp]
+ , hackMiddleware =
+ [ gzip
+ , cleanPath
+ , jsonp
+ , methodOverride
+ ]
, response404 = default404
, htmlWrapper = id
}
From c4eb5e1ee71fdd6de3fda79cdb6a1b183c21328a Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Tue, 4 Aug 2009 15:52:21 +0300
Subject: [PATCH 007/624] Replaced UrlRewriter with HandlerParser
---
.gitignore | 2 +
README.md | 19 ++++++
Web/Restful.hs | 2 +
Web/Restful/Application.hs | 132 ++++++++++++++++++-------------------
Web/Restful/Constants.hs | 4 +-
Web/Restful/Definitions.hs | 47 +++++++++++++
Web/Restful/Handler.hs | 36 ++++++++++
restful.cabal | 4 +-
8 files changed, 178 insertions(+), 68 deletions(-)
create mode 100644 Web/Restful/Definitions.hs
create mode 100644 Web/Restful/Handler.hs
diff --git a/.gitignore b/.gitignore
index 39b806f8..678893b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
dist
*.swp
+*.hi
+*.o
diff --git a/README.md b/README.md
index 21c4cc8d..6b699ebc 100644
--- a/README.md
+++ b/README.md
@@ -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
own ResourceName.
+### ResourceParser
+
+A ResourceParser converts a Resource (ie, a URL) to a ResourceName and URL
+parameters.
+
## RawRequest
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
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
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
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.
+
+# Passing global data
+
+You should use function currying to pass around global information (the list of
+entries in a blog, a database connection, etc).
diff --git a/Web/Restful.hs b/Web/Restful.hs
index f4925cad..c2878f2c 100644
--- a/Web/Restful.hs
+++ b/Web/Restful.hs
@@ -17,9 +17,11 @@ module Web.Restful
, module Web.Restful.Request
, module Web.Restful.Response
, module Web.Restful.Application
+ , module Web.Restful.Definitions
) where
import Data.Object
import Web.Restful.Request
import Web.Restful.Response
import Web.Restful.Application
+import Web.Restful.Definitions
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index dc9679fa..f03f3d43 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE ScopedTypeVariables #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Application
@@ -22,10 +23,12 @@ module Web.Restful.Application
-- ** Settings
, setHandler
, setRpxnowApiKey
- , setUrlRewriter
+ , setResourceParser
, setHtmlWrapper
-- ** Engage
, run
+ -- * FIXME
+ , Application (..)
) where
-- hideously long import list
@@ -56,31 +59,33 @@ import Web.Restful.Request
import Web.Restful.Response
import Web.Restful.Constants
import Web.Restful.Utils
+import Web.Restful.Handler
+import Web.Restful.Definitions
import Data.Object
-- | Contains settings and a list of resources.
-type ApplicationMonad = StateT ApplicationSettings (Writer [Resource])
-instance Applicative ApplicationMonad where
+type ApplicationMonad a = StateT (ApplicationSettings a) (Writer (HandlerMap a))
+instance Applicative (ApplicationMonad a) where
pure = return
f <*> a = do
f' <- f
a' <- a
return $! f' a'
-data ApplicationSettings = ApplicationSettings
+data ApplicationSettings rn = ApplicationSettings
{ hackHandler :: Hack.Application -> IO ()
, rpxnowApiKey :: Maybe String
, encryptKey :: Either FilePath Word256
- , urlRewriter :: UrlRewriter
+ , appResourceParser :: ResourceParser rn
, hackMiddleware :: [Hack.Middleware]
, response404 :: Hack.Env -> IO Hack.Response
, htmlWrapper :: BS.ByteString -> BS.ByteString
}
-instance Default ApplicationSettings where
+instance ResourceName a => Default (ApplicationSettings a) where
def = ApplicationSettings
{ hackHandler = Hack.Handler.CGI.run
, rpxnowApiKey = Nothing
, encryptKey = Left defaultKeyFile
- , urlRewriter = \s -> (s, [])
+ , appResourceParser = \s -> ParsedResource (toResourceName s) []
, hackMiddleware =
[ gzip
, cleanPath
@@ -98,44 +103,32 @@ default404 env = return $
[("Content-Type", "text/plain")]
$ 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
-addResource :: (Request req, Response res)
- => [Hack.RequestMethod]
- -> PathInfo
+addResource :: (Request req, Response res, ResourceName rn)
+ => Verb
+ -> rn
-> (req -> IO res)
- -> ApplicationMonad ()
-addResource methods path f =
- tell [Resource methods path $ liftHandler $ Handler f]
+ -> ApplicationMonad rn ()
+addResource verb resourceName' f = do
+ let handler :: Handler
+ handler = Handler $ (fmap ResponseWrapper) . f
+ handlerDesc = HandlerDesc resourceName' verb handler
+ tell [handlerDesc]
-setUrlRewriter :: UrlRewriter -> ApplicationMonad ()
-setUrlRewriter newUrlRewriter = do
+setResourceParser :: ResourceName rn
+ => ResourceParser rn
+ -> ApplicationMonad rn ()
+setResourceParser newRP = do
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
s <- get
put $ s { htmlWrapper = f }
-run :: ApplicationMonad () -> IO ()
+run :: ResourceName a => ApplicationMonad a () -> IO ()
run m = do
let (settings, resources') = runWriter $ execStateT m def
key <- case encryptKey settings of
@@ -143,36 +136,38 @@ run m = do
Right k -> return k
let defApp = defaultResources settings
defResources = execWriter $ execStateT defApp def
- resources = resources' ++ defResources
+ resources = resources' ++ defResources -- FIXME rename HandlerDescs
app' :: Hack.Application
- app' = makeApplication' resources settings
+ app' = toHackApplication $ Application resources settings
clientsession' :: Hack.Middleware
clientsession' = clientsession [authCookieName] key
app :: Hack.Application
app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
hackHandler settings app
-setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad ()
+setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad a ()
setHandler h = do
settings <- get
put $ settings { hackHandler = h }
-setRpxnowApiKey :: String -> ApplicationMonad ()
+setRpxnowApiKey :: String -> ApplicationMonad a ()
setRpxnowApiKey k = do
settings <- get
put $ settings { rpxnowApiKey = Just k }
-defaultResources :: ApplicationSettings -> ApplicationMonad ()
+defaultResources :: ResourceName rn
+ => ApplicationSettings rn
+ -> ApplicationMonad rn ()
defaultResources settings = do
- addResource [Hack.GET] ["auth", "check"] authCheck
- addResource [Hack.GET] ["auth", "logout"] authLogout
- addResource [Hack.GET] ["auth", "openid"] authOpenidForm
- addResource [Hack.GET] ["auth", "openid", "forward"] authOpenidForward
- addResource [Hack.GET] ["auth", "openid", "complete"] authOpenidComplete
+ addResource Get (toResourceName ["auth", "check"]) authCheck
+ addResource Get (toResourceName ["auth", "logout"]) authLogout
+ addResource Get (toResourceName ["auth", "openid"]) authOpenidForm
+ addResource Get (toResourceName ["auth", "openid", "forward"]) authOpenidForward
+ addResource Get (toResourceName ["auth", "openid", "complete"]) authOpenidComplete
case rpxnowApiKey settings of
Nothing -> return ()
Just key -> do
- addResource [Hack.GET] ["auth", "login", "rpxnow"] $
+ addResource Get (toResourceName ["auth", "login", "rpxnow"]) $
rpxnowLogin key
data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
@@ -314,22 +309,24 @@ instance Response LogoutResponse where
Hack.Response s (h':h) c
h' = resetCookie authCookieName
-makeApplication' :: [Resource]
- -> ApplicationSettings
- -> Hack.Env
- -> IO Hack.Response
-makeApplication' resources settings env = do
- let method = Hack.requestMethod env
- rr = envToRawRequest (urlRewriter settings) env
- path' = rawPathInfo rr
- isValid :: Resource -> Bool
- isValid (Resource methods path _) = method `elem` methods
- && path == path'
- case filter isValid resources of
- [Resource _ _ handler] -> do
+toHackApplication :: Eq resourceName
+ => Application resourceName
+ -> Hack.Application
+toHackApplication (Application hm settings) env = do
+ let (Right resource) = splitPath $ Hack.pathInfo env
+ (ParsedResource rn urlParams') = (appResourceParser settings) resource
+ verb :: Verb
+ verb = toVerb $ Hack.requestMethod env
+ rr :: RawRequest
+ rr = envToRawRequest urlParams' env
+ matchingHandler (HandlerDesc resourceName' verb' _) =
+ rn == resourceName' &&
+ verb == verb'
+ case filter matchingHandler hm of
+ [HandlerDesc _ _ handler] -> do
let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
ctypes' = parseHttpAccept rawHttpAccept
- body <- handler rr
+ body <- runHandler handler rr
let reps' = reps body
ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
let handlerPair =
@@ -351,13 +348,11 @@ makeApplication' resources settings env = do
(("Content-Type", ctype) : headers)
$ toLazyByteString $ wrapper content
[] -> response404 settings $ env
- _ -> fail "Overlapping handlers"
+ _ -> fail $ "Overlapping handlers for: " ++ show env
-type UrlRewriter = PathInfo -> (PathInfo, [(String, String)])
-envToRawRequest :: UrlRewriter -> Hack.Env -> RawRequest
-envToRawRequest rewriter env =
+envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
+envToRawRequest urlParams' env =
let (Right rawPieces) = splitPath $ Hack.pathInfo env
- (pi', urls) = rewriter rawPieces
gets' = decodeUrlPairs $ Hack.queryString env :: [(String, String)]
clength = tryLookup "0" "Content-Length" $ Hack.http env
ctype = tryLookup "" "Content-Type" $ Hack.http env
@@ -365,4 +360,9 @@ envToRawRequest rewriter env =
$ Hack.hackInput env
rawCookie = tryLookup "" "Cookie" $ Hack.http env
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
+ }
diff --git a/Web/Restful/Constants.hs b/Web/Restful/Constants.hs
index c39aa532..e6445b54 100644
--- a/Web/Restful/Constants.hs
+++ b/Web/Restful/Constants.hs
@@ -11,7 +11,9 @@
-- Constants used throughout Restful.
--
---------------------------------------------------------
-module Web.Restful.Constants where
+module Web.Restful.Constants
+ ( authCookieName
+ ) where
authCookieName :: String
authCookieName = "IDENTIFIER"
diff --git a/Web/Restful/Definitions.hs b/Web/Restful/Definitions.hs
new file mode 100644
index 00000000..f96ca895
--- /dev/null
+++ b/Web/Restful/Definitions.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE FlexibleInstances #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Definitions
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- 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
diff --git a/Web/Restful/Handler.hs b/Web/Restful/Handler.hs
new file mode 100644
index 00000000..100440ad
--- /dev/null
+++ b/Web/Restful/Handler.hs
@@ -0,0 +1,36 @@
+{-# LANGUAGE ExistentialQuantification #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Handler
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- 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]
diff --git a/restful.cabal b/restful.cabal
index dacf32c7..7a10957a 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -1,5 +1,5 @@
name: restful
-version: 0.0.0
+version: 0.1.0
license: BSD3
license-file: LICENSE
author: Michael Snoyman
@@ -35,5 +35,7 @@ library
Web.Restful.Request,
Web.Restful.Response,
Web.Restful.Utils,
+ Web.Restful.Definitions,
+ Web.Restful.Handler,
Web.Restful.Application
ghc-options: -Wall
From cb963a6231cad41dc3a773a707e2ff0ebd6e4f9a Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Wed, 5 Aug 2009 13:32:01 +0300
Subject: [PATCH 008/624] Vastly simplified Web.Restful.Application
---
Data/Object/Instances.hs | 93 +++++++++++
Web/Restful.hs | 27 ---
Web/Restful/Application.hs | 321 +++++++-----------------------------
Web/Restful/Definitions.hs | 15 +-
Web/Restful/Handler.hs | 16 +-
Web/Restful/Helpers/Auth.hs | 216 ++++++++++++++++++++++++
Web/Restful/Response.hs | 1 +
restful.cabal | 8 +-
8 files changed, 393 insertions(+), 304 deletions(-)
create mode 100644 Data/Object/Instances.hs
delete mode 100644 Web/Restful.hs
create mode 100644 Web/Restful/Helpers/Auth.hs
diff --git a/Data/Object/Instances.hs b/Data/Object/Instances.hs
new file mode 100644
index 00000000..6f2301ef
--- /dev/null
+++ b/Data/Object/Instances.hs
@@ -0,0 +1,93 @@
+---------------------------------------------------------
+--
+-- Module : Data.Object.Instances
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Instances for converting various types of data into Data.Object.Object.
+--
+---------------------------------------------------------
+module Data.Object.Instances
+ ( Json (..)
+ , Yaml (..)
+ , Html (..)
+ ) where
+
+import Data.Object
+import qualified Data.ByteString as B
+import Data.ByteString.Class
+import Web.Encodings (encodeJson)
+import qualified Text.Yaml as Y
+
+newtype Json = Json B.ByteString
+instance FromObject Json where
+ fromObject = return . Json . helper where
+ helper :: Object -> B.ByteString
+ helper (Scalar s) = B.concat
+ [ toStrictByteString "\""
+ , encodeJson $ fromStrictByteString s
+ , toStrictByteString "\""
+ ]
+ helper (Sequence s) = B.concat
+ [ toStrictByteString "["
+ , B.intercalate (toStrictByteString ",") $ map helper s
+ , toStrictByteString "]"
+ ]
+ helper (Mapping m) = B.concat
+ [ toStrictByteString "{"
+ , B.intercalate (toStrictByteString ",") $ map helper2 m
+ , toStrictByteString "}"
+ ]
+ helper2 :: (B.ByteString, Object) -> B.ByteString
+ helper2 (k, v) = B.concat
+ [ toStrictByteString "\""
+ , encodeJson $ fromStrictByteString k
+ , toStrictByteString "\":"
+ , helper v
+ ]
+
+newtype Yaml = Yaml B.ByteString
+instance FromObject Yaml where
+ fromObject = return . Yaml . Y.encode
+
+-- | Represents as an entire HTML 5 document by using the following:
+--
+-- * A scalar is a paragraph.
+-- * A sequence is an unordered list.
+-- * A mapping is a definition list.
+newtype Html = Html B.ByteString
+
+instance FromObject Html where
+ fromObject o = return $ Html $ B.concat
+ [ toStrictByteString "\n"
+ , helper o
+ , toStrictByteString ""
+ ] where
+ helper :: Object -> B.ByteString
+ helper (Scalar s) = B.concat
+ [ toStrictByteString ""
+ , s
+ , toStrictByteString "
"
+ ]
+ helper (Sequence []) = toStrictByteString ""
+ helper (Sequence s) = B.concat
+ [ toStrictByteString ""
+ , B.intercalate (toStrictByteString " ") $ map helper s
+ , toStrictByteString " "
+ ]
+ helper (Mapping m) = B.concat $
+ toStrictByteString "" :
+ map helper2 m ++
+ [ toStrictByteString " " ]
+ helper2 :: (B.ByteString, Object) -> B.ByteString
+ helper2 (k, v) = B.concat $
+ [ toStrictByteString ""
+ , k
+ , toStrictByteString " "
+ , helper v
+ , toStrictByteString " "
+ ]
diff --git a/Web/Restful.hs b/Web/Restful.hs
deleted file mode 100644
index c2878f2c..00000000
--- a/Web/Restful.hs
+++ /dev/null
@@ -1,27 +0,0 @@
----------------------------------------------------------
---
--- Module : Web.Restful
--- Copyright : Michael Snoyman
--- License : BSD3
---
--- Maintainer : Michael Snoyman
--- Stability : Stable
--- Portability : portable
---
--- Lightweight framework for designing RESTful APIs.
---
----------------------------------------------------------
-module Web.Restful
- (
- module Data.Object
- , module Web.Restful.Request
- , module Web.Restful.Response
- , module Web.Restful.Application
- , module Web.Restful.Definitions
- ) where
-
-import Data.Object
-import Web.Restful.Request
-import Web.Restful.Response
-import Web.Restful.Application
-import Web.Restful.Definitions
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index f03f3d43..dbe63821 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -18,24 +18,14 @@ module Web.Restful.Application
(
-- * Defining an application
ApplicationMonad
- -- ** Routing
- , addResource
-- ** Settings
- , setHandler
- , setRpxnowApiKey
- , setResourceParser
, setHtmlWrapper
-- ** Engage
- , run
- -- * FIXME
- , Application (..)
+ , toHackApp
) where
-- hideously long import list
import qualified Hack
-import qualified Hack.Handler.CGI
-import Control.Monad.Reader
-import Control.Monad.Writer
import Control.Monad.State hiding (gets)
import Web.Encodings
import Data.Maybe (isJust)
@@ -43,8 +33,7 @@ import Data.ByteString.Class
import qualified Data.ByteString.Lazy as BS
import Data.Function.Predicate (equals)
import Data.Default
-import qualified Web.Authenticate.Rpxnow as Rpxnow
-import qualified Web.Authenticate.OpenId as OpenId
+import Control.Applicative ( Applicative (..))
import Hack.Middleware.Gzip
import Hack.Middleware.CleanPath
@@ -52,19 +41,15 @@ import Hack.Middleware.Jsonp
import Hack.Middleware.ClientSession
import Hack.Middleware.MethodOverride
-import Control.Applicative ((<$>), Applicative (..))
-import Control.Arrow (second)
-
import Web.Restful.Request
import Web.Restful.Response
-import Web.Restful.Constants
import Web.Restful.Utils
import Web.Restful.Handler
import Web.Restful.Definitions
-import Data.Object
+import Web.Restful.Constants
-- | Contains settings and a list of resources.
-type ApplicationMonad a = StateT (ApplicationSettings a) (Writer (HandlerMap a))
+type ApplicationMonad a = State (ApplicationSettings a)
instance Applicative (ApplicationMonad a) where
pure = return
f <*> a = do
@@ -72,20 +57,16 @@ instance Applicative (ApplicationMonad a) where
a' <- a
return $! f' a'
data ApplicationSettings rn = ApplicationSettings
- { hackHandler :: Hack.Application -> IO ()
- , rpxnowApiKey :: Maybe String
- , encryptKey :: Either FilePath Word256
- , appResourceParser :: ResourceParser rn
+ { encryptKey :: Either FilePath Word256
, hackMiddleware :: [Hack.Middleware]
, response404 :: Hack.Env -> IO Hack.Response
, htmlWrapper :: BS.ByteString -> BS.ByteString
}
-instance ResourceName a => Default (ApplicationSettings a) where
+
+instance (HasResourceParser a) =>
+ Default (ApplicationSettings a) where
def = ApplicationSettings
- { hackHandler = Hack.Handler.CGI.run
- , rpxnowApiKey = Nothing
- , encryptKey = Left defaultKeyFile
- , appResourceParser = \s -> ParsedResource (toResourceName s) []
+ { encryptKey = Left defaultKeyFile
, hackMiddleware =
[ gzip
, cleanPath
@@ -105,250 +86,65 @@ default404 env = return $
-- FIXME document below here
-addResource :: (Request req, Response res, ResourceName rn)
- => Verb
- -> rn
- -> (req -> IO res)
- -> ApplicationMonad rn ()
-addResource verb resourceName' f = do
- let handler :: Handler
- handler = Handler $ (fmap ResponseWrapper) . f
- handlerDesc = HandlerDesc resourceName' verb handler
- tell [handlerDesc]
-
-setResourceParser :: ResourceName rn
- => ResourceParser rn
- -> ApplicationMonad rn ()
-setResourceParser newRP = do
- s <- get
- put $ s { appResourceParser = newRP }
-
setHtmlWrapper :: (BS.ByteString -> BS.ByteString) -> ApplicationMonad a ()
setHtmlWrapper f = do
s <- get
put $ s { htmlWrapper = f }
-run :: ResourceName a => ApplicationMonad a () -> IO ()
-run m = do
- let (settings, resources') = runWriter $ execStateT m def
+toHackApp :: (Eq a, HasResourceParser a, HasHandlers a b)
+ => ApplicationMonad a ()
+ -> b
+ -> IO Hack.Application
+toHackApp am model = do
+ let settings = execState am def
key <- case encryptKey settings of
Left f -> getKey f
Right k -> return k
- let defApp = defaultResources settings
- defResources = execWriter $ execStateT defApp def
- resources = resources' ++ defResources -- FIXME rename HandlerDescs
- app' :: Hack.Application
- app' = toHackApplication $ Application resources settings
- clientsession' :: Hack.Middleware
- clientsession' = clientsession [authCookieName] key
- app :: Hack.Application
+ let handlers = getHandler model
+ app' = toHackApplication handlers settings
+ clientsession' = clientsession [authCookieName] key -- FIXME gotta be a better way...
app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
- hackHandler settings app
+ return app
-setHandler :: (Hack.Application -> IO ()) -> ApplicationMonad a ()
-setHandler h = do
- settings <- get
- put $ settings { hackHandler = h }
-
-setRpxnowApiKey :: String -> ApplicationMonad a ()
-setRpxnowApiKey k = do
- settings <- get
- put $ settings { rpxnowApiKey = Just k }
-
-defaultResources :: ResourceName rn
- => ApplicationSettings rn
- -> ApplicationMonad rn ()
-defaultResources settings = do
- addResource Get (toResourceName ["auth", "check"]) authCheck
- addResource Get (toResourceName ["auth", "logout"]) authLogout
- addResource Get (toResourceName ["auth", "openid"]) authOpenidForm
- addResource Get (toResourceName ["auth", "openid", "forward"]) authOpenidForward
- addResource Get (toResourceName ["auth", "openid", "complete"]) authOpenidComplete
- case rpxnowApiKey settings of
- Nothing -> return ()
- Just key -> do
- addResource Get (toResourceName ["auth", "login", "rpxnow"]) $
- rpxnowLogin key
-
-data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
-instance Request OIDFormReq where
- parseRequest = OIDFormReq <$> getParam "message" <*> getParam "dest"
-instance Show OIDFormReq where
- show (OIDFormReq Nothing _) = ""
- show (OIDFormReq (Just s) _) = "" ++ encodeHtml s ++
- "
"
-data OIDFormRes = OIDFormRes String (Maybe String)
-instance Response OIDFormRes where
- reps (OIDFormRes s dest) = [("text/html", response 200 heads s)]
- where
- heads =
- case dest of
- Nothing -> []
- Just dest' ->
- [("Set-Cookie", "DEST=" ++ dest' ++ "; path=/")]
-authOpenidForm :: OIDFormReq -> IO OIDFormRes
-authOpenidForm m@(OIDFormReq _ dest) =
- let html =
- show m ++
- ""
- in return $! OIDFormRes html dest
-data OIDFReq = OIDFReq String String
-instance Request OIDFReq where
- parseRequest = do
- oid <- getParam "openid"
- env <- parseEnv
- let complete = "http://" ++ Hack.serverName env ++ ":" ++
- show (Hack.serverPort env) ++
- "/auth/openid/complete/"
- return $! OIDFReq oid complete
-authOpenidForward :: OIDFReq -> IO GenResponse
-authOpenidForward (OIDFReq oid complete) = do
- res <- OpenId.getForwardUrl oid complete :: IO (Either String String)
- return $
- case res of
- Left err -> RedirectResponse $ "/auth/openid/?message=" ++
- encodeUrl err
- Right url -> RedirectResponse url
-
-data OIDComp = OIDComp [(String, String)] (Maybe String)
-instance Request OIDComp where
- parseRequest = do
- rr <- ask
- let gets = rawGetParams rr
- dest <- cookieParam "DEST"
- return $! OIDComp gets dest
-data OIDCompRes = OIDCompResErr String
- | OIDCompResGood String (Maybe String)
-instance Response OIDCompRes where
- reps (OIDCompResErr err) =
- reps $ RedirectResponse
- $ "/auth/openid/?message=" ++
- encodeUrl err
- reps (OIDCompResGood ident Nothing) =
- reps $ OIDCompResGood ident (Just "/")
- reps (OIDCompResGood ident (Just dest)) =
- [("text/plain", response 303 heads "")] where
- heads =
- [ (authCookieName, ident)
- , resetCookie "DEST"
- , ("Location", dest)
- ]
-
-resetCookie :: String -> (String, String)
-resetCookie name =
- ("Set-Cookie",
- name ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
-
-authOpenidComplete :: OIDComp -> IO OIDCompRes
-authOpenidComplete (OIDComp gets' dest) = do
- res <- OpenId.authenticate gets' :: IO (Either String OpenId.Identifier)
- return $
- case res of
- Left err -> OIDCompResErr err
- Right (OpenId.Identifier ident) -> OIDCompResGood ident dest
-
--- | token dest
-data RpxnowRequest = RpxnowRequest String (Maybe String)
-instance Request RpxnowRequest where
- parseRequest = do
- token <- getParam "token"
- dest <- getParam "dest"
- return $! RpxnowRequest token $ chopHash `fmap` dest
-
-chopHash :: String -> String
-chopHash ('#':rest) = rest
-chopHash x = x
-
--- | dest identifier
-data RpxnowResponse = RpxnowResponse String (Maybe String)
-instance Response RpxnowResponse where
- reps (RpxnowResponse dest Nothing) =
- [("text/html", response 303 [("Location", dest)] "")]
- reps (RpxnowResponse dest (Just ident)) =
- [("text/html", response 303
- [ ("Location", dest)
- , (authCookieName, ident)
- ]
- "")]
-
-rpxnowLogin :: String -- ^ api key
- -> RpxnowRequest
- -> IO RpxnowResponse
-rpxnowLogin apiKey (RpxnowRequest token dest') = do
- let dest = case dest' of
- Nothing -> "/"
- Just "" -> "/"
- Just s -> s
- ident' <- Rpxnow.authenticate apiKey token
- return $ RpxnowResponse dest (Rpxnow.identifier `fmap` ident')
-
-data AuthRequest = AuthRequest (Maybe String)
-instance Request AuthRequest where
- parseRequest = AuthRequest `fmap` identifier
-
-authCheck :: AuthRequest -> IO Object
-authCheck (AuthRequest Nothing) =
- return $ toObject [("status", "notloggedin")]
-authCheck (AuthRequest (Just i)) =
- return $ toObject
- [ ("status", "loggedin")
- , ("ident", i)
- ]
-
-authLogout :: () -> IO LogoutResponse
-authLogout _ = return LogoutResponse
-
-data LogoutResponse = LogoutResponse
-instance Response LogoutResponse where
- reps _ = map (second addCookie) $ reps tree where
- tree = toObject [("status", "loggedout")]
- addCookie (Hack.Response s h c) =
- Hack.Response s (h':h) c
- h' = resetCookie authCookieName
-
-toHackApplication :: Eq resourceName
- => Application resourceName
+toHackApplication :: (HasResourceParser resourceName, Eq resourceName)
+ => HandlerMap resourceName
+ -> ApplicationSettings resourceName
-> Hack.Application
-toHackApplication (Application hm settings) env = do
+toHackApplication hm settings env = do
let (Right resource) = splitPath $ Hack.pathInfo env
- (ParsedResource rn urlParams') = (appResourceParser settings) resource
- verb :: Verb
- verb = toVerb $ Hack.requestMethod env
- rr :: RawRequest
- rr = envToRawRequest urlParams' env
- matchingHandler (HandlerDesc resourceName' verb' _) =
- rn == resourceName' &&
- verb == verb'
- case filter matchingHandler hm of
- [HandlerDesc _ _ handler] -> do
- let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
- ctypes' = parseHttpAccept rawHttpAccept
- body <- runHandler handler rr
- let reps' = reps body
- ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
- let handlerPair =
- case ctypes of
- [] -> Just $ head reps'
- (c:_) ->
- case filter (fst `equals` c) reps' of
- [pair] -> Just pair
- [] -> Nothing
- _ -> error "Overlapping reps"
- case handlerPair of
- Nothing -> response404 settings $ env
- Just (ctype, Hack.Response status headers content) -> do
- let wrapper =
- case ctype of
- "text/html" -> htmlWrapper settings
- _ -> id
- return $ Hack.Response status
- (("Content-Type", ctype) : headers)
- $ toLazyByteString $ wrapper content
- [] -> response404 settings $ env
- _ -> fail $ "Overlapping handlers for: " ++ show env
+ case resourceParser resource of
+ Nothing -> response404 settings $ env
+ (Just (ParsedResource rn urlParams')) -> do
+ let verb :: Verb
+ verb = toVerb $ Hack.requestMethod env
+ rr :: RawRequest
+ rr = envToRawRequest urlParams' env
+ case hm rn verb of
+ (Just handler) -> do
+ let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
+ ctypes' = parseHttpAccept rawHttpAccept
+ body <- runHandler handler rr
+ let reps' = reps body
+ ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
+ let handlerPair =
+ case ctypes of
+ [] -> Just $ head reps'
+ (c:_) ->
+ case filter (fst `equals` c) reps' of
+ [pair] -> Just pair
+ [] -> Nothing
+ _ -> error "Overlapping reps"
+ case handlerPair of
+ Nothing -> response404 settings $ env
+ Just (ctype, Hack.Response status headers content) -> do
+ let wrapper =
+ case ctype of
+ "text/html" -> htmlWrapper settings
+ _ -> id
+ return $ Hack.Response status
+ (("Content-Type", ctype) : headers)
+ $ toLazyByteString $ wrapper content
+ Nothing -> response404 settings $ env
envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
envToRawRequest urlParams' env =
@@ -361,8 +157,3 @@ envToRawRequest urlParams' env =
rawCookie = tryLookup "" "Cookie" $ Hack.http env
cookies' = decodeCookies rawCookie :: [(String, String)]
in RawRequest rawPieces urlParams' gets' posts cookies' files env
-
-data Application a = Application
- { handlerMap :: HandlerMap a
- , applicationSettings :: ApplicationSettings a
- }
diff --git a/Web/Restful/Definitions.hs b/Web/Restful/Definitions.hs
index f96ca895..12a6aab1 100644
--- a/Web/Restful/Definitions.hs
+++ b/Web/Restful/Definitions.hs
@@ -1,4 +1,5 @@
{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Definitions
@@ -18,7 +19,7 @@ module Web.Restful.Definitions
, Resource
, ParsedResource (..)
, ResourceParser
- , ResourceName (..)
+ , HasResourceParser (..)
) where
import qualified Hack
@@ -34,14 +35,14 @@ 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
+type ResourceParser a = Resource -> Maybe (ParsedResource a)
+
+class HasResourceParser a where
+ resourceParser :: ResourceParser a
+ simpleParse :: a -> Maybe (ParsedResource a)
+ simpleParse x = Just $ ParsedResource x []
diff --git a/Web/Restful/Handler.hs b/Web/Restful/Handler.hs
index 100440ad..980ff848 100644
--- a/Web/Restful/Handler.hs
+++ b/Web/Restful/Handler.hs
@@ -1,4 +1,6 @@
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FunctionalDependencies #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Handler
@@ -16,7 +18,8 @@ module Web.Restful.Handler
( Handler (..)
, runHandler
, HandlerMap
- , HandlerDesc (..)
+ , HasHandlers (..)
+ , liftHandler
) where
import Web.Restful.Definitions
@@ -32,5 +35,12 @@ runHandler (Handler f) rreq = do
Left errors -> fail $ unlines errors -- FIXME
Right req -> f req
-data HandlerDesc a = HandlerDesc a Verb Handler
-type HandlerMap a = [HandlerDesc a]
+type HandlerMap a = a -> Verb -> Maybe Handler
+
+class HasHandlers a b | a -> b where
+ getHandler :: b -> a -> Verb -> Maybe Handler
+
+liftHandler :: (Request req, Response res)
+ => (req -> IO res)
+ -> Maybe Handler
+liftHandler f = Just . Handler $ fmap ResponseWrapper . f
diff --git a/Web/Restful/Helpers/Auth.hs b/Web/Restful/Helpers/Auth.hs
new file mode 100644
index 00000000..44cd26e8
--- /dev/null
+++ b/Web/Restful/Helpers/Auth.hs
@@ -0,0 +1,216 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Helpers.Auth
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Authentication through the authentication package.
+--
+---------------------------------------------------------
+module Web.Restful.Helpers.Auth
+ ( AuthResource
+ , FromAuthResource (..)
+ , authResourceParser
+ ) where
+
+import qualified Hack
+import Web.Encodings
+import qualified Web.Authenticate.Rpxnow as Rpxnow
+import qualified Web.Authenticate.OpenId as OpenId
+
+import Web.Restful
+import Web.Restful.Constants
+
+import Control.Applicative ((<$>), Applicative (..))
+import Control.Arrow (second)
+import Control.Monad.Reader
+
+import Data.Object
+
+data AuthResource =
+ AuthCheck
+ | AuthLogout
+ | AuthOpenid
+ | AuthOpenidForward
+ | AuthOpenidComplete
+ | AuthLoginRpxnow
+ deriving Eq
+class FromAuthResource a where
+ fromAuthResource :: AuthResource -> a
+
+authResourceParser :: FromAuthResource far
+ => Resource
+ -> Maybe (ParsedResource far)
+authResourceParser ["check"] =
+ authResourceParser' AuthCheck
+authResourceParser ["logout"] =
+ authResourceParser' AuthLogout
+authResourceParser ["openid"] =
+ authResourceParser' AuthOpenid
+authResourceParser ["openid", "forward"] =
+ authResourceParser' AuthOpenidForward
+authResourceParser ["openid", "complete"] =
+ authResourceParser' AuthOpenidComplete
+authResourceParser ["login", "rpxnow"] =
+ authResourceParser' AuthLoginRpxnow
+authResourceParser _ = Nothing
+
+authResourceParser' :: FromAuthResource far
+ => AuthResource
+ -> Maybe (ParsedResource far)
+authResourceParser' x = Just $ ParsedResource (fromAuthResource x) []
+
+type RpxnowApiKey = String -- FIXME newtype
+instance HasHandlers AuthResource (Maybe RpxnowApiKey) where
+ getHandler _ AuthCheck Get = liftHandler authCheck
+ getHandler _ AuthLogout Get = liftHandler authLogout
+ getHandler _ AuthOpenid Get = liftHandler authOpenidForm
+ getHandler _ AuthOpenidForward Get = liftHandler authOpenidForward
+ getHandler _ AuthOpenidComplete Get = liftHandler authOpenidComplete
+ getHandler (Just key) AuthLoginRpxnow Get = liftHandler $ rpxnowLogin key
+ getHandler _ _ _ = Nothing
+
+data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
+instance Request OIDFormReq where
+ parseRequest = OIDFormReq <$> getParam "message" <*> getParam "dest"
+instance Show OIDFormReq where
+ show (OIDFormReq Nothing _) = ""
+ show (OIDFormReq (Just s) _) = "" ++ encodeHtml s ++
+ "
"
+data OIDFormRes = OIDFormRes String (Maybe String)
+instance Response OIDFormRes where
+ reps (OIDFormRes s dest) = [("text/html", response 200 heads s)]
+ where
+ heads =
+ case dest of
+ Nothing -> []
+ Just dest' ->
+ [("Set-Cookie", "DEST=" ++ dest' ++ "; path=/")]
+authOpenidForm :: OIDFormReq -> IO OIDFormRes
+authOpenidForm m@(OIDFormReq _ dest) =
+ let html =
+ show m ++
+ ""
+ in return $! OIDFormRes html dest
+data OIDFReq = OIDFReq String String
+instance Request OIDFReq where
+ parseRequest = do
+ oid <- getParam "openid"
+ env <- parseEnv
+ let complete = "http://" ++ Hack.serverName env ++ ":" ++
+ show (Hack.serverPort env) ++
+ "/auth/openid/complete/"
+ return $! OIDFReq oid complete
+authOpenidForward :: OIDFReq -> IO GenResponse
+authOpenidForward (OIDFReq oid complete) = do
+ res <- OpenId.getForwardUrl oid complete :: IO (Either String String)
+ return $
+ case res of
+ Left err -> RedirectResponse $ "/auth/openid/?message=" ++
+ encodeUrl err
+ Right url -> RedirectResponse url
+
+data OIDComp = OIDComp [(String, String)] (Maybe String)
+instance Request OIDComp where
+ parseRequest = do
+ rr <- ask
+ let gets = rawGetParams rr
+ dest <- cookieParam "DEST"
+ return $! OIDComp gets dest
+data OIDCompRes = OIDCompResErr String
+ | OIDCompResGood String (Maybe String)
+instance Response OIDCompRes where
+ reps (OIDCompResErr err) =
+ reps $ RedirectResponse
+ $ "/auth/openid/?message=" ++
+ encodeUrl err
+ reps (OIDCompResGood ident Nothing) =
+ reps $ OIDCompResGood ident (Just "/")
+ reps (OIDCompResGood ident (Just dest)) =
+ [("text/plain", response 303 heads "")] where
+ heads =
+ [ (authCookieName, ident)
+ , resetCookie "DEST"
+ , ("Location", dest)
+ ]
+
+resetCookie :: String -> (String, String)
+resetCookie name =
+ ("Set-Cookie",
+ name ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
+
+authOpenidComplete :: OIDComp -> IO OIDCompRes
+authOpenidComplete (OIDComp gets' dest) = do
+ res <- OpenId.authenticate gets' :: IO (Either String OpenId.Identifier)
+ return $
+ case res of
+ Left err -> OIDCompResErr err
+ Right (OpenId.Identifier ident) -> OIDCompResGood ident dest
+
+-- | token dest
+data RpxnowRequest = RpxnowRequest String (Maybe String)
+instance Request RpxnowRequest where
+ parseRequest = do
+ token <- getParam "token"
+ dest <- getParam "dest"
+ return $! RpxnowRequest token $ chopHash `fmap` dest
+
+chopHash :: String -> String
+chopHash ('#':rest) = rest
+chopHash x = x
+
+-- | dest identifier
+data RpxnowResponse = RpxnowResponse String (Maybe String)
+instance Response RpxnowResponse where
+ reps (RpxnowResponse dest Nothing) =
+ [("text/html", response 303 [("Location", dest)] "")]
+ reps (RpxnowResponse dest (Just ident)) =
+ [("text/html", response 303
+ [ ("Location", dest)
+ , (authCookieName, ident)
+ ]
+ "")]
+
+rpxnowLogin :: String -- ^ api key
+ -> RpxnowRequest
+ -> IO RpxnowResponse
+rpxnowLogin apiKey (RpxnowRequest token dest') = do
+ let dest = case dest' of
+ Nothing -> "/"
+ Just "" -> "/"
+ Just s -> s
+ ident' <- Rpxnow.authenticate apiKey token
+ return $ RpxnowResponse dest (Rpxnow.identifier `fmap` ident')
+
+data AuthRequest = AuthRequest (Maybe String)
+instance Request AuthRequest where
+ parseRequest = AuthRequest `fmap` identifier
+
+authCheck :: AuthRequest -> IO Object
+authCheck (AuthRequest Nothing) =
+ return $ toObject [("status", "notloggedin")]
+authCheck (AuthRequest (Just i)) =
+ return $ toObject
+ [ ("status", "loggedin")
+ , ("ident", i)
+ ]
+
+authLogout :: () -> IO LogoutResponse
+authLogout _ = return LogoutResponse
+
+data LogoutResponse = LogoutResponse
+instance Response LogoutResponse where
+ reps _ = map (second addCookie) $ reps tree where
+ tree = toObject [("status", "loggedout")]
+ addCookie (Hack.Response s h c) =
+ Hack.Response s (h':h) c
+ h' = resetCookie authCookieName
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index 8fd46e7c..a26027bd 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -47,6 +47,7 @@ import System.Locale
import Web.Restful.Request -- FIXME ultimately remove
import Data.Object
import Data.List (intercalate)
+import Data.Object.Instances
type ContentType = String
diff --git a/restful.cabal b/restful.cabal
index 7a10957a..661ffd09 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -29,7 +29,8 @@ library
bytestring-class,
web-encodings,
mtl >= 1.1.0.2,
- data-object
+ data-object,
+ yaml >= 0.0.1
exposed-modules: Web.Restful,
Web.Restful.Constants,
Web.Restful.Request,
@@ -37,5 +38,8 @@ library
Web.Restful.Utils,
Web.Restful.Definitions,
Web.Restful.Handler,
- Web.Restful.Application
+ Web.Restful.Application,
+ Data.Object.Instances,
+ Hack.Middleware.MethodOverride,
+ Web.Restful.Helpers.Auth
ghc-options: -Wall
From b338a160f5779440c7106ce8df2fb826a65c3a08 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Thu, 6 Aug 2009 16:28:06 +0300
Subject: [PATCH 009/624] Added missing Web/Restful.hs
---
Web/Restful.hs | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
create mode 100644 Web/Restful.hs
diff --git a/Web/Restful.hs b/Web/Restful.hs
new file mode 100644
index 00000000..a4699743
--- /dev/null
+++ b/Web/Restful.hs
@@ -0,0 +1,29 @@
+---------------------------------------------------------
+--
+-- Module : Web.Restful
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Lightweight framework for designing RESTful APIs.
+--
+---------------------------------------------------------
+module Web.Restful
+ (
+ module Data.Object
+ , module Web.Restful.Request
+ , module Web.Restful.Response
+ , module Web.Restful.Application
+ , module Web.Restful.Definitions
+ , module Web.Restful.Handler
+ ) where
+
+import Data.Object
+import Web.Restful.Request
+import Web.Restful.Response
+import Web.Restful.Application
+import Web.Restful.Definitions
+import Web.Restful.Handler
From a3e328ca9969c4e31f752dc4f1b5fc504ab3b547 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Mon, 10 Aug 2009 08:51:16 +0300
Subject: [PATCH 010/624] Switched to ResourceName type class
---
Web/Restful.hs | 2 ++
Web/Restful/Application.hs | 34 ++++++++++++++----
Web/Restful/Definitions.hs | 15 --------
Web/Restful/Handler.hs | 4 ---
Web/Restful/Helpers/Auth.hs | 71 ++++++++++++++++---------------------
Web/Restful/Resource.hs | 65 +++++++++++++++++++++++++++++++++
6 files changed, 125 insertions(+), 66 deletions(-)
create mode 100644 Web/Restful/Resource.hs
diff --git a/Web/Restful.hs b/Web/Restful.hs
index a4699743..47ac2fbc 100644
--- a/Web/Restful.hs
+++ b/Web/Restful.hs
@@ -19,6 +19,7 @@ module Web.Restful
, module Web.Restful.Application
, module Web.Restful.Definitions
, module Web.Restful.Handler
+ , module Web.Restful.Resource
) where
import Data.Object
@@ -27,3 +28,4 @@ import Web.Restful.Response
import Web.Restful.Application
import Web.Restful.Definitions
import Web.Restful.Handler
+import Web.Restful.Resource
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index dbe63821..1890644e 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -47,6 +47,7 @@ import Web.Restful.Utils
import Web.Restful.Handler
import Web.Restful.Definitions
import Web.Restful.Constants
+import Web.Restful.Resource
-- | Contains settings and a list of resources.
type ApplicationMonad a = State (ApplicationSettings a)
@@ -63,8 +64,7 @@ data ApplicationSettings rn = ApplicationSettings
, htmlWrapper :: BS.ByteString -> BS.ByteString
}
-instance (HasResourceParser a) =>
- Default (ApplicationSettings a) where
+instance Default (ApplicationSettings a) where
def = ApplicationSettings
{ encryptKey = Left defaultKeyFile
, hackMiddleware =
@@ -91,7 +91,7 @@ setHtmlWrapper f = do
s <- get
put $ s { htmlWrapper = f }
-toHackApp :: (Eq a, HasResourceParser a, HasHandlers a b)
+toHackApp :: ResourceName a b
=> ApplicationMonad a ()
-> b
-> IO Hack.Application
@@ -106,15 +106,34 @@ toHackApp am model = do
app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
return app
-toHackApplication :: (HasResourceParser resourceName, Eq resourceName)
+findResourceNames :: ResourceName a model
+ => Resource
+ -> [(a, [(String, String)])]
+findResourceNames r = takeJusts $ map (checkPatternHelper r) allValues
+
+checkPatternHelper :: ResourceName a model
+ => Resource
+ -> a
+ -> Maybe (a, [(String, String)])
+checkPatternHelper r rn =
+ case checkPattern (fromString $ resourcePattern rn) r of
+ Nothing -> Nothing
+ Just pairs -> Just (rn, pairs)
+
+takeJusts :: [Maybe a] -> [a]
+takeJusts [] = []
+takeJusts (Nothing:rest) = takeJusts rest
+takeJusts (Just x:rest) = x : takeJusts rest
+
+toHackApplication :: ResourceName resourceName model
=> HandlerMap resourceName
-> ApplicationSettings resourceName
-> Hack.Application
toHackApplication hm settings env = do
let (Right resource) = splitPath $ Hack.pathInfo env
- case resourceParser resource of
- Nothing -> response404 settings $ env
- (Just (ParsedResource rn urlParams')) -> do
+ case findResourceNames resource of
+ [] -> response404 settings $ env
+ [(rn, urlParams')] -> do
let verb :: Verb
verb = toVerb $ Hack.requestMethod env
rr :: RawRequest
@@ -145,6 +164,7 @@ toHackApplication hm settings env = do
(("Content-Type", ctype) : headers)
$ toLazyByteString $ wrapper content
Nothing -> response404 settings $ env
+ x -> error $ "Invalid matches: " ++ show x
envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
envToRawRequest urlParams' env =
diff --git a/Web/Restful/Definitions.hs b/Web/Restful/Definitions.hs
index 12a6aab1..0d21bcd8 100644
--- a/Web/Restful/Definitions.hs
+++ b/Web/Restful/Definitions.hs
@@ -17,9 +17,6 @@ module Web.Restful.Definitions
( Verb (..)
, toVerb
, Resource
- , ParsedResource (..)
- , ResourceParser
- , HasResourceParser (..)
) where
import qualified Hack
@@ -34,15 +31,3 @@ toVerb Hack.POST = Post
toVerb _ = Get
type Resource = [String]
-
-data ParsedResource a = ParsedResource
- { resourceName :: a
- , urlParameters :: [(String, String)]
- }
-
-type ResourceParser a = Resource -> Maybe (ParsedResource a)
-
-class HasResourceParser a where
- resourceParser :: ResourceParser a
- simpleParse :: a -> Maybe (ParsedResource a)
- simpleParse x = Just $ ParsedResource x []
diff --git a/Web/Restful/Handler.hs b/Web/Restful/Handler.hs
index 980ff848..b4a1f1fa 100644
--- a/Web/Restful/Handler.hs
+++ b/Web/Restful/Handler.hs
@@ -18,7 +18,6 @@ module Web.Restful.Handler
( Handler (..)
, runHandler
, HandlerMap
- , HasHandlers (..)
, liftHandler
) where
@@ -37,9 +36,6 @@ runHandler (Handler f) rreq = do
type HandlerMap a = a -> Verb -> Maybe Handler
-class HasHandlers a b | a -> b where
- getHandler :: b -> a -> Verb -> Maybe Handler
-
liftHandler :: (Request req, Response res)
=> (req -> IO res)
-> Maybe Handler
diff --git a/Web/Restful/Helpers/Auth.hs b/Web/Restful/Helpers/Auth.hs
index 44cd26e8..7d2c2481 100644
--- a/Web/Restful/Helpers/Auth.hs
+++ b/Web/Restful/Helpers/Auth.hs
@@ -15,8 +15,6 @@
---------------------------------------------------------
module Web.Restful.Helpers.Auth
( AuthResource
- , FromAuthResource (..)
- , authResourceParser
) where
import qualified Hack
@@ -34,48 +32,41 @@ import Control.Monad.Reader
import Data.Object
data AuthResource =
- AuthCheck
- | AuthLogout
- | AuthOpenid
- | AuthOpenidForward
- | AuthOpenidComplete
- | AuthLoginRpxnow
- deriving Eq
-class FromAuthResource a where
- fromAuthResource :: AuthResource -> a
-
-authResourceParser :: FromAuthResource far
- => Resource
- -> Maybe (ParsedResource far)
-authResourceParser ["check"] =
- authResourceParser' AuthCheck
-authResourceParser ["logout"] =
- authResourceParser' AuthLogout
-authResourceParser ["openid"] =
- authResourceParser' AuthOpenid
-authResourceParser ["openid", "forward"] =
- authResourceParser' AuthOpenidForward
-authResourceParser ["openid", "complete"] =
- authResourceParser' AuthOpenidComplete
-authResourceParser ["login", "rpxnow"] =
- authResourceParser' AuthLoginRpxnow
-authResourceParser _ = Nothing
-
-authResourceParser' :: FromAuthResource far
- => AuthResource
- -> Maybe (ParsedResource far)
-authResourceParser' x = Just $ ParsedResource (fromAuthResource x) []
+ Check
+ | Logout
+ | Openid
+ | OpenidForward
+ | OpenidComplete
+ | LoginRpxnow
+ deriving Show
type RpxnowApiKey = String -- FIXME newtype
-instance HasHandlers AuthResource (Maybe RpxnowApiKey) where
- getHandler _ AuthCheck Get = liftHandler authCheck
- getHandler _ AuthLogout Get = liftHandler authLogout
- getHandler _ AuthOpenid Get = liftHandler authOpenidForm
- getHandler _ AuthOpenidForward Get = liftHandler authOpenidForward
- getHandler _ AuthOpenidComplete Get = liftHandler authOpenidComplete
- getHandler (Just key) AuthLoginRpxnow Get = liftHandler $ rpxnowLogin key
+instance ResourceName AuthResource (Maybe RpxnowApiKey) where
+ getHandler _ Check Get = liftHandler authCheck
+ getHandler _ Logout Get = liftHandler authLogout
+ getHandler _ Openid Get = liftHandler authOpenidForm
+ getHandler _ OpenidForward Get = liftHandler authOpenidForward
+ getHandler _ OpenidComplete Get = liftHandler authOpenidComplete
+ getHandler (Just key) LoginRpxnow Get = liftHandler $ rpxnowLogin key
getHandler _ _ _ = Nothing
+ allValues =
+ Check
+ : Logout
+ : Openid
+ : OpenidForward
+ : OpenidComplete
+ : LoginRpxnow
+ : []
+
+ resourcePattern Check = "/auth/check/"
+ resourcePattern Logout = "/auth/logout/"
+ resourcePattern Openid = "/auth/openid/"
+ resourcePattern OpenidForward = "/auth/openid/forward/"
+ resourcePattern OpenidComplete = "/auth/openid/complete/"
+ resourcePattern LoginRpxnow = "/auth/login/rpxnow/"
+
+
data OIDFormReq = OIDFormReq (Maybe String) (Maybe String)
instance Request OIDFormReq where
parseRequest = OIDFormReq <$> getParam "message" <*> getParam "dest"
diff --git a/Web/Restful/Resource.hs b/Web/Restful/Resource.hs
new file mode 100644
index 00000000..f229c6d6
--- /dev/null
+++ b/Web/Restful/Resource.hs
@@ -0,0 +1,65 @@
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FunctionalDependencies #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Resource
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Defines the Resource class.
+--
+---------------------------------------------------------
+module Web.Restful.Resource
+ ( ResourceName (..)
+ , fromString
+ , checkPattern
+ ) where
+
+import Data.List.Split (splitOn)
+import Web.Restful.Definitions
+import Web.Restful.Handler
+
+data ResourcePatternPiece =
+ Static String
+ | Dynamic String
+ deriving Show
+
+type ResourcePattern = [ResourcePatternPiece]
+
+fromString :: String -> ResourcePattern
+fromString = map fromString' . filter (not . null) . splitOn "/"
+
+fromString' :: String -> ResourcePatternPiece
+fromString' ('$':rest) = Dynamic rest
+fromString' x = Static x
+
+class Show a => ResourceName a b | a -> b where
+ resourcePattern :: a -> String
+ allValues :: [a]
+ getHandler :: b -> a -> Verb -> Maybe Handler
+
+-- FIXME add some overlap checking functions
+
+type SMap = [(String, String)]
+
+data CheckPatternReturn = StaticMatch | DynamicMatch (String, String) | NoMatch
+
+checkPattern :: ResourcePattern -> Resource -> Maybe SMap
+checkPattern rp r =
+ if length rp /= length r
+ then Nothing
+ else combine [] $ zipWith checkPattern' rp r
+
+checkPattern' :: ResourcePatternPiece -> String -> CheckPatternReturn
+checkPattern' (Static x) y = if x == y then StaticMatch else NoMatch
+checkPattern' (Dynamic x) y = DynamicMatch (x, y)
+
+combine :: SMap -> [CheckPatternReturn] -> Maybe SMap
+combine s [] = Just $ reverse s
+combine _ (NoMatch:_) = Nothing
+combine s (StaticMatch:rest) = combine s rest
+combine s (DynamicMatch x:rest) = combine (x:s) rest
From 2b8131b29bd8b84709cda497de4c21a368d27028 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Mon, 24 Aug 2009 21:08:57 +0300
Subject: [PATCH 011/624] Added a Response instance
---
Web/Restful/Response.hs | 4 ++++
restful.cabal | 1 +
2 files changed, 5 insertions(+)
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index a26027bd..c950ab86 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -1,4 +1,5 @@
{-# LANGUAGE ExistentialQuantification #-}
+{-# LANGUAGE FlexibleInstances #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Response
@@ -292,3 +293,6 @@ instance Response Object where
[ ("text/html", response 200 [] $ treeToHtml tree)
, ("application/json", response 200 [] $ treeToJson tree)
]
+
+instance Response [(String, Hack.Response)] where
+ reps = id
diff --git a/restful.cabal b/restful.cabal
index 661ffd09..b8ee84d9 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -39,6 +39,7 @@ library
Web.Restful.Definitions,
Web.Restful.Handler,
Web.Restful.Application,
+ Web.Restful.Resource,
Data.Object.Instances,
Hack.Middleware.MethodOverride,
Web.Restful.Helpers.Auth
From 1caa0c4891bfe2fffdcf1909b589cccd153a4506 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Fri, 28 Aug 2009 11:01:19 +0300
Subject: [PATCH 012/624] Split up Web.Restful.Response
---
Data/Object/Instances.hs | 23 ++--
Web/Restful/Generic/ListDetail.hs | 55 +++++++++
Web/Restful/Response.hs | 187 ++----------------------------
Web/Restful/Response/AtomFeed.hs | 88 ++++++++++++++
Web/Restful/Response/Sitemap.hs | 90 ++++++++++++++
restful.cabal | 5 +-
6 files changed, 258 insertions(+), 190 deletions(-)
create mode 100644 Web/Restful/Generic/ListDetail.hs
create mode 100644 Web/Restful/Response/AtomFeed.hs
create mode 100644 Web/Restful/Response/Sitemap.hs
diff --git a/Data/Object/Instances.hs b/Data/Object/Instances.hs
index 6f2301ef..8b3ba8ef 100644
--- a/Data/Object/Instances.hs
+++ b/Data/Object/Instances.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE FlexibleInstances #-}
---------------------------------------------------------
--
-- Module : Data.Object.Instances
@@ -15,6 +16,7 @@ module Data.Object.Instances
( Json (..)
, Yaml (..)
, Html (..)
+ , SafeFromObject (..)
) where
import Data.Object
@@ -23,9 +25,12 @@ import Data.ByteString.Class
import Web.Encodings (encodeJson)
import qualified Text.Yaml as Y
-newtype Json = Json B.ByteString
-instance FromObject Json where
- fromObject = return . Json . helper where
+class SafeFromObject a where
+ safeFromObject :: Object -> a
+
+newtype Json = Json { unJson :: B.ByteString }
+instance SafeFromObject Json where
+ safeFromObject = Json . helper where
helper :: Object -> B.ByteString
helper (Scalar s) = B.concat
[ toStrictByteString "\""
@@ -50,19 +55,19 @@ instance FromObject Json where
, helper v
]
-newtype Yaml = Yaml B.ByteString
-instance FromObject Yaml where
- fromObject = return . Yaml . Y.encode
+newtype Yaml = Yaml { unYaml :: B.ByteString }
+instance SafeFromObject Yaml where
+ safeFromObject = Yaml . Y.encode
-- | Represents as an entire HTML 5 document by using the following:
--
-- * A scalar is a paragraph.
-- * A sequence is an unordered list.
-- * A mapping is a definition list.
-newtype Html = Html B.ByteString
+newtype Html = Html { unHtml :: B.ByteString }
-instance FromObject Html where
- fromObject o = return $ Html $ B.concat
+instance SafeFromObject Html where
+ safeFromObject o = Html $ B.concat
[ toStrictByteString "\n"
, helper o
, toStrictByteString ""
diff --git a/Web/Restful/Generic/ListDetail.hs b/Web/Restful/Generic/ListDetail.hs
new file mode 100644
index 00000000..00d48d57
--- /dev/null
+++ b/Web/Restful/Generic/ListDetail.hs
@@ -0,0 +1,55 @@
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Generic.ListDetail
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Generic responses for listing items and then detailing them.
+--
+---------------------------------------------------------
+module Web.Restful.Generic.ListDetail
+ ( ListDetail (..)
+ , ItemList (..)
+ , ItemDetail (..)
+ ) where
+
+import Web.Restful.Response
+import Web.Encodings
+import Data.Object
+import Data.Object.Instances
+import Data.ByteString.Class
+
+class ToObject a => ListDetail a where
+ htmlDetail :: a -> String
+ htmlDetail = fromStrictByteString . unHtml . safeFromObject . toObject
+ detailTitle :: a -> String
+ detailUrl :: a -> String
+ htmlList :: [a] -> String
+ htmlList l = "" ++ concatMap helper l ++ " "
+ where
+ helper i = "" ++ encodeHtml (detailTitle i) ++
+ " "
+ -- | Often times for the JSON response of the list, we don't need all
+ -- the information.
+ treeList :: [a] -> Object -- FIXME
+ treeList = Sequence . map treeListSingle
+ treeListSingle :: a -> Object
+ treeListSingle = toObject
+
+newtype ItemList a = ItemList [a]
+instance ListDetail a => Response (ItemList a) where
+ reps (ItemList l) =
+ [ ("text/html", response 200 [] $ htmlList l)
+ , ("application/json", response 200 [] $ unJson $ safeFromObject $ treeList l)
+ ]
+newtype ItemDetail a = ItemDetail a
+instance ListDetail a => Response (ItemDetail a) where
+ reps (ItemDetail i) =
+ [ ("text/html", response 200 [] $ htmlDetail i)
+ , ("application/json", response 200 [] $ unJson $ safeFromObject $ toObject i)
+ ]
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index c950ab86..9d5e8221 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -18,25 +18,12 @@ module Web.Restful.Response
-- * Response construction
Response (..)
, response
- -- ** Helper 'Response' instances
- -- *** Atom news feed
- , AtomFeed (..)
- , AtomFeedEntry (..)
- -- *** Sitemap
- , sitemap
- , SitemapUrl (..)
- , SitemapLoc (..)
- , SitemapChangeFreq (..)
- -- *** Generics
- -- **** List/detail
- , ListDetail (..)
- , ItemList (..)
- , ItemDetail (..)
- -- **** Multiple response types.
- , GenResponse (..)
-- * FIXME
+ , GenResponse (..)
, ResponseWrapper (..)
, ErrorResponse (..)
+ , formatW3
+ , UTCTime
) where
import Data.ByteString.Class
@@ -45,10 +32,8 @@ import Data.Time.Format
import Data.Time.Clock
import Web.Encodings
import System.Locale
-import Web.Restful.Request -- FIXME ultimately remove
import Data.Object
import Data.List (intercalate)
-import Data.Object.Instances
type ContentType = String
@@ -80,138 +65,6 @@ data ResponseWrapper = forall res. Response res => ResponseWrapper res
instance Response ResponseWrapper where
reps (ResponseWrapper res) = reps res
-data AtomFeed = AtomFeed
- { atomTitle :: String
- , atomLinkSelf :: String
- , atomLinkHome :: String
- , atomUpdated :: UTCTime
- , atomEntries :: [AtomFeedEntry]
- }
-instance Response AtomFeed where
- reps e =
- [ ("application/atom+xml", response 200 [] $ show e)
- ]
-
-data AtomFeedEntry = AtomFeedEntry
- { atomEntryLink :: String
- , atomEntryUpdated :: UTCTime
- , atomEntryTitle :: String
- , atomEntryContent :: String
- }
-
-instance Show AtomFeed where
- show f = concat
- [ "\n"
- , ""
- , ""
- , encodeHtml $ atomTitle f
- , " "
- , " "
- , " "
- , ""
- , formatW3 $ atomUpdated f
- , " "
- , ""
- , encodeHtml $ atomLinkHome f
- , " "
- , concatMap show $ atomEntries f
- , " "
- ]
-
-instance Show AtomFeedEntry where
- show e = concat
- [ ""
- , ""
- , encodeHtml $ atomEntryLink e
- , " "
- , " "
- , ""
- , formatW3 $ atomEntryUpdated e
- , " "
- , ""
- , encodeHtml $ atomEntryTitle e
- , " "
- , " "
- , " "
- ]
-
-formatW3 :: UTCTime -> String
-formatW3 = formatTime defaultTimeLocale "%FT%X-08:00"
-
--- sitemaps
-data SitemapLoc = AbsLoc String | RelLoc String
-data SitemapChangeFreq = Always
- | Hourly
- | Daily
- | Weekly
- | Monthly
- | Yearly
- | Never
-instance Show SitemapChangeFreq where
- show Always = "always"
- show Hourly = "hourly"
- show Daily = "daily"
- show Weekly = "weekly"
- show Monthly = "monthly"
- show Yearly = "yearly"
- show Never = "never"
-
-data SitemapUrl = SitemapUrl
- { sitemapLoc :: SitemapLoc
- , sitemapLastMod :: UTCTime
- , sitemapChangeFreq :: SitemapChangeFreq
- , priority :: Double
- }
-data SitemapRequest = SitemapRequest String Int
-instance Request SitemapRequest where
- parseRequest = do
- env <- parseEnv
- return $! SitemapRequest (Hack.serverName env)
- (Hack.serverPort env)
-data SitemapResponse = SitemapResponse SitemapRequest [SitemapUrl]
-instance Show SitemapResponse where
- show (SitemapResponse (SitemapRequest host port) urls) =
- "\n" ++
- "" ++
- concatMap helper urls ++
- " "
- where
- prefix = "http://" ++ host ++
- case port of
- 80 -> ""
- _ -> ":" ++ show port
- helper (SitemapUrl loc modTime freq pri) = concat
- [ ""
- , encodeHtml $ showLoc loc
- , " "
- , formatW3 modTime
- , " "
- , show freq
- , " "
- , show pri
- , " "
- ]
- showLoc (AbsLoc s) = s
- showLoc (RelLoc s) = prefix ++ s
-
-instance Response SitemapResponse where
- reps res =
- [ ("text/xml", response 200 [] $ show res)
- ]
-
-sitemap :: IO [SitemapUrl] -> SitemapRequest -> IO SitemapResponse
-sitemap urls' req = do
- urls <- urls'
- return $ SitemapResponse req urls
-
data GenResponse = HtmlResponse String
| ObjectResponse Object
| HtmlOrObjectResponse String Object
@@ -230,36 +83,6 @@ instance Response GenResponse where
"'>" ++ encodeHtml url ++ "
"
reps (PermissionDeniedResult s) = [("text/plain", response 403 [] s)]
reps (NotFoundResponse s) = [("text/plain", response 404 [] s)]
-class ToObject a => ListDetail a where
- htmlDetail :: a -> String
- htmlDetail = treeToHtml . toObject
- detailTitle :: a -> String
- detailUrl :: a -> String
- htmlList :: [a] -> String
- htmlList l = "" ++ concatMap helper l ++ " "
- where
- helper i = "" ++ encodeHtml (detailTitle i) ++
- " "
- -- | Often times for the JSON response of the list, we don't need all
- -- the information.
- treeList :: [a] -> Object -- FIXME
- treeList = Sequence . map treeListSingle
- treeListSingle :: a -> Object
- treeListSingle = toObject
-
-newtype ItemList a = ItemList [a]
-instance ListDetail a => Response (ItemList a) where
- reps (ItemList l) =
- [ ("text/html", response 200 [] $ htmlList l)
- , ("application/json", response 200 [] $ treeToJson $ treeList l)
- ]
-newtype ItemDetail a = ItemDetail a
-instance ListDetail a => Response (ItemDetail a) where
- reps (ItemDetail i) =
- [ ("text/html", response 200 [] $ htmlDetail i)
- , ("application/json", response 200 [] $ treeToJson $ toObject i)
- ]
-- FIXME remove treeTo functions, replace with Object instances
treeToJson :: Object -> String
@@ -296,3 +119,7 @@ instance Response Object where
instance Response [(String, Hack.Response)] where
reps = id
+
+-- FIXME put in a separate module (maybe Web.Encodings)
+formatW3 :: UTCTime -> String
+formatW3 = formatTime defaultTimeLocale "%FT%X-08:00"
diff --git a/Web/Restful/Response/AtomFeed.hs b/Web/Restful/Response/AtomFeed.hs
new file mode 100644
index 00000000..c79d9d3b
--- /dev/null
+++ b/Web/Restful/Response/AtomFeed.hs
@@ -0,0 +1,88 @@
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Response.AtomFeed
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Generating atom news feeds.
+--
+---------------------------------------------------------
+
+module Web.Restful.Response.AtomFeed
+ ( AtomFeed (..)
+ , AtomFeedEntry (..)
+ ) where
+
+import Web.Restful.Response
+
+import Data.Time.Format
+import Data.Time.Clock
+import Web.Encodings
+import System.Locale
+
+data AtomFeed = AtomFeed
+ { atomTitle :: String
+ , atomLinkSelf :: String
+ , atomLinkHome :: String
+ , atomUpdated :: UTCTime
+ , atomEntries :: [AtomFeedEntry]
+ }
+instance Response AtomFeed where
+ reps e =
+ [ ("application/atom+xml", response 200 [] $ show e)
+ ]
+
+data AtomFeedEntry = AtomFeedEntry
+ { atomEntryLink :: String
+ , atomEntryUpdated :: UTCTime
+ , atomEntryTitle :: String
+ , atomEntryContent :: String
+ }
+
+instance Show AtomFeed where
+ show f = concat
+ [ "\n"
+ , ""
+ , ""
+ , encodeHtml $ atomTitle f
+ , " "
+ , " "
+ , " "
+ , ""
+ , formatW3 $ atomUpdated f
+ , " "
+ , ""
+ , encodeHtml $ atomLinkHome f
+ , " "
+ , concatMap show $ atomEntries f
+ , " "
+ ]
+
+instance Show AtomFeedEntry where
+ show e = concat
+ [ ""
+ , ""
+ , encodeHtml $ atomEntryLink e
+ , " "
+ , " "
+ , ""
+ , formatW3 $ atomEntryUpdated e
+ , " "
+ , ""
+ , encodeHtml $ atomEntryTitle e
+ , " "
+ , " "
+ , " "
+ ]
diff --git a/Web/Restful/Response/Sitemap.hs b/Web/Restful/Response/Sitemap.hs
new file mode 100644
index 00000000..0167a9f5
--- /dev/null
+++ b/Web/Restful/Response/Sitemap.hs
@@ -0,0 +1,90 @@
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Response.AtomFeed
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Stable
+-- Portability : portable
+--
+-- Generating Google sitemap files.
+--
+---------------------------------------------------------
+
+module Web.Restful.Response.Sitemap
+ ( sitemap
+ , SitemapUrl (..)
+ , SitemapLoc (..)
+ , SitemapChangeFreq (..)
+ ) where
+
+import Web.Restful.Response
+import Web.Encodings
+import qualified Hack
+import Web.Restful.Request
+
+data SitemapLoc = AbsLoc String | RelLoc String
+data SitemapChangeFreq = Always
+ | Hourly
+ | Daily
+ | Weekly
+ | Monthly
+ | Yearly
+ | Never
+instance Show SitemapChangeFreq where
+ show Always = "always"
+ show Hourly = "hourly"
+ show Daily = "daily"
+ show Weekly = "weekly"
+ show Monthly = "monthly"
+ show Yearly = "yearly"
+ show Never = "never"
+
+data SitemapUrl = SitemapUrl
+ { sitemapLoc :: SitemapLoc
+ , sitemapLastMod :: UTCTime
+ , sitemapChangeFreq :: SitemapChangeFreq
+ , priority :: Double
+ }
+data SitemapRequest = SitemapRequest String Int
+instance Request SitemapRequest where
+ parseRequest = do
+ env <- parseEnv
+ return $! SitemapRequest (Hack.serverName env)
+ (Hack.serverPort env)
+data SitemapResponse = SitemapResponse SitemapRequest [SitemapUrl]
+instance Show SitemapResponse where
+ show (SitemapResponse (SitemapRequest host port) urls) =
+ "\n" ++
+ "" ++
+ concatMap helper urls ++
+ " "
+ where
+ prefix = "http://" ++ host ++
+ case port of
+ 80 -> ""
+ _ -> ":" ++ show port
+ helper (SitemapUrl loc modTime freq pri) = concat
+ [ ""
+ , encodeHtml $ showLoc loc
+ , " "
+ , formatW3 modTime
+ , " "
+ , show freq
+ , " "
+ , show pri
+ , " "
+ ]
+ showLoc (AbsLoc s) = s
+ showLoc (RelLoc s) = prefix ++ s
+
+instance Response SitemapResponse where
+ reps res =
+ [ ("text/xml", response 200 [] $ show res)
+ ]
+
+sitemap :: IO [SitemapUrl] -> SitemapRequest -> IO SitemapResponse
+sitemap urls' req = do
+ urls <- urls'
+ return $ SitemapResponse req urls
diff --git a/restful.cabal b/restful.cabal
index b8ee84d9..8a8efa37 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -42,5 +42,8 @@ library
Web.Restful.Resource,
Data.Object.Instances,
Hack.Middleware.MethodOverride,
- Web.Restful.Helpers.Auth
+ Web.Restful.Helpers.Auth,
+ Web.Restful.Response.AtomFeed,
+ Web.Restful.Response.Sitemap,
+ Web.Restful.Generic.ListDetail
ghc-options: -Wall
From 6842ef68642a6aab2f9c989ea2b3606e289e962b Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Tue, 15 Sep 2009 22:34:34 +0300
Subject: [PATCH 013/624] Added parameter instance for day
---
Web/Restful/Request.hs | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/Web/Restful/Request.hs b/Web/Restful/Request.hs
index 9f98849b..4596458b 100644
--- a/Web/Restful/Request.hs
+++ b/Web/Restful/Request.hs
@@ -53,6 +53,8 @@ import Web.Restful.Constants
import Web.Restful.Utils
import Control.Applicative (Applicative (..))
import Web.Encodings
+import Data.Time.Calendar (Day, fromGregorian)
+import Data.Char (isDigit)
-- $param_overview
-- In Restful, all of the underlying parameter values are strings. They can
@@ -257,6 +259,24 @@ instance Parameter Int where
((x, _):_) -> Right x
_ -> Left $ "Invalid integer: " ++ s
+instance Parameter Day where
+ readParam s =
+ let t1 = length s == 10
+ t2 = s !! 4 == '-'
+ t3 = s !! 7 == '-'
+ t4 = all isDigit $ concat
+ [ take 4 s
+ , take 2 $ drop 5 s
+ , take 2 $ drop 8 s
+ ]
+ t = and [t1, t2, t3, t4]
+ y = read $ take 4 s
+ m = read $ take 2 $ drop 5 s
+ d = read $ take 2 $ drop 8 s
+ in if t
+ then Right $ fromGregorian y m d
+ else Left $ "Invalid date: " ++ s
+
-- | The input for a resource.
--
-- Each resource can define its own instance of 'Request' and then more
From b728e7ff84ffb9a7f92cebfe1bd91bacf5114c41 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Wed, 16 Sep 2009 23:27:37 +0300
Subject: [PATCH 014/624] Replaced ApplicationMonad with RestfulApp, version
bump
---
Web/Restful/Application.hs | 128 ++++++++++++++++---------------------
Web/Restful/Resource.hs | 9 +++
restful.cabal | 2 +-
3 files changed, 66 insertions(+), 73 deletions(-)
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index 1890644e..9236a35e 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE ScopedTypeVariables #-}
@@ -16,29 +18,21 @@
---------------------------------------------------------
module Web.Restful.Application
(
- -- * Defining an application
- ApplicationMonad
- -- ** Settings
- , setHtmlWrapper
- -- ** Engage
- , toHackApp
+ toHackApp
+ , RestfulApp (..)
) where
--- hideously long import list
-import qualified Hack
-import Control.Monad.State hiding (gets)
import Web.Encodings
import Data.Maybe (isJust)
-import Data.ByteString.Class
-import qualified Data.ByteString.Lazy as BS
import Data.Function.Predicate (equals)
-import Data.Default
-import Control.Applicative ( Applicative (..))
+import Data.ByteString.Class
+import qualified Data.ByteString.Lazy as B
-import Hack.Middleware.Gzip
+import qualified Hack
import Hack.Middleware.CleanPath
-import Hack.Middleware.Jsonp
import Hack.Middleware.ClientSession
+import Hack.Middleware.Gzip
+import Hack.Middleware.Jsonp
import Hack.Middleware.MethodOverride
import Web.Restful.Request
@@ -49,61 +43,45 @@ import Web.Restful.Definitions
import Web.Restful.Constants
import Web.Restful.Resource
--- | Contains settings and a list of resources.
-type ApplicationMonad a = State (ApplicationSettings a)
-instance Applicative (ApplicationMonad a) where
- pure = return
- f <*> a = do
- f' <- f
- a' <- a
- return $! f' a'
-data ApplicationSettings rn = ApplicationSettings
- { encryptKey :: Either FilePath Word256
- , hackMiddleware :: [Hack.Middleware]
- , response404 :: Hack.Env -> IO Hack.Response
- , htmlWrapper :: BS.ByteString -> BS.ByteString
- }
+-- | A data type that can be turned into a Hack application.
+class ResourceName a b => RestfulApp a b | a -> b where
+ -- | Load up the model, ie the data which use passed to each handler.
+ getModel :: a -> IO b
-instance Default (ApplicationSettings a) where
- def = ApplicationSettings
- { encryptKey = Left defaultKeyFile
- , hackMiddleware =
+ -- | The encryption key to be used for encrypting client sessions.
+ encryptKey :: a -> IO Word256
+ encryptKey _ = getKey defaultKeyFile
+
+ -- | All of the middlewares to install.
+ hackMiddleware :: a -> [Hack.Middleware]
+ hackMiddleware _ =
[ gzip
, cleanPath
, jsonp
, methodOverride
]
- , response404 = default404
- , htmlWrapper = id
- }
-default404 :: Hack.Env -> IO Hack.Response
-default404 env = return $
- Hack.Response
- 404
- [("Content-Type", "text/plain")]
- $ toLazyByteString $ "Not found: " ++ Hack.pathInfo env
+ -- | How to generate 404 pages. FIXME make more user-friendly.
+ response404 :: a -> Hack.Env -> IO Hack.Response
+ response404 _ = default404
--- FIXME document below here
+ -- | Wrappers for cleaning up responses. Especially intended for
+ -- beautifying static HTML. FIXME more user friendly.
+ responseWrapper :: a -> String -> B.ByteString -> IO B.ByteString
+ responseWrapper _ _ = return
-setHtmlWrapper :: (BS.ByteString -> BS.ByteString) -> ApplicationMonad a ()
-setHtmlWrapper f = do
- s <- get
- put $ s { htmlWrapper = f }
-
-toHackApp :: ResourceName a b
- => ApplicationMonad a ()
- -> b
+-- | Given a sample resource name (purely for typing reasons), generating
+-- a Hack application.
+toHackApp :: RestfulApp resourceName modelType
+ => resourceName
-> IO Hack.Application
-toHackApp am model = do
- let settings = execState am def
- key <- case encryptKey settings of
- Left f -> getKey f
- Right k -> return k
+toHackApp a = do
+ model <- getModel a
+ key <- encryptKey a
let handlers = getHandler model
- app' = toHackApplication handlers settings
+ app' = toHackApplication a handlers
clientsession' = clientsession [authCookieName] key -- FIXME gotta be a better way...
- app = foldr ($) app' $ hackMiddleware settings ++ [clientsession']
+ app = foldr ($) app' $ hackMiddleware a ++ [clientsession']
return app
findResourceNames :: ResourceName a model
@@ -125,14 +103,14 @@ takeJusts [] = []
takeJusts (Nothing:rest) = takeJusts rest
takeJusts (Just x:rest) = x : takeJusts rest
-toHackApplication :: ResourceName resourceName model
- => HandlerMap resourceName
- -> ApplicationSettings resourceName
+toHackApplication :: RestfulApp resourceName model
+ => resourceName
+ -> HandlerMap resourceName
-> Hack.Application
-toHackApplication hm settings env = do
+toHackApplication sampleRN hm env = do
let (Right resource) = splitPath $ Hack.pathInfo env
case findResourceNames resource of
- [] -> response404 settings $ env
+ [] -> response404 sampleRN $ env
[(rn, urlParams')] -> do
let verb :: Verb
verb = toVerb $ Hack.requestMethod env
@@ -154,16 +132,15 @@ toHackApplication hm settings env = do
[] -> Nothing
_ -> error "Overlapping reps"
case handlerPair of
- Nothing -> response404 settings $ env
+ Nothing -> response404 sampleRN $ env
Just (ctype, Hack.Response status headers content) -> do
- let wrapper =
- case ctype of
- "text/html" -> htmlWrapper settings
- _ -> id
- return $ Hack.Response status
- (("Content-Type", ctype) : headers)
- $ toLazyByteString $ wrapper content
- Nothing -> response404 settings $ env
+ content' <- responseWrapper sampleRN ctype content
+ let response' = Hack.Response
+ status
+ (("Content-Type", ctype) : headers)
+ content'
+ return response'
+ Nothing -> response404 sampleRN $ env
x -> error $ "Invalid matches: " ++ show x
envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
@@ -177,3 +154,10 @@ envToRawRequest urlParams' env =
rawCookie = tryLookup "" "Cookie" $ Hack.http env
cookies' = decodeCookies rawCookie :: [(String, String)]
in RawRequest rawPieces urlParams' gets' posts cookies' files env
+
+default404 :: Hack.Env -> IO Hack.Response
+default404 env = return $
+ Hack.Response
+ 404
+ [("Content-Type", "text/plain")]
+ $ toLazyByteString $ "Not found: " ++ Hack.pathInfo env
diff --git a/Web/Restful/Resource.hs b/Web/Restful/Resource.hs
index f229c6d6..afb1867c 100644
--- a/Web/Restful/Resource.hs
+++ b/Web/Restful/Resource.hs
@@ -38,8 +38,17 @@ fromString' ('$':rest) = Dynamic rest
fromString' x = Static x
class Show a => ResourceName a b | a -> b where
+ -- | Get the URL pattern for each different resource name.
+ -- Something like /foo/$bar/baz/ will match the regular expression
+ -- /foo/(\\w*)/baz/, matching the middle part with the urlParam bar.
resourcePattern :: a -> String
+
+ -- | Get all possible values for resource names.
+ -- Remember, if you use variables ($foo) in your resourcePatterns you
+ -- can get an unlimited number of resources for each resource name.
allValues :: [a]
+
+ -- | Find the handler for each resource name/verb pattern.
getHandler :: b -> a -> Verb -> Maybe Handler
-- FIXME add some overlap checking functions
diff --git a/restful.cabal b/restful.cabal
index 8a8efa37..4c28e492 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -1,5 +1,5 @@
name: restful
-version: 0.1.0
+version: 0.1.1
license: BSD3
license-file: LICENSE
author: Michael Snoyman
From c3c4d647d3867c3a246ba5bf9414f91bfafb0fbb Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Wed, 16 Sep 2009 23:33:34 +0300
Subject: [PATCH 015/624] Slight documentation update
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 6b699ebc..96cce18a 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,10 @@ 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
own ResourceName.
+NOTE: This has taken on a very central role in the restful library, in addition
+to the RestfulApp class. Hopefully I will have a chance to document this soon.
+As a result, some of the following documentation is outdated.
+
### ResourceParser
A ResourceParser converts a Resource (ie, a URL) to a ResourceName and URL
From 86ca811ac5d3b085fecb8202ee079937e5cb796b Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Fri, 18 Sep 2009 04:14:52 +0300
Subject: [PATCH 016/624] Massive revamp of responses; not yet fully functional
---
Data/Object/Instances.hs | 61 ++++----
Web/Restful/Application.hs | 34 +----
Web/Restful/Generic/ListDetail.hs | 14 +-
Web/Restful/Handler.hs | 30 ++--
Web/Restful/Helpers/Auth.hs | 110 +++++---------
Web/Restful/Resource.hs | 2 +-
Web/Restful/Response.hs | 239 ++++++++++++++++++++----------
Web/Restful/Response/AtomFeed.hs | 7 +-
Web/Restful/Response/Sitemap.hs | 10 +-
restful.cabal | 2 +-
10 files changed, 266 insertions(+), 243 deletions(-)
diff --git a/Data/Object/Instances.hs b/Data/Object/Instances.hs
index 8b3ba8ef..3bb2f241 100644
--- a/Data/Object/Instances.hs
+++ b/Data/Object/Instances.hs
@@ -20,10 +20,11 @@ module Data.Object.Instances
) where
import Data.Object
-import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as B
+import qualified Data.ByteString as BS
import Data.ByteString.Class
import Web.Encodings (encodeJson)
-import qualified Text.Yaml as Y
+import Text.Yaml (encode)
class SafeFromObject a where
safeFromObject :: Object -> a
@@ -33,31 +34,31 @@ instance SafeFromObject Json where
safeFromObject = Json . helper where
helper :: Object -> B.ByteString
helper (Scalar s) = B.concat
- [ toStrictByteString "\""
+ [ toLazyByteString "\""
, encodeJson $ fromStrictByteString s
- , toStrictByteString "\""
+ , toLazyByteString "\""
]
helper (Sequence s) = B.concat
- [ toStrictByteString "["
- , B.intercalate (toStrictByteString ",") $ map helper s
- , toStrictByteString "]"
+ [ toLazyByteString "["
+ , B.intercalate (toLazyByteString ",") $ map helper s
+ , toLazyByteString "]"
]
helper (Mapping m) = B.concat
- [ toStrictByteString "{"
- , B.intercalate (toStrictByteString ",") $ map helper2 m
- , toStrictByteString "}"
+ [ toLazyByteString "{"
+ , B.intercalate (toLazyByteString ",") $ map helper2 m
+ , toLazyByteString "}"
]
- helper2 :: (B.ByteString, Object) -> B.ByteString
+ helper2 :: (BS.ByteString, Object) -> B.ByteString
helper2 (k, v) = B.concat
- [ toStrictByteString "\""
+ [ toLazyByteString "\""
, encodeJson $ fromStrictByteString k
- , toStrictByteString "\":"
+ , toLazyByteString "\":"
, helper v
]
newtype Yaml = Yaml { unYaml :: B.ByteString }
instance SafeFromObject Yaml where
- safeFromObject = Yaml . Y.encode
+ safeFromObject = Yaml . encode
-- | Represents as an entire HTML 5 document by using the following:
--
@@ -68,31 +69,31 @@ newtype Html = Html { unHtml :: B.ByteString }
instance SafeFromObject Html where
safeFromObject o = Html $ B.concat
- [ toStrictByteString "\n"
+ [ toLazyByteString "\n" -- FIXME full doc or just fragment?
, helper o
- , toStrictByteString ""
+ , toLazyByteString ""
] where
helper :: Object -> B.ByteString
helper (Scalar s) = B.concat
- [ toStrictByteString ""
- , s
- , toStrictByteString "
"
+ [ toLazyByteString ""
+ , toLazyByteString s
+ , toLazyByteString "
"
]
- helper (Sequence []) = toStrictByteString ""
+ helper (Sequence []) = toLazyByteString ""
helper (Sequence s) = B.concat
- [ toStrictByteString ""
- , B.intercalate (toStrictByteString " ") $ map helper s
- , toStrictByteString " "
+ [ toLazyByteString ""
+ , B.intercalate (toLazyByteString " ") $ map helper s
+ , toLazyByteString " "
]
helper (Mapping m) = B.concat $
- toStrictByteString "" :
+ toLazyByteString "" :
map helper2 m ++
- [ toStrictByteString " " ]
- helper2 :: (B.ByteString, Object) -> B.ByteString
+ [ toLazyByteString " " ]
+ helper2 :: (BS.ByteString, Object) -> B.ByteString
helper2 (k, v) = B.concat $
- [ toStrictByteString ""
- , k
- , toStrictByteString " "
+ [ toLazyByteString " "
+ , toLazyByteString k
+ , toLazyByteString " "
, helper v
- , toStrictByteString " "
+ , toLazyByteString ""
]
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index 9236a35e..529910ba 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -23,8 +23,6 @@ module Web.Restful.Application
) where
import Web.Encodings
-import Data.Maybe (isJust)
-import Data.Function.Predicate (equals)
import Data.ByteString.Class
import qualified Data.ByteString.Lazy as B
@@ -105,7 +103,7 @@ takeJusts (Just x:rest) = x : takeJusts rest
toHackApplication :: RestfulApp resourceName model
=> resourceName
- -> HandlerMap resourceName
+ -> (resourceName -> Verb -> Handler)
-> Hack.Application
toHackApplication sampleRN hm env = do
let (Right resource) = splitPath $ Hack.pathInfo env
@@ -116,31 +114,11 @@ toHackApplication sampleRN hm env = do
verb = toVerb $ Hack.requestMethod env
rr :: RawRequest
rr = envToRawRequest urlParams' env
- case hm rn verb of
- (Just handler) -> do
- let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
- ctypes' = parseHttpAccept rawHttpAccept
- body <- runHandler handler rr
- let reps' = reps body
- ctypes = filter (\c -> isJust $ lookup c reps') ctypes'
- let handlerPair =
- case ctypes of
- [] -> Just $ head reps'
- (c:_) ->
- case filter (fst `equals` c) reps' of
- [pair] -> Just pair
- [] -> Nothing
- _ -> error "Overlapping reps"
- case handlerPair of
- Nothing -> response404 sampleRN $ env
- Just (ctype, Hack.Response status headers content) -> do
- content' <- responseWrapper sampleRN ctype content
- let response' = Hack.Response
- status
- (("Content-Type", ctype) : headers)
- content'
- return response'
- Nothing -> response404 sampleRN $ env
+ handler :: Handler
+ handler = hm rn verb
+ let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
+ ctypes' = parseHttpAccept rawHttpAccept
+ runResponse (handler rr) ctypes'
x -> error $ "Invalid matches: " ++ show x
envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
diff --git a/Web/Restful/Generic/ListDetail.hs b/Web/Restful/Generic/ListDetail.hs
index 00d48d57..6c4c45e7 100644
--- a/Web/Restful/Generic/ListDetail.hs
+++ b/Web/Restful/Generic/ListDetail.hs
@@ -25,7 +25,7 @@ import Data.ByteString.Class
class ToObject a => ListDetail a where
htmlDetail :: a -> String
- htmlDetail = fromStrictByteString . unHtml . safeFromObject . toObject
+ htmlDetail = fromLazyByteString . unHtml . safeFromObject . toObject
detailTitle :: a -> String
detailUrl :: a -> String
htmlList :: [a] -> String
@@ -42,14 +42,14 @@ class ToObject a => ListDetail a where
treeListSingle = toObject
newtype ItemList a = ItemList [a]
-instance ListDetail a => Response (ItemList a) where
+instance ListDetail a => HasReps (ItemList a) where
reps (ItemList l) =
- [ ("text/html", response 200 [] $ htmlList l)
- , ("application/json", response 200 [] $ unJson $ safeFromObject $ treeList l)
+ [ ("text/html", toLazyByteString $ htmlList l)
+ , ("application/json", unJson $ safeFromObject $ treeList l)
]
newtype ItemDetail a = ItemDetail a
-instance ListDetail a => Response (ItemDetail a) where
+instance ListDetail a => HasReps (ItemDetail a) where
reps (ItemDetail i) =
- [ ("text/html", response 200 [] $ htmlDetail i)
- , ("application/json", response 200 [] $ unJson $ safeFromObject $ toObject i)
+ [ ("text/html", toLazyByteString $ htmlDetail i)
+ , ("application/json", unJson $ safeFromObject $ toObject i)
]
diff --git a/Web/Restful/Handler.hs b/Web/Restful/Handler.hs
index b4a1f1fa..074240b6 100644
--- a/Web/Restful/Handler.hs
+++ b/Web/Restful/Handler.hs
@@ -15,28 +15,26 @@
--
---------------------------------------------------------
module Web.Restful.Handler
- ( Handler (..)
- , runHandler
- , HandlerMap
+ ( Handler
, liftHandler
+ , noHandler
) where
-import Web.Restful.Definitions
import Web.Restful.Request
import Web.Restful.Response
-data Handler = forall req. Request req => Handler (req -> IO ResponseWrapper)
+type Handler = RawRequest -> Response
-runHandler :: Handler -> RawRequest -> IO ResponseWrapper
-runHandler (Handler f) rreq = do
- let rparser = parseRequest
- case runRequestParser rparser rreq of
+liftHandler :: (Request req, HasReps rep)
+ => (req -> ResponseIO rep)
+ -> Handler
+liftHandler f req = liftRequest req >>= wrapResponse . f
+
+liftRequest :: (Request req, Monad m) => RawRequest -> m req
+liftRequest r =
+ case runRequestParser parseRequest r of
Left errors -> fail $ unlines errors -- FIXME
- Right req -> f req
+ Right req -> return req
-type HandlerMap a = a -> Verb -> Maybe Handler
-
-liftHandler :: (Request req, Response res)
- => (req -> IO res)
- -> Maybe Handler
-liftHandler f = Just . Handler $ fmap ResponseWrapper . f
+noHandler :: Handler
+noHandler = const notFound
diff --git a/Web/Restful/Helpers/Auth.hs b/Web/Restful/Helpers/Auth.hs
index 7d2c2481..7844e2c9 100644
--- a/Web/Restful/Helpers/Auth.hs
+++ b/Web/Restful/Helpers/Auth.hs
@@ -26,10 +26,10 @@ import Web.Restful
import Web.Restful.Constants
import Control.Applicative ((<$>), Applicative (..))
-import Control.Arrow (second)
import Control.Monad.Reader
import Data.Object
+import Data.Maybe (fromMaybe)
data AuthResource =
Check
@@ -48,7 +48,7 @@ instance ResourceName AuthResource (Maybe RpxnowApiKey) where
getHandler _ OpenidForward Get = liftHandler authOpenidForward
getHandler _ OpenidComplete Get = liftHandler authOpenidComplete
getHandler (Just key) LoginRpxnow Get = liftHandler $ rpxnowLogin key
- getHandler _ _ _ = Nothing
+ getHandler _ _ _ = noHandler
allValues =
Check
@@ -74,24 +74,20 @@ instance Show OIDFormReq where
show (OIDFormReq Nothing _) = ""
show (OIDFormReq (Just s) _) = "" ++ encodeHtml s ++
"
"
-data OIDFormRes = OIDFormRes String (Maybe String)
-instance Response OIDFormRes where
- reps (OIDFormRes s dest) = [("text/html", response 200 heads s)]
- where
- heads =
- case dest of
- Nothing -> []
- Just dest' ->
- [("Set-Cookie", "DEST=" ++ dest' ++ "; path=/")]
-authOpenidForm :: OIDFormReq -> IO OIDFormRes
-authOpenidForm m@(OIDFormReq _ dest) =
+
+authOpenidForm :: OIDFormReq -> ResponseIO GenResponse
+authOpenidForm m@(OIDFormReq _ dest) = do
let html =
show m ++
""
- in return $! OIDFormRes html dest
+ case dest of
+ Just dest' -> addCookie 20 "DEST" dest'
+ Nothing -> return ()
+ return $! HtmlResponse html
+
data OIDFReq = OIDFReq String String
instance Request OIDFReq where
parseRequest = do
@@ -101,14 +97,13 @@ instance Request OIDFReq where
show (Hack.serverPort env) ++
"/auth/openid/complete/"
return $! OIDFReq oid complete
-authOpenidForward :: OIDFReq -> IO GenResponse
+authOpenidForward :: OIDFReq -> Response
authOpenidForward (OIDFReq oid complete) = do
- res <- OpenId.getForwardUrl oid complete :: IO (Either String String)
- return $
- case res of
- Left err -> RedirectResponse $ "/auth/openid/?message=" ++
- encodeUrl err
- Right url -> RedirectResponse url
+ res <- liftIO $ OpenId.getForwardUrl oid complete
+ case res of
+ Left err -> redirect $ "/auth/openid/?message="
+ ++ encodeUrl (err :: String)
+ Right url -> redirect url
data OIDComp = OIDComp [(String, String)] (Maybe String)
instance Request OIDComp where
@@ -117,35 +112,17 @@ instance Request OIDComp where
let gets = rawGetParams rr
dest <- cookieParam "DEST"
return $! OIDComp gets dest
-data OIDCompRes = OIDCompResErr String
- | OIDCompResGood String (Maybe String)
-instance Response OIDCompRes where
- reps (OIDCompResErr err) =
- reps $ RedirectResponse
- $ "/auth/openid/?message=" ++
- encodeUrl err
- reps (OIDCompResGood ident Nothing) =
- reps $ OIDCompResGood ident (Just "/")
- reps (OIDCompResGood ident (Just dest)) =
- [("text/plain", response 303 heads "")] where
- heads =
- [ (authCookieName, ident)
- , resetCookie "DEST"
- , ("Location", dest)
- ]
-resetCookie :: String -> (String, String)
-resetCookie name =
- ("Set-Cookie",
- name ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
-
-authOpenidComplete :: OIDComp -> IO OIDCompRes
+authOpenidComplete :: OIDComp -> Response
authOpenidComplete (OIDComp gets' dest) = do
- res <- OpenId.authenticate gets' :: IO (Either String OpenId.Identifier)
- return $
- case res of
- Left err -> OIDCompResErr err
- Right (OpenId.Identifier ident) -> OIDCompResGood ident dest
+ res <- liftIO $ OpenId.authenticate gets'
+ case res of
+ Left err -> redirect $ "/auth/openid/?message="
+ ++ encodeUrl (err :: String)
+ Right (OpenId.Identifier ident) -> do
+ deleteCookie "DEST"
+ header authCookieName ident
+ redirect $ fromMaybe "/" dest
-- | token dest
data RpxnowRequest = RpxnowRequest String (Maybe String)
@@ -159,34 +136,25 @@ chopHash :: String -> String
chopHash ('#':rest) = rest
chopHash x = x
--- | dest identifier
-data RpxnowResponse = RpxnowResponse String (Maybe String)
-instance Response RpxnowResponse where
- reps (RpxnowResponse dest Nothing) =
- [("text/html", response 303 [("Location", dest)] "")]
- reps (RpxnowResponse dest (Just ident)) =
- [("text/html", response 303
- [ ("Location", dest)
- , (authCookieName, ident)
- ]
- "")]
-
rpxnowLogin :: String -- ^ api key
-> RpxnowRequest
- -> IO RpxnowResponse
+ -> Response
rpxnowLogin apiKey (RpxnowRequest token dest') = do
let dest = case dest' of
Nothing -> "/"
Just "" -> "/"
Just s -> s
- ident' <- Rpxnow.authenticate apiKey token
- return $ RpxnowResponse dest (Rpxnow.identifier `fmap` ident')
+ ident' <- liftIO $ Rpxnow.authenticate apiKey token
+ case ident' of
+ Nothing -> return ()
+ Just ident -> header authCookieName $ Rpxnow.identifier ident
+ redirect dest
data AuthRequest = AuthRequest (Maybe String)
instance Request AuthRequest where
parseRequest = AuthRequest `fmap` identifier
-authCheck :: AuthRequest -> IO Object
+authCheck :: AuthRequest -> ResponseIO Object
authCheck (AuthRequest Nothing) =
return $ toObject [("status", "notloggedin")]
authCheck (AuthRequest (Just i)) =
@@ -195,13 +163,7 @@ authCheck (AuthRequest (Just i)) =
, ("ident", i)
]
-authLogout :: () -> IO LogoutResponse
-authLogout _ = return LogoutResponse
-
-data LogoutResponse = LogoutResponse
-instance Response LogoutResponse where
- reps _ = map (second addCookie) $ reps tree where
- tree = toObject [("status", "loggedout")]
- addCookie (Hack.Response s h c) =
- Hack.Response s (h':h) c
- h' = resetCookie authCookieName
+authLogout :: () -> ResponseIO Object
+authLogout _ = do
+ deleteCookie authCookieName
+ return $ toObject [("status", "loggedout")]
diff --git a/Web/Restful/Resource.hs b/Web/Restful/Resource.hs
index afb1867c..2470c70d 100644
--- a/Web/Restful/Resource.hs
+++ b/Web/Restful/Resource.hs
@@ -49,7 +49,7 @@ class Show a => ResourceName a b | a -> b where
allValues :: [a]
-- | Find the handler for each resource name/verb pattern.
- getHandler :: b -> a -> Verb -> Maybe Handler
+ getHandler :: b -> a -> Verb -> Handler
-- FIXME add some overlap checking functions
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index 9d5e8221..19db94d4 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Response
@@ -14,110 +15,192 @@
--
---------------------------------------------------------
module Web.Restful.Response
- (
- -- * Response construction
- Response (..)
- , response
- -- * FIXME
+ ( formatW3
+ , HasReps (..)
+ , notFound
+ , wrapResponse
+ , ResponseIO
+ , ResponseT
+ , Response
+ , runResponse
+ , deleteCookie
+ , redirect
+ , addCookie
+ , header
, GenResponse (..)
- , ResponseWrapper (..)
- , ErrorResponse (..)
- , formatW3
- , UTCTime
+ , liftIO
) where
import Data.ByteString.Class
-import qualified Hack
import Data.Time.Format
import Data.Time.Clock
-import Web.Encodings
import System.Locale
import Data.Object
-import Data.List (intercalate)
+import qualified Data.ByteString.Lazy as B
+import Data.Object.Instances
+import Data.Maybe (fromJust)
+
+import Control.Monad.Trans
+
+import qualified Hack
type ContentType = String
--- | The output for a resource.
-class Response a where
- -- | Provide an ordered list of possible responses, depending on content
- -- type. If the user asked for a specific response type (like
+-- | Something which can be represented as multiple content types.
+-- Each content type is called a representation of the data.
+class HasReps a where
+ -- | Provide an ordered list of possible representations, depending on
+ -- content type. If the user asked for a specific response type (like
-- text/html), then that will get priority. If not, then the first
-- element in this list will be used.
- reps :: a -> [(ContentType, Hack.Response)]
+ reps :: a -> [(ContentType, B.ByteString)]
--- | Wrapper around 'Hack.Response' to allow arbitrary pieces of data to be
--- used for the body.
-response :: LazyByteString lbs
- => Int
- -> [(String, String)]
- -> lbs
- -> Hack.Response
-response a b c = Hack.Response a b $ toLazyByteString c
+-- | Wrap up any instance of 'HasReps'.
+data HasRepsW = forall a. HasReps a => HasRepsW a
-instance Response () where
- reps _ = [("text/plain", response 200 [] "")]
+instance HasReps HasRepsW where
+ reps (HasRepsW r) = reps r
-newtype ErrorResponse = ErrorResponse String
-instance Response ErrorResponse where
- reps (ErrorResponse s) = [("text/plain", response 500 [] s)]
+-- | The result of a request. This does not include possible headers.
+data Result =
+ Redirect String
+ | NotFound
+ | InternalError String
+ | Content HasRepsW
-data ResponseWrapper = forall res. Response res => ResponseWrapper res
-instance Response ResponseWrapper where
- reps (ResponseWrapper res) = reps res
+instance HasReps Result where
+ reps (Redirect s) = [("text/plain", toLazyByteString s)]
+ reps NotFound = [("text/plain", toLazyByteString "not found")] -- FIXME use the real 404 page
+ reps (InternalError s) = [("text/plain", toLazyByteString s)]
+ reps (Content r) = reps r
+
+getStatus :: Result -> Int
+getStatus (Redirect _) = 303
+getStatus NotFound = 404
+getStatus (InternalError _) = 500
+getStatus (Content _) = 200
+
+getHeaders :: Result -> [Header]
+getHeaders (Redirect s) = [Header "Location" s]
+getHeaders _ = []
+
+newtype ResponseT m a = ResponseT (m (Either Result a, [Header]))
+type ResponseIO = ResponseT IO
+type Response = ResponseIO HasRepsW
+
+runResponse :: Response -> [ContentType] -> IO Hack.Response
+runResponse (ResponseT inside) ctypesAll = do
+ (x, headers') <- inside
+ let extraHeaders =
+ case x of
+ Left r -> getHeaders r
+ Right _ -> []
+ headers <- mapM toPair (headers' ++ extraHeaders)
+ let outReps = either reps reps x
+ let statusCode =
+ case x of
+ Left r -> getStatus r
+ Right _ -> 200
+ (ctype, finalRep) <- chooseRep outReps ctypesAll
+ let headers'' = ("Content-Type", ctype) : headers
+ return $! Hack.Response statusCode headers'' finalRep
+
+chooseRep :: Monad m
+ => [(ContentType, B.ByteString)]
+ -> [ContentType]
+ -> m (ContentType, B.ByteString)
+chooseRep rs cs
+ | length rs == 0 = fail "All reps must have at least one value"
+ | otherwise = do
+ let availCs = map fst rs
+ case filter (`elem` availCs) cs of
+ [] -> return $ head rs
+ [ctype] -> return (ctype, fromJust $ lookup ctype rs)
+ _ -> fail "Overlapping representations"
+
+toPair :: Header -> IO (String, String)
+toPair (AddCookie minutes key value) = do
+ now <- getCurrentTime
+ let expires = addUTCTime (fromIntegral $ minutes * 60) now
+ return ("Set-Cookie", key ++ "=" ++ value ++"; path=/; expires="
+ ++ formatW3 expires)
+toPair (DeleteCookie key) = return
+ ("Set-Cookie",
+ key ++ "=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT")
+toPair (Header key value) = return (key, value)
+
+wrapResponse :: (Monad m, HasReps rep)
+ => ResponseT m rep
+ -> ResponseT m HasRepsW
+wrapResponse = fmap HasRepsW
+
+instance MonadTrans ResponseT where
+ lift ma = ResponseT $ do
+ a <- ma
+ return (Right a, [])
+
+instance MonadIO ResponseIO where
+ liftIO = lift
+
+redirect :: Monad m => String -> ResponseT m a
+redirect s = ResponseT (return (Left $ Redirect s, []))
+
+notFound :: Monad m => ResponseT m a
+notFound = ResponseT (return (Left NotFound, []))
+
+instance Monad m => Functor (ResponseT m) where
+ fmap f x = x >>= return . f
+
+instance Monad m => Monad (ResponseT m) where
+ return = lift . return
+ fail s = ResponseT (return (Left $ InternalError s, []))
+ (ResponseT mx) >>= f = ResponseT $ do
+ (x, hs1) <- mx
+ case x of
+ Left x' -> return (Left x', hs1)
+ Right a -> do
+ let (ResponseT b') = f a
+ (b, hs2) <- b'
+ return (b, hs1 ++ hs2)
+
+-- | Headers to be added to a 'Result'.
+data Header =
+ AddCookie Int String String
+ | DeleteCookie String
+ | Header String String
+
+addCookie :: Monad m => Int -> String -> String -> ResponseT m ()
+addCookie a b c = addHeader $ AddCookie a b c
+
+deleteCookie :: Monad m => String -> ResponseT m ()
+deleteCookie = addHeader . DeleteCookie
+
+header :: Monad m => String -> String -> ResponseT m ()
+header a b = addHeader $ Header a b
+
+addHeader :: Monad m => Header -> ResponseT m ()
+addHeader h = ResponseT (return (Right (), [h]))
+
+instance HasReps () where
+ reps _ = [("text/plain", toLazyByteString "")]
data GenResponse = HtmlResponse String
| ObjectResponse Object
| HtmlOrObjectResponse String Object
- | RedirectResponse String
- | PermissionDeniedResult String
- | NotFoundResponse String
-instance Response GenResponse where
- reps (HtmlResponse h) = [("text/html", response 200 [] h)]
+instance HasReps GenResponse where
+ reps (HtmlResponse h) = [("text/html", toLazyByteString h)]
reps (ObjectResponse t) = reps t
reps (HtmlOrObjectResponse h t) =
- ("text/html", response 200 [] h) : reps t
- reps (RedirectResponse url) = [("text/html", response 303 heads body)]
- where
- heads = [("Location", url)]
- body = "Redirecting to " ++ encodeHtml url ++ "
"
- reps (PermissionDeniedResult s) = [("text/plain", response 403 [] s)]
- reps (NotFoundResponse s) = [("text/plain", response 404 [] s)]
+ ("text/html", toLazyByteString h) : reps t
--- FIXME remove treeTo functions, replace with Object instances
-treeToJson :: Object -> String
-treeToJson (Scalar s) = '"' : encodeJson (fromStrictByteString s) ++ "\""
-treeToJson (Sequence l) =
- "[" ++ intercalate "," (map treeToJson l) ++ "]"
-treeToJson (Mapping m) =
- "{" ++ intercalate "," (map helper m) ++ "}" where
- helper (k, v) =
- treeToJson (Scalar k) ++
- ":" ++
- treeToJson v
-
-treeToHtml :: Object -> String
-treeToHtml (Scalar s) = encodeHtml $ fromStrictByteString s
-treeToHtml (Sequence l) =
- "" ++ concatMap (\e -> "" ++ treeToHtml e ++ " ") l ++
- " "
-treeToHtml (Mapping m) =
- "" ++
- concatMap (\(k, v) -> "" ++
- encodeHtml (fromStrictByteString k) ++
- " " ++
- "" ++
- treeToHtml v ++
- " ") m ++
- " "
-
-instance Response Object where
- reps tree =
- [ ("text/html", response 200 [] $ treeToHtml tree)
- , ("application/json", response 200 [] $ treeToJson tree)
+instance HasReps Object where
+ reps o =
+ [ ("text/html", unHtml $ safeFromObject o)
+ , ("application/json", unJson $ safeFromObject o)
+ , ("text/yaml", unYaml $ safeFromObject o)
]
-instance Response [(String, Hack.Response)] where
+instance HasReps [(ContentType, B.ByteString)] where
reps = id
-- FIXME put in a separate module (maybe Web.Encodings)
diff --git a/Web/Restful/Response/AtomFeed.hs b/Web/Restful/Response/AtomFeed.hs
index c79d9d3b..8f093a49 100644
--- a/Web/Restful/Response/AtomFeed.hs
+++ b/Web/Restful/Response/AtomFeed.hs
@@ -19,10 +19,9 @@ module Web.Restful.Response.AtomFeed
import Web.Restful.Response
-import Data.Time.Format
import Data.Time.Clock
import Web.Encodings
-import System.Locale
+import Data.ByteString.Class
data AtomFeed = AtomFeed
{ atomTitle :: String
@@ -31,9 +30,9 @@ data AtomFeed = AtomFeed
, atomUpdated :: UTCTime
, atomEntries :: [AtomFeedEntry]
}
-instance Response AtomFeed where
+instance HasReps AtomFeed where
reps e =
- [ ("application/atom+xml", response 200 [] $ show e)
+ [ ("application/atom+xml", toLazyByteString $ show e)
]
data AtomFeedEntry = AtomFeedEntry
diff --git a/Web/Restful/Response/Sitemap.hs b/Web/Restful/Response/Sitemap.hs
index 0167a9f5..92f2566a 100644
--- a/Web/Restful/Response/Sitemap.hs
+++ b/Web/Restful/Response/Sitemap.hs
@@ -23,6 +23,8 @@ import Web.Restful.Response
import Web.Encodings
import qualified Hack
import Web.Restful.Request
+import Data.ByteString.Class
+import Data.Time (UTCTime)
data SitemapLoc = AbsLoc String | RelLoc String
data SitemapChangeFreq = Always
@@ -79,12 +81,12 @@ instance Show SitemapResponse where
showLoc (AbsLoc s) = s
showLoc (RelLoc s) = prefix ++ s
-instance Response SitemapResponse where
+instance HasReps SitemapResponse where
reps res =
- [ ("text/xml", response 200 [] $ show res)
+ [ ("text/xml", toLazyByteString $ show res)
]
-sitemap :: IO [SitemapUrl] -> SitemapRequest -> IO SitemapResponse
+sitemap :: IO [SitemapUrl] -> SitemapRequest -> ResponseIO SitemapResponse
sitemap urls' req = do
- urls <- urls'
+ urls <- liftIO urls'
return $ SitemapResponse req urls
diff --git a/restful.cabal b/restful.cabal
index 4c28e492..a2f3f781 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -1,5 +1,5 @@
name: restful
-version: 0.1.1
+version: 0.1.2
license: BSD3
license-file: LICENSE
author: Michael Snoyman
From c75c72d9cb21d12042afe2fe9cce33896bf26f45 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Fri, 18 Sep 2009 09:36:47 +0300
Subject: [PATCH 017/624] Response wrapping and error handling done properly
---
Web/Restful/Application.hs | 48 ++++++++++++++++++--------------------
Web/Restful/Request.hs | 4 ++++
Web/Restful/Response.hs | 32 ++++++++++++-------------
3 files changed, 42 insertions(+), 42 deletions(-)
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index 529910ba..0ac40106 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -23,8 +23,8 @@ module Web.Restful.Application
) where
import Web.Encodings
-import Data.ByteString.Class
import qualified Data.ByteString.Lazy as B
+import Data.Object
import qualified Hack
import Hack.Middleware.CleanPath
@@ -59,15 +59,19 @@ class ResourceName a b => RestfulApp a b | a -> b where
, methodOverride
]
- -- | How to generate 404 pages. FIXME make more user-friendly.
- response404 :: a -> Hack.Env -> IO Hack.Response
- response404 _ = default404
-
-- | Wrappers for cleaning up responses. Especially intended for
-- beautifying static HTML. FIXME more user friendly.
responseWrapper :: a -> String -> B.ByteString -> IO B.ByteString
responseWrapper _ _ = return
+ -- | Output error response pages.
+ errorHandler :: a -> RawRequest -> ErrorResult -> HasRepsW
+ errorHandler _ rr NotFound = HasRepsW $ toObject $ "Not found: " ++ show rr
+ errorHandler _ _ (Redirect url) =
+ HasRepsW $ toObject $ "Redirect to: " ++ url
+ errorHandler _ _ (InternalError e) =
+ HasRepsW $ toObject $ "Internal server error: " ++ e
+
-- | Given a sample resource name (purely for typing reasons), generating
-- a Hack application.
toHackApp :: RestfulApp resourceName modelType
@@ -107,19 +111,20 @@ toHackApplication :: RestfulApp resourceName model
-> Hack.Application
toHackApplication sampleRN hm env = do
let (Right resource) = splitPath $ Hack.pathInfo env
- case findResourceNames resource of
- [] -> response404 sampleRN $ env
- [(rn, urlParams')] -> do
- let verb :: Verb
- verb = toVerb $ Hack.requestMethod env
- rr :: RawRequest
- rr = envToRawRequest urlParams' env
- handler :: Handler
- handler = hm rn verb
- let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
- ctypes' = parseHttpAccept rawHttpAccept
- runResponse (handler rr) ctypes'
- x -> error $ "Invalid matches: " ++ show x
+ let (handler, urlParams') =
+ case findResourceNames resource of
+ [] -> (noHandler, [])
+ [(rn, urlParams'')] ->
+ let verb = toVerb $ Hack.requestMethod env
+ in (hm rn verb, urlParams'')
+ x -> error $ "Invalid findResourceNames: " ++ show x
+ let rr = envToRawRequest urlParams' env
+ let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
+ ctypes' = parseHttpAccept rawHttpAccept
+ runResponse (errorHandler sampleRN rr)
+ (responseWrapper sampleRN)
+ ctypes'
+ (handler rr)
envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
envToRawRequest urlParams' env =
@@ -132,10 +137,3 @@ envToRawRequest urlParams' env =
rawCookie = tryLookup "" "Cookie" $ Hack.http env
cookies' = decodeCookies rawCookie :: [(String, String)]
in RawRequest rawPieces urlParams' gets' posts cookies' files env
-
-default404 :: Hack.Env -> IO Hack.Response
-default404 env = return $
- Hack.Response
- 404
- [("Content-Type", "text/plain")]
- $ toLazyByteString $ "Not found: " ++ Hack.pathInfo env
diff --git a/Web/Restful/Request.hs b/Web/Restful/Request.hs
index 4596458b..789de3ec 100644
--- a/Web/Restful/Request.hs
+++ b/Web/Restful/Request.hs
@@ -1,6 +1,7 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE OverlappingInstances #-}
+{-# LANGUAGE StandaloneDeriving #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Request
@@ -210,6 +211,9 @@ data RawRequest = RawRequest
, rawFiles :: [(ParamName, FileInfo)]
, rawEnv :: Hack.Env
}
+ deriving Show
+
+deriving instance Show FileInfo
-- | All GET paramater values with the given name.
getParams :: RawRequest -> ParamName -> [ParamValue]
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index 19db94d4..98fc13d8 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -29,6 +29,8 @@ module Web.Restful.Response
, header
, GenResponse (..)
, liftIO
+ , ErrorResult (..)
+ , HasRepsW (..)
) where
import Data.ByteString.Class
@@ -61,47 +63,43 @@ data HasRepsW = forall a. HasReps a => HasRepsW a
instance HasReps HasRepsW where
reps (HasRepsW r) = reps r
--- | The result of a request. This does not include possible headers.
-data Result =
+data ErrorResult =
Redirect String
| NotFound
| InternalError String
- | Content HasRepsW
-instance HasReps Result where
- reps (Redirect s) = [("text/plain", toLazyByteString s)]
- reps NotFound = [("text/plain", toLazyByteString "not found")] -- FIXME use the real 404 page
- reps (InternalError s) = [("text/plain", toLazyByteString s)]
- reps (Content r) = reps r
-
-getStatus :: Result -> Int
+getStatus :: ErrorResult -> Int
getStatus (Redirect _) = 303
getStatus NotFound = 404
getStatus (InternalError _) = 500
-getStatus (Content _) = 200
-getHeaders :: Result -> [Header]
+getHeaders :: ErrorResult -> [Header]
getHeaders (Redirect s) = [Header "Location" s]
getHeaders _ = []
-newtype ResponseT m a = ResponseT (m (Either Result a, [Header]))
+newtype ResponseT m a = ResponseT (m (Either ErrorResult a, [Header]))
type ResponseIO = ResponseT IO
type Response = ResponseIO HasRepsW
-runResponse :: Response -> [ContentType] -> IO Hack.Response
-runResponse (ResponseT inside) ctypesAll = do
+runResponse :: (ErrorResult -> HasRepsW)
+ -> (ContentType -> B.ByteString -> IO B.ByteString)
+ -> [ContentType]
+ -> Response
+ -> IO Hack.Response
+runResponse eh wrapper ctypesAll (ResponseT inside) = do
(x, headers') <- inside
let extraHeaders =
case x of
Left r -> getHeaders r
Right _ -> []
headers <- mapM toPair (headers' ++ extraHeaders)
- let outReps = either reps reps x
+ let outReps = either (reps . eh) reps x
let statusCode =
case x of
Left r -> getStatus r
Right _ -> 200
- (ctype, finalRep) <- chooseRep outReps ctypesAll
+ (ctype, selectedRep) <- chooseRep outReps ctypesAll
+ finalRep <- wrapper ctype selectedRep
let headers'' = ("Content-Type", ctype) : headers
return $! Hack.Response statusCode headers'' finalRep
From 649661e13351ce8d9ebfedc6c89508ca93ddfc3e Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Fri, 18 Sep 2009 13:29:08 +0300
Subject: [PATCH 018/624] Added ByteStringResponse and TODO list
---
TODO | 2 ++
Web/Restful/Response.hs | 6 ++++++
2 files changed, 8 insertions(+)
create mode 100644 TODO
diff --git a/TODO b/TODO
new file mode 100644
index 00000000..7fb61ea3
--- /dev/null
+++ b/TODO
@@ -0,0 +1,2 @@
+Static files and directories
+Better error handling for invalid arguments (currently 500 error)
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index 98fc13d8..f0db694b 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -31,6 +31,7 @@ module Web.Restful.Response
, liftIO
, ErrorResult (..)
, HasRepsW (..)
+ , byteStringResponse
) where
import Data.ByteString.Class
@@ -185,11 +186,16 @@ instance HasReps () where
data GenResponse = HtmlResponse String
| ObjectResponse Object
| HtmlOrObjectResponse String Object
+ | ByteStringResponse ContentType B.ByteString
instance HasReps GenResponse where
reps (HtmlResponse h) = [("text/html", toLazyByteString h)]
reps (ObjectResponse t) = reps t
reps (HtmlOrObjectResponse h t) =
("text/html", toLazyByteString h) : reps t
+ reps (ByteStringResponse ct con) = [(ct, con)]
+
+byteStringResponse :: LazyByteString lbs => ContentType -> lbs -> GenResponse
+byteStringResponse ct = ByteStringResponse ct . toLazyByteString
instance HasReps Object where
reps o =
From 4a0d7baa68ec3d1e62b7a811d4831b980b976101 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Sun, 20 Sep 2009 23:26:30 +0300
Subject: [PATCH 019/624] Slurp paths and static helper
---
TODO | 1 +
Web/Restful/Helpers/Auth.hs | 2 +-
Web/Restful/Helpers/Static.hs | 54 +++++++++++++++++++++++++++++++++++
Web/Restful/Resource.hs | 25 +++++++++++++---
Web/Restful/Response.hs | 6 +++-
restful.cabal | 1 +
6 files changed, 83 insertions(+), 6 deletions(-)
create mode 100644 Web/Restful/Helpers/Static.hs
diff --git a/TODO b/TODO
index 7fb61ea3..443e2ed8 100644
--- a/TODO
+++ b/TODO
@@ -1,2 +1,3 @@
Static files and directories
Better error handling for invalid arguments (currently 500 error)
+Include request getting in Response monad.
diff --git a/Web/Restful/Helpers/Auth.hs b/Web/Restful/Helpers/Auth.hs
index 7844e2c9..067cd551 100644
--- a/Web/Restful/Helpers/Auth.hs
+++ b/Web/Restful/Helpers/Auth.hs
@@ -86,7 +86,7 @@ authOpenidForm m@(OIDFormReq _ dest) = do
case dest of
Just dest' -> addCookie 20 "DEST" dest'
Nothing -> return ()
- return $! HtmlResponse html
+ return $! htmlResponse html
data OIDFReq = OIDFReq String String
instance Request OIDFReq where
diff --git a/Web/Restful/Helpers/Static.hs b/Web/Restful/Helpers/Static.hs
new file mode 100644
index 00000000..9d88131c
--- /dev/null
+++ b/Web/Restful/Helpers/Static.hs
@@ -0,0 +1,54 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+---------------------------------------------------------
+--
+-- Module : Web.Restful.Helpers.Static
+-- Copyright : Michael Snoyman
+-- License : BSD3
+--
+-- Maintainer : Michael Snoyman
+-- Stability : Unstable
+-- Portability : portable
+--
+-- Serve static files from a Restful app.
+--
+---------------------------------------------------------
+module Web.Restful.Helpers.Static
+ ( serveStatic
+ , FileLookup
+ ) where
+
+import qualified Data.ByteString as B
+
+import Web.Restful
+
+type FileLookup = FilePath -> IO (Maybe B.ByteString)
+
+serveStatic :: FileLookup -> Verb -> Handler
+serveStatic fl Get = liftHandler $ getStatic fl
+serveStatic _ _ = noHandler
+
+newtype StaticReq = StaticReq FilePath
+instance Request StaticReq where
+ parseRequest = StaticReq `fmap` urlParam "filepath" -- FIXME check for ..
+
+getStatic :: FileLookup -> StaticReq -> ResponseIO GenResponse
+getStatic fl (StaticReq fp) = do
+ content <- liftIO $ fl fp
+ case content of
+ Nothing -> notFound
+ Just bs -> return $ byteStringResponse (mimeType $ ext fp) bs
+
+mimeType :: String -> String
+mimeType "jpg" = "image/jpeg"
+mimeType "jpeg" = "image/jpeg"
+mimeType "js" = "text/javascript"
+mimeType "css" = "text/css"
+mimeType "html" = "text/html"
+mimeType "png" = "image/png"
+mimeType "gif" = "image/gif"
+mimeType "txt" = "text/plain"
+mimeType _ = "application/octet-stream"
+
+ext :: String -> String
+ext = reverse . fst . break (== '.') . reverse
diff --git a/Web/Restful/Resource.hs b/Web/Restful/Resource.hs
index 2470c70d..d253edb3 100644
--- a/Web/Restful/Resource.hs
+++ b/Web/Restful/Resource.hs
@@ -22,12 +22,18 @@ module Web.Restful.Resource
import Data.List.Split (splitOn)
import Web.Restful.Definitions
import Web.Restful.Handler
+import Data.List (intercalate)
data ResourcePatternPiece =
Static String
| Dynamic String
+ | Slurp String -- ^ take up the rest of the pieces. must be last
deriving Show
+isSlurp :: ResourcePatternPiece -> Bool
+isSlurp (Slurp _) = True
+isSlurp _ = False
+
type ResourcePattern = [ResourcePatternPiece]
fromString :: String -> ResourcePattern
@@ -35,12 +41,16 @@ fromString = map fromString' . filter (not . null) . splitOn "/"
fromString' :: String -> ResourcePatternPiece
fromString' ('$':rest) = Dynamic rest
+fromString' ('*':rest) = Slurp rest
fromString' x = Static x
class Show a => ResourceName a b | a -> b where
-- | Get the URL pattern for each different resource name.
-- Something like /foo/$bar/baz/ will match the regular expression
-- /foo/(\\w*)/baz/, matching the middle part with the urlParam bar.
+ --
+ -- Also, /foo/*bar/ will match /foo/[anything else], capturing the value
+ -- into the bar urlParam.
resourcePattern :: a -> String
-- | Get all possible values for resource names.
@@ -58,14 +68,21 @@ type SMap = [(String, String)]
data CheckPatternReturn = StaticMatch | DynamicMatch (String, String) | NoMatch
checkPattern :: ResourcePattern -> Resource -> Maybe SMap
-checkPattern rp r =
- if length rp /= length r
- then Nothing
- else combine [] $ zipWith checkPattern' rp r
+checkPattern rp r
+ | length rp /= 0 && isSlurp (last rp) = do
+ let rp' = init rp
+ (r1, r2) = splitAt (length rp') r
+ smap <- checkPattern rp' r1
+ let slurpValue = intercalate "/" r2
+ Slurp slurpKey = last rp
+ return $ (slurpKey, slurpValue) : smap
+ | length rp /= length r = Nothing
+ | otherwise = combine [] $ zipWith checkPattern' rp r
checkPattern' :: ResourcePatternPiece -> String -> CheckPatternReturn
checkPattern' (Static x) y = if x == y then StaticMatch else NoMatch
checkPattern' (Dynamic x) y = DynamicMatch (x, y)
+checkPattern' (Slurp x) _ = error $ "Slurp pattern " ++ x ++ " must be last"
combine :: SMap -> [CheckPatternReturn] -> Maybe SMap
combine s [] = Just $ reverse s
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index f0db694b..5336bfb9 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -32,6 +32,7 @@ module Web.Restful.Response
, ErrorResult (..)
, HasRepsW (..)
, byteStringResponse
+ , htmlResponse
) where
import Data.ByteString.Class
@@ -183,7 +184,7 @@ addHeader h = ResponseT (return (Right (), [h]))
instance HasReps () where
reps _ = [("text/plain", toLazyByteString "")]
-data GenResponse = HtmlResponse String
+data GenResponse = HtmlResponse B.ByteString
| ObjectResponse Object
| HtmlOrObjectResponse String Object
| ByteStringResponse ContentType B.ByteString
@@ -197,6 +198,9 @@ instance HasReps GenResponse where
byteStringResponse :: LazyByteString lbs => ContentType -> lbs -> GenResponse
byteStringResponse ct = ByteStringResponse ct . toLazyByteString
+htmlResponse :: LazyByteString lbs => lbs -> GenResponse
+htmlResponse = HtmlResponse . toLazyByteString
+
instance HasReps Object where
reps o =
[ ("text/html", unHtml $ safeFromObject o)
diff --git a/restful.cabal b/restful.cabal
index a2f3f781..73dce39d 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -43,6 +43,7 @@ library
Data.Object.Instances,
Hack.Middleware.MethodOverride,
Web.Restful.Helpers.Auth,
+ Web.Restful.Helpers.Static,
Web.Restful.Response.AtomFeed,
Web.Restful.Response.Sitemap,
Web.Restful.Generic.ListDetail
From f4dc87bab6430d782474fa0b0e84c076d1f67ed7 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Mon, 21 Sep 2009 00:02:38 +0300
Subject: [PATCH 020/624] getRequest for the Response monad
---
TODO | 2 --
Web/Restful/Application.hs | 3 ++-
Web/Restful/Handler.hs | 14 +++++--------
Web/Restful/Response.hs | 40 ++++++++++++++++++++++++++------------
4 files changed, 35 insertions(+), 24 deletions(-)
diff --git a/TODO b/TODO
index 443e2ed8..052dfd91 100644
--- a/TODO
+++ b/TODO
@@ -1,3 +1 @@
-Static files and directories
Better error handling for invalid arguments (currently 500 error)
-Include request getting in Response monad.
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index 0ac40106..3c61f556 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -124,7 +124,8 @@ toHackApplication sampleRN hm env = do
runResponse (errorHandler sampleRN rr)
(responseWrapper sampleRN)
ctypes'
- (handler rr)
+ handler
+ rr
envToRawRequest :: [(ParamName, ParamValue)] -> Hack.Env -> RawRequest
envToRawRequest urlParams' env =
diff --git a/Web/Restful/Handler.hs b/Web/Restful/Handler.hs
index 074240b6..bcfd86f9 100644
--- a/Web/Restful/Handler.hs
+++ b/Web/Restful/Handler.hs
@@ -23,18 +23,14 @@ module Web.Restful.Handler
import Web.Restful.Request
import Web.Restful.Response
-type Handler = RawRequest -> Response
+type Handler = Response -- FIXME maybe move some stuff around now...
liftHandler :: (Request req, HasReps rep)
=> (req -> ResponseIO rep)
-> Handler
-liftHandler f req = liftRequest req >>= wrapResponse . f
-
-liftRequest :: (Request req, Monad m) => RawRequest -> m req
-liftRequest r =
- case runRequestParser parseRequest r of
- Left errors -> fail $ unlines errors -- FIXME
- Right req -> return req
+liftHandler f = do
+ req <- getRequest
+ wrapResponse $ f req
noHandler :: Handler
-noHandler = const notFound
+noHandler = notFound
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index 5336bfb9..b5d1c1fb 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -33,6 +33,7 @@ module Web.Restful.Response
, HasRepsW (..)
, byteStringResponse
, htmlResponse
+ , getRequest
) where
import Data.ByteString.Class
@@ -44,7 +45,10 @@ import qualified Data.ByteString.Lazy as B
import Data.Object.Instances
import Data.Maybe (fromJust)
+import Web.Restful.Request
+
import Control.Monad.Trans
+import Control.Monad (liftM)
import qualified Hack
@@ -79,7 +83,8 @@ getHeaders :: ErrorResult -> [Header]
getHeaders (Redirect s) = [Header "Location" s]
getHeaders _ = []
-newtype ResponseT m a = ResponseT (m (Either ErrorResult a, [Header]))
+newtype ResponseT m a =
+ ResponseT (RawRequest -> m (Either ErrorResult a, [Header]))
type ResponseIO = ResponseT IO
type Response = ResponseIO HasRepsW
@@ -87,9 +92,10 @@ runResponse :: (ErrorResult -> HasRepsW)
-> (ContentType -> B.ByteString -> IO B.ByteString)
-> [ContentType]
-> Response
+ -> RawRequest
-> IO Hack.Response
-runResponse eh wrapper ctypesAll (ResponseT inside) = do
- (x, headers') <- inside
+runResponse eh wrapper ctypesAll (ResponseT inside) rr = do
+ (x, headers') <- inside rr
let extraHeaders =
case x of
Left r -> getHeaders r
@@ -135,7 +141,7 @@ wrapResponse :: (Monad m, HasReps rep)
wrapResponse = fmap HasRepsW
instance MonadTrans ResponseT where
- lift ma = ResponseT $ do
+ lift ma = ResponseT $ const $ do
a <- ma
return (Right a, [])
@@ -143,24 +149,24 @@ instance MonadIO ResponseIO where
liftIO = lift
redirect :: Monad m => String -> ResponseT m a
-redirect s = ResponseT (return (Left $ Redirect s, []))
+redirect s = ResponseT (const $ return (Left $ Redirect s, []))
notFound :: Monad m => ResponseT m a
-notFound = ResponseT (return (Left NotFound, []))
+notFound = ResponseT (const $ return (Left NotFound, []))
instance Monad m => Functor (ResponseT m) where
- fmap f x = x >>= return . f
+ fmap = liftM
instance Monad m => Monad (ResponseT m) where
return = lift . return
- fail s = ResponseT (return (Left $ InternalError s, []))
- (ResponseT mx) >>= f = ResponseT $ do
- (x, hs1) <- mx
+ fail s = ResponseT (const $ return (Left $ InternalError s, []))
+ (ResponseT mx) >>= f = ResponseT $ \rr -> do
+ (x, hs1) <- mx rr
case x of
Left x' -> return (Left x', hs1)
Right a -> do
let (ResponseT b') = f a
- (b, hs2) <- b'
+ (b, hs2) <- b' rr
return (b, hs1 ++ hs2)
-- | Headers to be added to a 'Result'.
@@ -179,7 +185,7 @@ header :: Monad m => String -> String -> ResponseT m ()
header a b = addHeader $ Header a b
addHeader :: Monad m => Header -> ResponseT m ()
-addHeader h = ResponseT (return (Right (), [h]))
+addHeader h = ResponseT (const $ return (Right (), [h]))
instance HasReps () where
reps _ = [("text/plain", toLazyByteString "")]
@@ -214,3 +220,13 @@ instance HasReps [(ContentType, B.ByteString)] where
-- FIXME put in a separate module (maybe Web.Encodings)
formatW3 :: UTCTime -> String
formatW3 = formatTime defaultTimeLocale "%FT%X-08:00"
+
+getRequest :: (Monad m, Request r) => ResponseT m r
+getRequest = ResponseT $ \rr -> return (helper rr, []) where
+ helper :: Request r
+ => RawRequest
+ -> Either ErrorResult r
+ helper rr =
+ case runRequestParser parseRequest rr of
+ Left errors -> Left $ InternalError $ unlines errors -- FIXME better error output
+ Right r -> Right r
From 2a958c1a8f0a7479287376630f03c6c6884f3178 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Mon, 21 Sep 2009 01:00:04 +0300
Subject: [PATCH 021/624] Better error handling for invalid arguments
---
TODO | 1 -
Web/Restful/Application.hs | 5 +++++
Web/Restful/Request.hs | 15 +++++++--------
Web/Restful/Response.hs | 4 +++-
4 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/TODO b/TODO
index 052dfd91..e69de29b 100644
--- a/TODO
+++ b/TODO
@@ -1 +0,0 @@
-Better error handling for invalid arguments (currently 500 error)
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index 3c61f556..017824f2 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -71,6 +71,11 @@ class ResourceName a b => RestfulApp a b | a -> b where
HasRepsW $ toObject $ "Redirect to: " ++ url
errorHandler _ _ (InternalError e) =
HasRepsW $ toObject $ "Internal server error: " ++ e
+ errorHandler _ _ (InvalidArgs ia) =
+ HasRepsW $ toObject $
+ [ ("errorMsg", toObject "Invalid arguments")
+ , ("messages", toObject ia)
+ ]
-- | Given a sample resource name (purely for typing reasons), generating
-- a Hack application.
diff --git a/Web/Restful/Request.hs b/Web/Restful/Request.hs
index 789de3ec..30a13770 100644
--- a/Web/Restful/Request.hs
+++ b/Web/Restful/Request.hs
@@ -109,7 +109,7 @@ tryReadParams:: Parameter a
tryReadParams name params =
case readParams params of
Left s -> do
- tell [name ++ ": " ++ s]
+ tell [(name, s)]
return $
error $
"Trying to evaluate nonpresent parameter " ++
@@ -185,16 +185,15 @@ requestPath = do
q' -> q'
return $! Hack.pathInfo env ++ q
-type RequestParser a = WriterT [ParamError] (Reader RawRequest) a
-instance Applicative (WriterT [ParamError] (Reader RawRequest)) where
+type RequestParser a = WriterT [(ParamName, ParamError)] (Reader RawRequest) a
+instance Applicative (WriterT [(ParamName, ParamError)] (Reader RawRequest)) where
pure = return
- f <*> a = do
- f' <- f
- a' <- a
- return $! f' a'
+ (<*>) = ap
-- | Parse a request into either the desired 'Request' or a list of errors.
-runRequestParser :: RequestParser a -> RawRequest -> Either [ParamError] a
+runRequestParser :: RequestParser a
+ -> RawRequest
+ -> Either [(ParamName, ParamError)] a
runRequestParser p req =
let (val, errors) = (runReader (runWriterT p)) req
in case errors of
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index b5d1c1fb..4fbdfa3f 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -73,11 +73,13 @@ data ErrorResult =
Redirect String
| NotFound
| InternalError String
+ | InvalidArgs [(String, String)]
getStatus :: ErrorResult -> Int
getStatus (Redirect _) = 303
getStatus NotFound = 404
getStatus (InternalError _) = 500
+getStatus (InvalidArgs _) = 400
getHeaders :: ErrorResult -> [Header]
getHeaders (Redirect s) = [Header "Location" s]
@@ -228,5 +230,5 @@ getRequest = ResponseT $ \rr -> return (helper rr, []) where
-> Either ErrorResult r
helper rr =
case runRequestParser parseRequest rr of
- Left errors -> Left $ InternalError $ unlines errors -- FIXME better error output
+ Left errors -> Left $ InvalidArgs errors -- FIXME better error output
Right r -> Right r
From 0519b99fedb9c05123f394cd03e466e8819e9e07 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Mon, 21 Sep 2009 22:21:21 +0300
Subject: [PATCH 022/624] Refactored and documented Response and Handler
---
Web/Restful/Application.hs | 14 +--
Web/Restful/Handler.hs | 135 +++++++++++++++++++--
Web/Restful/Helpers/Auth.hs | 61 +++++-----
Web/Restful/Helpers/Static.hs | 11 +-
Web/Restful/Response.hs | 196 +++++++------------------------
Web/Restful/Response/AtomFeed.hs | 1 +
Web/Restful/Response/Sitemap.hs | 6 +-
Web/Restful/Utils.hs | 18 ++-
8 files changed, 230 insertions(+), 212 deletions(-)
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index 017824f2..6f4f72ac 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -65,14 +65,14 @@ class ResourceName a b => RestfulApp a b | a -> b where
responseWrapper _ _ = return
-- | Output error response pages.
- errorHandler :: a -> RawRequest -> ErrorResult -> HasRepsW
- errorHandler _ rr NotFound = HasRepsW $ toObject $ "Not found: " ++ show rr
+ errorHandler :: a -> RawRequest -> ErrorResult -> Reps
+ errorHandler _ rr NotFound = reps $ toObject $ "Not found: " ++ show rr
errorHandler _ _ (Redirect url) =
- HasRepsW $ toObject $ "Redirect to: " ++ url
+ reps $ toObject $ "Redirect to: " ++ url
errorHandler _ _ (InternalError e) =
- HasRepsW $ toObject $ "Internal server error: " ++ e
+ reps $ toObject $ "Internal server error: " ++ e
errorHandler _ _ (InvalidArgs ia) =
- HasRepsW $ toObject $
+ reps $ toObject $
[ ("errorMsg", toObject "Invalid arguments")
, ("messages", toObject ia)
]
@@ -118,7 +118,7 @@ toHackApplication sampleRN hm env = do
let (Right resource) = splitPath $ Hack.pathInfo env
let (handler, urlParams') =
case findResourceNames resource of
- [] -> (noHandler, [])
+ [] -> (notFound, [])
[(rn, urlParams'')] ->
let verb = toVerb $ Hack.requestMethod env
in (hm rn verb, urlParams'')
@@ -126,7 +126,7 @@ toHackApplication sampleRN hm env = do
let rr = envToRawRequest urlParams' env
let rawHttpAccept = tryLookup "" "Accept" $ Hack.http env
ctypes' = parseHttpAccept rawHttpAccept
- runResponse (errorHandler sampleRN rr)
+ runHandler (errorHandler sampleRN rr)
(responseWrapper sampleRN)
ctypes'
handler
diff --git a/Web/Restful/Handler.hs b/Web/Restful/Handler.hs
index bcfd86f9..3a9454cf 100644
--- a/Web/Restful/Handler.hs
+++ b/Web/Restful/Handler.hs
@@ -1,6 +1,8 @@
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances #-}
---------------------------------------------------------
--
-- Module : Web.Restful.Handler
@@ -15,22 +17,133 @@
--
---------------------------------------------------------
module Web.Restful.Handler
- ( Handler
- , liftHandler
- , noHandler
+ ( -- * Handler monad
+ HandlerT
+ , HandlerIO
+ , Handler
+ , runHandler
+ , getRequest
+ , liftIO
+ -- * Special handlers
+ , redirect
+ , notFound
+ -- * Setting headers
+ , addCookie
+ , deleteCookie
+ , header
) where
import Web.Restful.Request
import Web.Restful.Response
-type Handler = Response -- FIXME maybe move some stuff around now...
+import Control.Monad.Trans
+import Control.Monad (liftM)
-liftHandler :: (Request req, HasReps rep)
- => (req -> ResponseIO rep)
+import Data.Maybe (fromJust)
+import qualified Data.ByteString.Lazy as B
+import qualified Hack
+
+------ Handler monad
+newtype HandlerT m a =
+ HandlerT (RawRequest -> m (Either ErrorResult a, [Header]))
+type HandlerIO = HandlerT IO
+type Handler = HandlerIO Reps
+
+runHandler :: (ErrorResult -> Reps)
+ -> (ContentType -> B.ByteString -> IO B.ByteString)
+ -> [ContentType]
-> Handler
-liftHandler f = do
- req <- getRequest
- wrapResponse $ f req
+ -> RawRequest
+ -> IO Hack.Response
+runHandler eh wrapper ctypesAll (HandlerT inside) rr = do
+ (x, headers') <- inside rr
+ let extraHeaders =
+ case x of
+ Left r -> getHeaders r
+ Right _ -> []
+ headers <- mapM toPair (headers' ++ extraHeaders)
+ let outReps = either (reps . eh) reps x
+ let statusCode =
+ case x of
+ Left r -> getStatus r
+ Right _ -> 200
+ (ctype, selectedRep) <- chooseRep outReps ctypesAll
+ finalRep <- wrapper ctype selectedRep
+ let headers'' = ("Content-Type", ctype) : headers
+ return $! Hack.Response statusCode headers'' finalRep
-noHandler :: Handler
-noHandler = notFound
+chooseRep :: Monad m
+ => [(ContentType, B.ByteString)]
+ -> [ContentType]
+ -> m (ContentType, B.ByteString)
+chooseRep rs cs
+ | length rs == 0 = fail "All reps must have at least one value"
+ | otherwise = do
+ let availCs = map fst rs
+ case filter (`elem` availCs) cs of
+ [] -> return $ head rs
+ [ctype] -> return (ctype, fromJust $ lookup ctype rs)
+ _ -> fail "Overlapping representations"
+
+instance MonadTrans HandlerT where
+ lift ma = HandlerT $ const $ do
+ a <- ma
+ return (Right a, [])
+
+instance MonadIO HandlerIO where
+ liftIO = lift
+
+instance Monad m => Functor (HandlerT m) where
+ fmap = liftM
+
+instance Monad m => Monad (HandlerT m) where
+ return = lift . return
+ fail s = HandlerT (const $ return (Left $ InternalError s, []))
+ (HandlerT mx) >>= f = HandlerT $ \rr -> do
+ (x, hs1) <- mx rr
+ case x of
+ Left x' -> return (Left x', hs1)
+ Right a -> do
+ let (HandlerT b') = f a
+ (b, hs2) <- b' rr
+ return (b, hs1 ++ hs2)
+
+-- | Parse a request in the Handler monad. On failure, return a 400 error.
+getRequest :: (Monad m, Request r) => HandlerT m r
+getRequest = HandlerT $ \rr -> return (helper rr, []) where
+ helper :: Request r
+ => RawRequest
+ -> Either ErrorResult r
+ helper rr =
+ case runRequestParser parseRequest rr of
+ Left errors -> Left $ InvalidArgs errors
+ Right r -> Right r
+
+------ Special handlers
+-- | Redirect to the given URL.
+redirect :: Monad m => String -> HandlerT m a
+redirect s = HandlerT (const $ return (Left $ Redirect s, []))
+
+-- | Return a 404 not found page. Also denotes no handler available.
+notFound :: Monad m => HandlerT m a
+notFound = HandlerT (const $ return (Left NotFound, []))
+
+------- Headers
+-- | Set the cookie on the client.
+addCookie :: Monad m
+ => Int -- ^ minutes to timeout
+ -> String -- ^ key
+ -> String -- ^ value
+ -> HandlerT m ()
+addCookie a b c = addHeader $ AddCookie a b c
+
+-- | Unset the cookie on the client.
+deleteCookie :: Monad m => String -> HandlerT m ()
+deleteCookie = addHeader . DeleteCookie
+
+-- | Set an arbitrary header on the client.
+header :: Monad m => String -> String -> HandlerT m ()
+header a b = addHeader $ Header a b
+
+addHeader :: Monad m => Header -> HandlerT m ()
+addHeader h = HandlerT (const $ return (Right (), [h]))
diff --git a/Web/Restful/Helpers/Auth.hs b/Web/Restful/Helpers/Auth.hs
index 067cd551..fec60a3d 100644
--- a/Web/Restful/Helpers/Auth.hs
+++ b/Web/Restful/Helpers/Auth.hs
@@ -28,7 +28,6 @@ import Web.Restful.Constants
import Control.Applicative ((<$>), Applicative (..))
import Control.Monad.Reader
-import Data.Object
import Data.Maybe (fromMaybe)
data AuthResource =
@@ -42,13 +41,13 @@ data AuthResource =
type RpxnowApiKey = String -- FIXME newtype
instance ResourceName AuthResource (Maybe RpxnowApiKey) where
- getHandler _ Check Get = liftHandler authCheck
- getHandler _ Logout Get = liftHandler authLogout
- getHandler _ Openid Get = liftHandler authOpenidForm
- getHandler _ OpenidForward Get = liftHandler authOpenidForward
- getHandler _ OpenidComplete Get = liftHandler authOpenidComplete
- getHandler (Just key) LoginRpxnow Get = liftHandler $ rpxnowLogin key
- getHandler _ _ _ = noHandler
+ getHandler _ Check Get = authCheck
+ getHandler _ Logout Get = authLogout
+ getHandler _ Openid Get = authOpenidForm
+ getHandler _ OpenidForward Get = authOpenidForward
+ getHandler _ OpenidComplete Get = authOpenidComplete
+ getHandler (Just key) LoginRpxnow Get = rpxnowLogin key
+ getHandler _ _ _ = notFound
allValues =
Check
@@ -75,8 +74,9 @@ instance Show OIDFormReq where
show (OIDFormReq (Just s) _) = "" ++ encodeHtml s ++
"
"
-authOpenidForm :: OIDFormReq -> ResponseIO GenResponse
-authOpenidForm m@(OIDFormReq _ dest) = do
+authOpenidForm :: Handler
+authOpenidForm = do
+ m@(OIDFormReq _ dest) <- getRequest
let html =
show m ++
""
case dest of
- Just dest' -> addCookie 20 "DEST" dest'
+ Just dest' -> addCookie 120 "DEST" dest'
Nothing -> return ()
htmlResponse html
diff --git a/Web/Restful/Request.hs b/Web/Restful/Request.hs
index 6a8d9ee4..50886fc4 100644
--- a/Web/Restful/Request.hs
+++ b/Web/Restful/Request.hs
@@ -160,7 +160,7 @@ hackHeaderParam name = do
-- | Extract the cookie which specifies the identifier for a logged in
-- user.
identifier :: Parameter a => RequestParser a
-identifier = hackHeaderParam authCookieName
+identifier = hackHeaderParam authCookieName -- FIXME better error message
-- | Get the raw 'Hack.Env' value.
parseEnv :: RequestParser Hack.Env
@@ -278,6 +278,12 @@ instance Parameter Day where
then Right $ fromGregorian y m d
else Left $ "Invalid date: " ++ s
+-- for checkboxes; checks for presence
+instance Parameter Bool where
+ readParams [] = Right False
+ readParams [_] = Right True
+ readParams x = Left $ "Invalid Bool parameter: " ++ show x
+
-- | The input for a resource.
--
-- Each resource can define its own instance of 'Request' and then more
diff --git a/Web/Restful/Utils.hs b/Web/Restful/Utils.hs
index 66e36db7..2862fa2c 100644
--- a/Web/Restful/Utils.hs
+++ b/Web/Restful/Utils.hs
@@ -45,7 +45,7 @@ tryLookup def key = fromMaybe def . lookup key
-- | Format a 'UTCTime' in W3 format; useful for setting cookies.
formatW3 :: UTCTime -> String
-formatW3 = formatTime defaultTimeLocale "%FT%X-08:00" -- FIXME time zone?
+formatW3 = formatTime defaultTimeLocale "%FT%X-00:00"
----- Testing
testSuite :: Test
From 7df28f4f30c2c385f0f97830403153300c0a7fe0 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Thu, 1 Oct 2009 21:24:41 +0200
Subject: [PATCH 028/624] Moved formatW3 to web-encodings package
---
Web/Restful/Response.hs | 2 +-
Web/Restful/Response/AtomFeed.hs | 1 -
Web/Restful/Response/Sitemap.hs | 1 -
Web/Restful/Utils.hs | 9 ---------
restful.cabal | 4 ++--
5 files changed, 3 insertions(+), 14 deletions(-)
diff --git a/Web/Restful/Response.hs b/Web/Restful/Response.hs
index fc0fd5ce..dff158b9 100644
--- a/Web/Restful/Response.hs
+++ b/Web/Restful/Response.hs
@@ -40,7 +40,7 @@ import Data.Object
import qualified Data.ByteString.Lazy as B
import Data.Object.Instances
-import Web.Restful.Utils (formatW3)
+import Web.Encodings (formatW3)
import Test.Framework (testGroup, Test)
diff --git a/Web/Restful/Response/AtomFeed.hs b/Web/Restful/Response/AtomFeed.hs
index 323c0676..8f093a49 100644
--- a/Web/Restful/Response/AtomFeed.hs
+++ b/Web/Restful/Response/AtomFeed.hs
@@ -18,7 +18,6 @@ module Web.Restful.Response.AtomFeed
) where
import Web.Restful.Response
-import Web.Restful.Utils
import Data.Time.Clock
import Web.Encodings
diff --git a/Web/Restful/Response/Sitemap.hs b/Web/Restful/Response/Sitemap.hs
index e2e9a736..54d8a169 100644
--- a/Web/Restful/Response/Sitemap.hs
+++ b/Web/Restful/Response/Sitemap.hs
@@ -21,7 +21,6 @@ module Web.Restful.Response.Sitemap
import Web.Restful.Handler
import Web.Restful.Response
-import Web.Restful.Utils
import Web.Encodings
import qualified Hack
import Web.Restful.Request
diff --git a/Web/Restful/Utils.hs b/Web/Restful/Utils.hs
index 2862fa2c..19053d68 100644
--- a/Web/Restful/Utils.hs
+++ b/Web/Restful/Utils.hs
@@ -15,17 +15,12 @@
module Web.Restful.Utils
( parseHttpAccept
, tryLookup
- , formatW3
, testSuite
) where
import Data.List.Split (splitOneOf)
import Data.Maybe (fromMaybe)
-import Data.Time.Clock
-import System.Locale
-import Data.Time.Format
-
import Test.Framework (testGroup, Test)
import Test.Framework.Providers.HUnit
import Test.HUnit hiding (Test)
@@ -43,10 +38,6 @@ specialHttpAccept _ = False
tryLookup :: Eq k => v -> k -> [(k, v)] -> v
tryLookup def key = fromMaybe def . lookup key
--- | Format a 'UTCTime' in W3 format; useful for setting cookies.
-formatW3 :: UTCTime -> String
-formatW3 = formatTime defaultTimeLocale "%FT%X-00:00"
-
----- Testing
testSuite :: Test
testSuite = testGroup "Web.Restful.Response"
diff --git a/restful.cabal b/restful.cabal
index 68a5a6e6..f88a2904 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -29,8 +29,8 @@ library
bytestring-class,
web-encodings >= 0.0.1,
mtl >= 1.1.0.2,
- data-object,
- yaml >= 0.0.1,
+ data-object >= 0.0.2,
+ yaml >= 0.0.4,
test-framework,
test-framework-quickcheck,
test-framework-hunit,
From 5231da57a365e23ef1dd94686881c0dbb672c437 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Sun, 4 Oct 2009 20:25:06 +0200
Subject: [PATCH 029/624] Built in function for statics from dir
---
TODO | 1 +
Web/Restful/Helpers/Static.hs | 12 ++++++++++++
restful.cabal | 3 ++-
3 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/TODO b/TODO
index c3062510..cabb808c 100644
--- a/TODO
+++ b/TODO
@@ -1,3 +1,4 @@
Catch exceptions and return as 500 errors
approot
Request parameters without a request object?
+More checking on parameters (minimum length etc)
diff --git a/Web/Restful/Helpers/Static.hs b/Web/Restful/Helpers/Static.hs
index d2e46614..3b84d2e3 100644
--- a/Web/Restful/Helpers/Static.hs
+++ b/Web/Restful/Helpers/Static.hs
@@ -16,14 +16,26 @@
module Web.Restful.Helpers.Static
( serveStatic
, FileLookup
+ , fileLookupDir
) where
import qualified Data.ByteString as B
+import System.Directory (doesFileExist)
+import Control.Applicative ((<$>))
import Web.Restful
type FileLookup = FilePath -> IO (Maybe B.ByteString)
+-- | A 'FileLookup' for files in a directory.
+fileLookupDir :: FilePath -> FileLookup
+fileLookupDir dir fp = do
+ let fp' = dir ++ '/' : fp -- FIXME incredibly insecure...
+ exists <- doesFileExist fp'
+ if exists
+ then Just <$> B.readFile fp'
+ else return Nothing
+
serveStatic :: FileLookup -> Verb -> Handler
serveStatic fl Get = getStatic fl
serveStatic _ _ = notFound
diff --git a/restful.cabal b/restful.cabal
index f88a2904..7005f940 100644
--- a/restful.cabal
+++ b/restful.cabal
@@ -36,7 +36,8 @@ library
test-framework-hunit,
HUnit,
QuickCheck == 1.*,
- enumerable >= 0.0.3
+ enumerable >= 0.0.3,
+ directory >= 1
exposed-modules: Web.Restful,
Web.Restful.Constants,
Web.Restful.Request,
From ab49c1e0faa1bbe4f149c2e891e6b2f7fea19bb6 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Sun, 4 Oct 2009 20:25:20 +0200
Subject: [PATCH 030/624] Rpxnow supporting GET and POST
---
Web/Restful/Helpers/Auth.hs | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/Web/Restful/Helpers/Auth.hs b/Web/Restful/Helpers/Auth.hs
index 50cb23e7..14b12067 100644
--- a/Web/Restful/Helpers/Auth.hs
+++ b/Web/Restful/Helpers/Auth.hs
@@ -57,6 +57,8 @@ instance ResourceName AuthResource (Maybe RpxnowApiKey) where
getHandler _ Openid Get = authOpenidForm
getHandler _ OpenidForward Get = authOpenidForward
getHandler _ OpenidComplete Get = authOpenidComplete
+ -- two different versions of RPX protocol apparently...
+ getHandler (Just key) LoginRpxnow Get = rpxnowLogin key
getHandler (Just key) LoginRpxnow Post = rpxnowLogin key
getHandler _ _ _ = notFound
@@ -132,8 +134,8 @@ authOpenidComplete = do
data RpxnowRequest = RpxnowRequest String (Maybe String)
instance Request RpxnowRequest where
parseRequest = do
- token <- postParam "token"
- dest <- postParam "dest"
+ token <- anyParam "token"
+ dest <- anyParam "dest"
return $! RpxnowRequest token $ chopHash `fmap` dest
chopHash :: String -> String
From 45bd3dca6601832f5ff121669b66a4f52f6a8943 Mon Sep 17 00:00:00 2001
From: Michael Snoyman
Date: Sun, 4 Oct 2009 22:32:23 +0200
Subject: [PATCH 031/624] Massive overhaul of Request system
---
Web/Restful/Application.hs | 2 +
Web/Restful/Handler.hs | 30 +++---
Web/Restful/Helpers/Auth.hs | 53 ++++------
Web/Restful/Helpers/Static.hs | 6 +-
Web/Restful/Request.hs | 172 +++++++++++++++++---------------
Web/Restful/Response.hs | 2 +
Web/Restful/Response/Sitemap.hs | 9 +-
restful.cabal | 2 +-
8 files changed, 138 insertions(+), 138 deletions(-)
diff --git a/Web/Restful/Application.hs b/Web/Restful/Application.hs
index eb287784..035101ce 100644
--- a/Web/Restful/Application.hs
+++ b/Web/Restful/Application.hs
@@ -77,6 +77,8 @@ class ResourceName a b => RestfulApp a b | a -> b where
[ ("errorMsg", toObject "Invalid arguments")
, ("messages", toObject ia)
]
+ errorHandler _ _ PermissionDenied =
+ reps $ toObject $ "Permission denied"
-- | Given a sample resource name (purely for typing reasons), generating
-- a Hack application.
diff --git a/Web/Restful/Handler.hs b/Web/Restful/Handler.hs
index 3a9454cf..043fb109 100644
--- a/Web/Restful/Handler.hs
+++ b/Web/Restful/Handler.hs
@@ -22,7 +22,6 @@ module Web.Restful.Handler
, HandlerIO
, Handler
, runHandler
- , getRequest
, liftIO
-- * Special handlers
, redirect
@@ -37,7 +36,8 @@ import Web.Restful.Request
import Web.Restful.Response
import Control.Monad.Trans
-import Control.Monad (liftM)
+import Control.Monad (liftM, ap)
+import Control.Applicative
import Data.Maybe (fromJust)
import qualified Data.ByteString.Lazy as B
@@ -108,25 +108,27 @@ instance Monad m => Monad (HandlerT m) where
(b, hs2) <- b' rr
return (b, hs1 ++ hs2)
--- | Parse a request in the Handler monad. On failure, return a 400 error.
-getRequest :: (Monad m, Request r) => HandlerT m r
-getRequest = HandlerT $ \rr -> return (helper rr, []) where
- helper :: Request r
- => RawRequest
- -> Either ErrorResult r
- helper rr =
- case runRequestParser parseRequest rr of
- Left errors -> Left $ InvalidArgs errors
- Right r -> Right r
+instance Monad m => Applicative (HandlerT m) where
+ pure = return
+ (<*>) = ap
+
+instance Monad m => MonadRequestReader (HandlerT m) where
+ askRawRequest = HandlerT $ \rr -> return (Right rr, [])
+ invalidParam ptype name msg =
+ errorResult $ InvalidArgs [(name ++ " (" ++ show ptype ++ ")", msg)]
+ authRequired = errorResult PermissionDenied
------ Special handlers
+errorResult :: Monad m => ErrorResult -> HandlerT m a
+errorResult er = HandlerT (const $ return (Left er, []))
+
-- | Redirect to the given URL.
redirect :: Monad m => String -> HandlerT m a
-redirect s = HandlerT (const $ return (Left $ Redirect s, []))
+redirect = errorResult . Redirect
-- | Return a 404 not found page. Also denotes no handler available.
notFound :: Monad m => HandlerT m a
-notFound = HandlerT (const $ return (Left NotFound, []))
+notFound = errorResult NotFound
------- Headers
-- | Set the cookie on the client.
diff --git a/Web/Restful/Helpers/Auth.hs b/Web/Restful/Helpers/Auth.hs
index 14b12067..83c909ca 100644
--- a/Web/Restful/Helpers/Auth.hs
+++ b/Web/Restful/Helpers/Auth.hs
@@ -80,7 +80,7 @@ instance Show OIDFormReq where
authOpenidForm :: Handler
authOpenidForm = do
- m@(OIDFormReq _ dest) <- getRequest
+ m@(OIDFormReq _ dest) <- parseRequest
let html =
show m ++
"