Merge branch 'beta'

Conflicts:
	yesod-core/yesod-core.cabal
	yesod-json/yesod-json.cabal
	yesod-test/Yesod/Test.hs
	yesod-test/test/main.hs
	yesod-test/yesod-test.cabal
This commit is contained in:
Michael Snoyman 2012-07-11 08:48:09 +03:00
commit 699d76d13a
71 changed files with 1096 additions and 854 deletions

View File

@ -149,7 +149,10 @@ setCreds doRedirects creds = do
Nothing -> Nothing ->
when doRedirects $ do when doRedirects $ do
case authRoute y of case authRoute y of
Nothing -> do rh <- defaultLayout $ toWidget [shamlet| <h1>Invalid login |] Nothing -> do rh <- defaultLayout $ toWidget [shamlet|
$newline never
<h1>Invalid login
|]
sendResponse rh sendResponse rh
Just ar -> do setMessageI Msg.InvalidLogin Just ar -> do setMessageI Msg.InvalidLogin
redirect ar redirect ar
@ -168,6 +171,7 @@ getCheckR = do
where where
html' creds = html' creds =
[shamlet| [shamlet|
$newline never
<h1>Authentication Status <h1>Authentication Status
$maybe _ <- creds $maybe _ <- creds
<p>Logged in. <p>Logged in.

View File

@ -62,6 +62,7 @@ helper maudience = AuthPlugin
, apLogin = \toMaster -> do , apLogin = \toMaster -> do
addScriptRemote browserIdJs addScriptRemote browserIdJs
toWidget [hamlet| toWidget [hamlet|
$newline never
<p> <p>
<a href="javascript:navigator.id.getVerifiedEmail(function(a){if(a)document.location='@{toMaster complete}/'+a});"> <a href="javascript:navigator.id.getVerifiedEmail(function(a){if(a)document.location='@{toMaster complete}/'+a});">
<img src="https://browserid.org/i/sign_in_green.png"> <img src="https://browserid.org/i/sign_in_green.png">

View File

@ -24,6 +24,7 @@ authDummy =
url = PluginR "dummy" [] url = PluginR "dummy" []
login authToMaster = login authToMaster =
toWidget [hamlet| toWidget [hamlet|
$newline never
<form method="post" action="@{authToMaster url}"> <form method="post" action="@{authToMaster url}">
Your new identifier is: # Your new identifier is: #
<input type="text" name="ident"> <input type="text" name="ident">

View File

@ -79,6 +79,7 @@ authEmail :: YesodAuthEmail m => AuthPlugin m
authEmail = authEmail =
AuthPlugin "email" dispatch $ \tm -> AuthPlugin "email" dispatch $ \tm ->
[whamlet| [whamlet|
$newline never
<form method="post" action="@{tm loginR}"> <form method="post" action="@{tm loginR}">
<table> <table>
<tr> <tr>
@ -112,6 +113,7 @@ getRegisterR = do
defaultLayout $ do defaultLayout $ do
setTitleI Msg.RegisterLong setTitleI Msg.RegisterLong
[whamlet| [whamlet|
$newline never
<p>_{Msg.EnterEmail} <p>_{Msg.EnterEmail}
<form method="post" action="@{toMaster registerR}"> <form method="post" action="@{toMaster registerR}">
<label for="email">_{Msg.Email} <label for="email">_{Msg.Email}
@ -141,7 +143,10 @@ postRegisterR = do
sendVerifyEmail email verKey verUrl sendVerifyEmail email verKey verUrl
defaultLayout $ do defaultLayout $ do
setTitleI Msg.ConfirmationEmailSentTitle setTitleI Msg.ConfirmationEmailSentTitle
[whamlet| <p>_{Msg.ConfirmationEmailSent email} |] [whamlet|
$newline never
<p>_{Msg.ConfirmationEmailSent email}
|]
getVerifyR :: YesodAuthEmail m getVerifyR :: YesodAuthEmail m
=> AuthEmailId m -> Text -> GHandler Auth m RepHtml => AuthEmailId m -> Text -> GHandler Auth m RepHtml
@ -161,7 +166,10 @@ getVerifyR lid key = do
_ -> return () _ -> return ()
defaultLayout $ do defaultLayout $ do
setTitleI Msg.InvalidKey setTitleI Msg.InvalidKey
[whamlet| <p>_{Msg.InvalidKey} |] [whamlet|
$newline never
<p>_{Msg.InvalidKey}
|]
postLoginR :: YesodAuthEmail master => GHandler Auth master () postLoginR :: YesodAuthEmail master => GHandler Auth master ()
postLoginR = do postLoginR = do
@ -200,6 +208,7 @@ getPasswordR = do
defaultLayout $ do defaultLayout $ do
setTitleI Msg.SetPassTitle setTitleI Msg.SetPassTitle
[whamlet| [whamlet|
$newline never
<h3>_{Msg.SetPass} <h3>_{Msg.SetPass}
<form method="post" action="@{toMaster setpassR}"> <form method="post" action="@{toMaster setpassR}">
<table> <table>

View File

@ -46,7 +46,10 @@ authGoogleEmail =
where where
complete = PluginR pid ["complete"] complete = PluginR pid ["complete"]
login tm = login tm =
[whamlet|<a href=@{tm forwardUrl}>_{Msg.LoginGoogle}|] [whamlet|
$newline never
<a href=@{tm forwardUrl}>_{Msg.LoginGoogle}
|]
dispatch "GET" ["forward"] = do dispatch "GET" ["forward"] = do
render <- getUrlRender render <- getUrlRender
toMaster <- getRouteToMaster toMaster <- getRouteToMaster

View File

@ -76,7 +76,7 @@ import Yesod.Handler
import Yesod.Form import Yesod.Form
import Yesod.Auth import Yesod.Auth
import Yesod.Widget (toWidget) import Yesod.Widget (toWidget)
import Text.Hamlet (hamlet, shamlet) import Text.Hamlet (hamlet)
import Control.Applicative ((<$>), (<*>)) import Control.Applicative ((<$>), (<*>))
import Control.Monad (replicateM,liftM) import Control.Monad (replicateM,liftM)
@ -176,7 +176,7 @@ postLoginR uniq = do
(validateUser <$> (uniq =<< mu) <*> mp) (validateUser <$> (uniq =<< mu) <*> mp)
if isValid if isValid
then setCreds True $ Creds "hashdb" (fromMaybe "" mu) [] then setCreds True $ Creds "hashdb" (fromMaybe "" mu) []
else do setMessage [shamlet| Invalid username/password |] else do setMessage "Invalid username/password"
toMaster <- getRouteToMaster toMaster <- getRouteToMaster
redirect $ toMaster LoginR redirect $ toMaster LoginR
@ -207,7 +207,7 @@ getAuthIdHashDB authR uniq creds = do
-- user exists -- user exists
Just (Entity uid _) -> return $ Just uid Just (Entity uid _) -> return $ Just uid
Nothing -> do Nothing -> do
setMessage [shamlet| User not found |] setMessage "User not found"
redirect $ authR LoginR redirect $ authR LoginR
-- | Prompt for username and password, validate that against a database -- | Prompt for username and password, validate that against a database
@ -221,6 +221,7 @@ authHashDB :: ( YesodAuth m, YesodPersist m
, PersistUnique b (GHandler Auth m)) , PersistUnique b (GHandler Auth m))
=> (Text -> Maybe (Unique user b)) -> AuthPlugin m => (Text -> Maybe (Unique user b)) -> AuthPlugin m
authHashDB uniq = AuthPlugin "hashdb" dispatch $ \tm -> toWidget [hamlet| authHashDB uniq = AuthPlugin "hashdb" dispatch $ \tm -> toWidget [hamlet|
$newline never
<div id="header"> <div id="header">
<h1>Login <h1>Login

View File

@ -3,10 +3,11 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
module Yesod.Auth.OpenId module Yesod.Auth.OpenId
( authOpenId ( authOpenId
, authOpenIdExtended
, forwardUrl , forwardUrl
, claimedKey , claimedKey
, opLocalKey
, credsIdentClaimed , credsIdentClaimed
, IdentifierType (..)
) where ) where
import Yesod.Auth import Yesod.Auth
@ -30,11 +31,13 @@ import Data.Maybe (fromMaybe)
forwardUrl :: AuthRoute forwardUrl :: AuthRoute
forwardUrl = PluginR "openid" ["forward"] forwardUrl = PluginR "openid" ["forward"]
authOpenId :: YesodAuth m => AuthPlugin m data IdentifierType = Claimed | OPLocal
authOpenId = authOpenIdExtended []
authOpenIdExtended :: YesodAuth m => [(Text, Text)] -> AuthPlugin m authOpenId :: YesodAuth m
authOpenIdExtended extensionFields = => IdentifierType
-> [(Text, Text)] -- ^ extension fields
-> AuthPlugin m
authOpenId idType extensionFields =
AuthPlugin "openid" dispatch login AuthPlugin "openid" dispatch login
where where
complete = PluginR "openid" ["complete"] complete = PluginR "openid" ["complete"]
@ -46,6 +49,7 @@ authOpenIdExtended extensionFields =
padding-left: 18px; padding-left: 18px;
|] |]
[whamlet| [whamlet|
$newline never
<form method="get" action="@{tm forwardUrl}"> <form method="get" action="@{tm forwardUrl}">
<input type="hidden" name="openid_identifier" value="https://www.google.com/accounts/o8/id"> <input type="hidden" name="openid_identifier" value="https://www.google.com/accounts/o8/id">
<button .openid-google>_{Msg.LoginGoogle} <button .openid-google>_{Msg.LoginGoogle}
@ -78,15 +82,15 @@ authOpenIdExtended extensionFields =
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
dispatch "GET" ["complete"] = do dispatch "GET" ["complete"] = do
rr <- getRequest rr <- getRequest
completeHelper $ reqGetParams rr completeHelper idType $ reqGetParams rr
dispatch "POST" ["complete", ""] = dispatch "POST" ["complete"] -- compatibility issues dispatch "POST" ["complete", ""] = dispatch "POST" ["complete"] -- compatibility issues
dispatch "POST" ["complete"] = do dispatch "POST" ["complete"] = do
(posts, _) <- runRequestBody (posts, _) <- runRequestBody
completeHelper posts completeHelper idType posts
dispatch _ _ = notFound dispatch _ _ = notFound
completeHelper :: YesodAuth m => [(Text, Text)] -> GHandler Auth m () completeHelper :: YesodAuth m => IdentifierType -> [(Text, Text)] -> GHandler Auth m ()
completeHelper gets' = do completeHelper idType gets' = do
master <- getYesod master <- getYesod
eres <- lift $ try $ OpenId.authenticateClaimed gets' (authHttpManager master) eres <- lift $ try $ OpenId.authenticateClaimed gets' (authHttpManager master)
toMaster <- getRouteToMaster toMaster <- getRouteToMaster
@ -98,8 +102,14 @@ completeHelper gets' = do
case OpenId.oirClaimed oir of case OpenId.oirClaimed oir of
Nothing -> id Nothing -> id
Just (OpenId.Identifier i') -> ((claimedKey, i'):) Just (OpenId.Identifier i') -> ((claimedKey, i'):)
gets'' = claimed $ filter (\(k, _) -> not $ "__" `isPrefixOf` k) gets' oplocal =
i = OpenId.identifier $ OpenId.oirOpLocal oir case OpenId.oirOpLocal oir of
OpenId.Identifier i' -> ((opLocalKey, i'):)
gets'' = oplocal $ claimed $ filter (\(k, _) -> not $ "__" `isPrefixOf` k) gets'
i = OpenId.identifier $
case idType of
OPLocal -> OpenId.oirOpLocal oir
Claimed -> fromMaybe (OpenId.oirOpLocal oir) $ OpenId.oirClaimed oir
setCreds True $ Creds "openid" i gets'' setCreds True $ Creds "openid" i gets''
either onFailure onSuccess eres either onFailure onSuccess eres
@ -117,6 +127,9 @@ completeHelper gets' = do
claimedKey :: Text claimedKey :: Text
claimedKey = "__CLAIMED" claimedKey = "__CLAIMED"
opLocalKey :: Text
opLocalKey = "__OPLOCAL"
-- | A helper function which will get the claimed identifier, if available, falling back to the OP local identifier. -- | A helper function which will get the claimed identifier, if available, falling back to the OP local identifier.
-- --
-- See 'claimedKey'. -- See 'claimedKey'.

View File

@ -25,6 +25,7 @@ authRpxnow app apiKey =
login tm = do login tm = do
let url = {- FIXME urlEncode $ -} tm $ PluginR "rpxnow" [] let url = {- FIXME urlEncode $ -} tm $ PluginR "rpxnow" []
toWidget [hamlet| toWidget [hamlet|
$newline never
<iframe src="http://#{app}.rpxnow.com/openid/embed?token_url=@{url}" scrolling="no" frameBorder="no" allowtransparency="true" style="width:400px;height:240px"> <iframe src="http://#{app}.rpxnow.com/openid/embed?token_url=@{url}" scrolling="no" frameBorder="no" allowtransparency="true" style="width:400px;height:240px">
|] |]
dispatch _ [] = do dispatch _ [] = do

View File

@ -1,5 +1,5 @@
name: yesod-auth name: yesod-auth
version: 1.0.2.1 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman, Patrick Brisbin author: Michael Snoyman, Patrick Brisbin
@ -12,44 +12,34 @@ build-type: Simple
homepage: http://www.yesodweb.com/ homepage: http://www.yesodweb.com/
description: Authentication for Yesod. description: Authentication for Yesod.
flag blaze_html_0_5
description: use blaze-html 0.5 and blaze-markup 0.5
default: True
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, authenticate >= 1.2.1 && < 1.3 , authenticate >= 1.3 && < 1.4
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, template-haskell , template-haskell
, pureMD5 >= 2.0 && < 2.2 , pureMD5 >= 2.0 && < 2.2
, random >= 1.0.0.2 && < 1.1 , random >= 1.0.0.2 && < 1.1
, text >= 0.7 && < 0.12 , text >= 0.7 && < 0.12
, mime-mail >= 0.3 && < 0.5 , mime-mail >= 0.3 && < 0.5
, yesod-persistent >= 1.0 && < 1.1 , yesod-persistent >= 1.1 && < 1.2
, hamlet >= 1.0 && < 1.1 , hamlet >= 1.1 && < 1.2
, shakespeare-css >= 1.0 && < 1.1 , shakespeare-css >= 1.0 && < 1.1
, yesod-json >= 1.0 && < 1.1 , yesod-json >= 1.1 && < 1.2
, containers , containers
, unordered-containers , unordered-containers
, yesod-form >= 1.0 && < 1.1 , yesod-form >= 1.1 && < 1.2
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, persistent >= 0.9 && < 0.10 , persistent >= 1.0 && < 1.1
, persistent-template >= 0.9 && < 0.10 , persistent-template >= 1.0 && < 1.1
, SHA >= 1.4.1.3 && < 1.6 , SHA >= 1.4.1.3 && < 1.6
, http-conduit >= 1.4.1.1 && < 1.5 , http-conduit >= 1.5 && < 1.6
, aeson >= 0.5 , aeson >= 0.5
, pwstore-fast >= 2.2 && < 3 , pwstore-fast >= 2.2 && < 3
, lifted-base >= 0.1 && < 0.2 , lifted-base >= 0.1 && < 0.2
, blaze-html >= 0.5 && < 0.6
if flag(blaze_html_0_5) , blaze-markup >= 0.5.1 && < 0.6
build-depends:
blaze-html >= 0.5 && < 0.6
, blaze-markup >= 0.5.1 && < 0.6
else
build-depends:
blaze-html >= 0.4 && < 0.5
exposed-modules: Yesod.Auth exposed-modules: Yesod.Auth
Yesod.Auth.BrowserId Yesod.Auth.BrowserId

View File

@ -60,11 +60,7 @@ import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString)
import Data.Monoid (mempty) import Data.Monoid (mempty)
import Text.Hamlet (Html) import Text.Hamlet (Html)
#if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder) import Text.Blaze.Html.Renderer.Utf8 (renderHtmlBuilder)
#else
import Text.Blaze.Renderer.Utf8 (renderHtmlBuilder)
#endif
import Data.String (IsString (fromString)) import Data.String (IsString (fromString))
import Network.Wai (FilePart) import Network.Wai (FilePart)
import Data.Conduit (Source, ResourceT, Flush) import Data.Conduit (Source, ResourceT, Flush)

View File

@ -10,6 +10,7 @@ module Yesod.Core
, breadcrumbs , breadcrumbs
-- * Types -- * Types
, Approot (..) , Approot (..)
, FileUpload (..)
-- * Utitlities -- * Utitlities
, maybeAuthorized , maybeAuthorized
, widgetToPageContent , widgetToPageContent
@ -20,8 +21,6 @@ module Yesod.Core
, unauthorizedI , unauthorizedI
-- * Logging -- * Logging
, LogLevel (..) , LogLevel (..)
, formatLogMessage
, fileLocationToString
, logDebug , logDebug
, logInfo , logInfo
, logWarn , logWarn
@ -59,38 +58,7 @@ import Yesod.Request
import Yesod.Widget import Yesod.Widget
import Yesod.Message import Yesod.Message
import Language.Haskell.TH.Syntax import Control.Monad.Logger
import qualified Language.Haskell.TH.Syntax as TH
import Data.Text (Text)
logTH :: LogLevel -> Q Exp
logTH level =
[|messageLoggerHandler $(qLocation >>= liftLoc) $(TH.lift level)|]
where
liftLoc :: Loc -> Q Exp
liftLoc (Loc a b c d e) = [|Loc $(TH.lift a) $(TH.lift b) $(TH.lift c) $(TH.lift d) $(TH.lift e)|]
-- | Generates a function that takes a 'Text' and logs a 'LevelDebug' message. Usage:
--
-- > $(logDebug) "This is a debug log message"
logDebug :: Q Exp
logDebug = logTH LevelDebug
-- | See 'logDebug'
logInfo :: Q Exp
logInfo = logTH LevelInfo
-- | See 'logDebug'
logWarn :: Q Exp
logWarn = logTH LevelWarn
-- | See 'logDebug'
logError :: Q Exp
logError = logTH LevelError
-- | Generates a function that takes a 'Text' and logs a 'LevelOther' message. Usage:
--
-- > $(logOther "My new level") "This is a log message"
logOther :: Text -> Q Exp
logOther = logTH . LevelOther
-- | Return an 'Unauthorized' value, with the given i18n message. -- | Return an 'Unauthorized' value, with the given i18n message.
unauthorizedI :: RenderMessage master msg => msg -> GHandler sub master AuthResult unauthorizedI :: RenderMessage master msg => msg -> GHandler sub master AuthResult

View File

@ -28,7 +28,7 @@ module Yesod.Dispatch
, WaiSubsite (..) , WaiSubsite (..)
) where ) where
import Data.Functor ((<$>)) import Control.Applicative ((<$>), (<*>))
import Prelude hiding (exp) import Prelude hiding (exp)
import Yesod.Internal.Core import Yesod.Internal.Core
import Yesod.Handler hiding (lift) import Yesod.Handler hiding (lift)
@ -53,6 +53,7 @@ import Network.HTTP.Types (status301)
import Yesod.Routes.TH import Yesod.Routes.TH
import Yesod.Content (chooseRep) import Yesod.Content (chooseRep)
import Yesod.Routes.Parse import Yesod.Routes.Parse
import System.Log.FastLogger (Logger)
type Texts = [Text] type Texts = [Text]
@ -60,7 +61,7 @@ type Texts = [Text]
-- is used for creating sites, /not/ subsites. See 'mkYesodSub' for the latter. -- is used for creating sites, /not/ subsites. See 'mkYesodSub' for the latter.
-- Use 'parseRoutes' to create the 'Resource's. -- Use 'parseRoutes' to create the 'Resource's.
mkYesod :: String -- ^ name of the argument datatype mkYesod :: String -- ^ name of the argument datatype
-> [Resource String] -> [ResourceTree String]
-> Q [Dec] -> Q [Dec]
mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] [] False mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] [] False
@ -71,7 +72,7 @@ mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] [] False
-- be embedded in other sites. -- be embedded in other sites.
mkYesodSub :: String -- ^ name of the argument datatype mkYesodSub :: String -- ^ name of the argument datatype
-> Cxt -> Cxt
-> [Resource String] -> [ResourceTree String]
-> Q [Dec] -> Q [Dec]
mkYesodSub name clazzes = mkYesodSub name clazzes =
fmap (uncurry (++)) . mkYesodGeneral name' rest clazzes True fmap (uncurry (++)) . mkYesodGeneral name' rest clazzes True
@ -82,28 +83,28 @@ mkYesodSub name clazzes =
-- your handlers elsewhere. For example, this is the only way to break up a -- your handlers elsewhere. For example, this is the only way to break up a
-- monolithic file into smaller parts. Use this function, paired with -- monolithic file into smaller parts. Use this function, paired with
-- 'mkYesodDispatch', to do just that. -- 'mkYesodDispatch', to do just that.
mkYesodData :: String -> [Resource String] -> Q [Dec] mkYesodData :: String -> [ResourceTree String] -> Q [Dec]
mkYesodData name res = mkYesodDataGeneral name [] False res mkYesodData name res = mkYesodDataGeneral name [] False res
mkYesodSubData :: String -> Cxt -> [Resource String] -> Q [Dec] mkYesodSubData :: String -> Cxt -> [ResourceTree String] -> Q [Dec]
mkYesodSubData name clazzes res = mkYesodDataGeneral name clazzes True res mkYesodSubData name clazzes res = mkYesodDataGeneral name clazzes True res
mkYesodDataGeneral :: String -> Cxt -> Bool -> [Resource String] -> Q [Dec] mkYesodDataGeneral :: String -> Cxt -> Bool -> [ResourceTree String] -> Q [Dec]
mkYesodDataGeneral name clazzes isSub res = do mkYesodDataGeneral name clazzes isSub res = do
let (name':rest) = words name let (name':rest) = words name
(x, _) <- mkYesodGeneral name' rest clazzes isSub res (x, _) <- mkYesodGeneral name' rest clazzes isSub res
let rname = mkName $ "resources" ++ name let rname = mkName $ "resources" ++ name
eres <- lift res eres <- lift res
let y = [ SigD rname $ ListT `AppT` (ConT ''Resource `AppT` ConT ''String) let y = [ SigD rname $ ListT `AppT` (ConT ''ResourceTree `AppT` ConT ''String)
, FunD rname [Clause [] (NormalB eres) []] , FunD rname [Clause [] (NormalB eres) []]
] ]
return $ x ++ y return $ x ++ y
-- | See 'mkYesodData'. -- | See 'mkYesodData'.
mkYesodDispatch :: String -> [Resource String] -> Q [Dec] mkYesodDispatch :: String -> [ResourceTree String] -> Q [Dec]
mkYesodDispatch name = fmap snd . mkYesodGeneral name [] [] False mkYesodDispatch name = fmap snd . mkYesodGeneral name [] [] False
mkYesodSubDispatch :: String -> Cxt -> [Resource String] -> Q [Dec] mkYesodSubDispatch :: String -> Cxt -> [ResourceTree String] -> Q [Dec]
mkYesodSubDispatch name clazzes = fmap snd . mkYesodGeneral name' rest clazzes True mkYesodSubDispatch name clazzes = fmap snd . mkYesodGeneral name' rest clazzes True
where (name':rest) = words name where (name':rest) = words name
@ -111,7 +112,7 @@ mkYesodGeneral :: String -- ^ foundation type
-> [String] -> [String]
-> Cxt -- ^ classes -> Cxt -- ^ classes
-> Bool -- ^ is subsite? -> Bool -- ^ is subsite?
-> [Resource String] -> [ResourceTree String]
-> Q ([Dec], [Dec]) -> Q ([Dec], [Dec])
mkYesodGeneral name args clazzes isSub resS = do mkYesodGeneral name args clazzes isSub resS = do
let args' = map mkName args let args' = map mkName args
@ -119,7 +120,13 @@ mkYesodGeneral name args clazzes isSub resS = do
let res = map (fmap parseType) resS let res = map (fmap parseType) resS
renderRouteDec <- mkRenderRouteInstance arg res renderRouteDec <- mkRenderRouteInstance arg res
disp <- mkDispatchClause [|yesodRunner|] [|yesodDispatch|] [|fmap chooseRep|] res let logger = mkName "logger"
Clause pat body decs <- mkDispatchClause
[|yesodRunner $(return $ VarE logger)|]
[|yesodDispatch $(return $ VarE logger)|]
[|fmap chooseRep|]
res
let disp = Clause (VarP logger : pat) body decs
let master = mkName "master" let master = mkName "master"
let ctx = if isSub let ctx = if isSub
then ClassP (mkName "Yesod") [VarT master] : clazzes then ClassP (mkName "Yesod") [VarT master] : clazzes
@ -130,7 +137,7 @@ mkYesodGeneral name args clazzes isSub resS = do
let yesodDispatch' = let yesodDispatch' =
InstanceD ctx ytyp [FunD (mkName "yesodDispatch") [disp]] InstanceD ctx ytyp [FunD (mkName "yesodDispatch") [disp]]
return (renderRouteDec : masterTypSyns, [yesodDispatch']) return (renderRouteDec ++ masterTypSyns, [yesodDispatch'])
where where
name' = mkName name name' = mkName name
masterTypSyns masterTypSyns
@ -160,23 +167,24 @@ toWaiApp y = gzip (gzipSettings y) . autohead <$> toWaiAppPlain y
toWaiAppPlain :: ( Yesod master toWaiAppPlain :: ( Yesod master
, YesodDispatch master master , YesodDispatch master master
) => master -> IO W.Application ) => master -> IO W.Application
toWaiAppPlain a = toWaiApp' a <$> makeSessionBackend a toWaiAppPlain a = toWaiApp' a <$> getLogger a <*> makeSessionBackend a
toWaiApp' :: ( Yesod master toWaiApp' :: ( Yesod master
, YesodDispatch master master , YesodDispatch master master
) )
=> master => master
-> Logger
-> Maybe (SessionBackend master) -> Maybe (SessionBackend master)
-> W.Application -> W.Application
toWaiApp' y sb env = toWaiApp' y logger sb env =
case cleanPath y $ W.pathInfo env of case cleanPath y $ W.pathInfo env of
Left pieces -> sendRedirect y pieces env Left pieces -> sendRedirect y pieces env
Right pieces -> Right pieces ->
yesodDispatch y y id app404 handler405 method pieces sb env yesodDispatch logger y y id app404 handler405 method pieces sb env
where where
app404 = yesodRunner notFound y y Nothing id app404 = yesodRunner logger notFound y y Nothing id
handler405 route = yesodRunner badMethod y y (Just route) id handler405 route = yesodRunner logger badMethod y y (Just route) id
method = decodeUtf8With lenientDecode $ W.requestMethod env method = decodeUtf8With lenientDecode $ W.requestMethod env
sendRedirect :: Yesod master => master -> [Text] -> W.Application sendRedirect :: Yesod master => master -> [Text] -> W.Application
@ -202,4 +210,4 @@ instance RenderRoute WaiSubsite where
renderRoute (WaiSubsiteRoute ps qs) = (ps, qs) renderRoute (WaiSubsiteRoute ps qs) = (ps, qs)
instance YesodDispatch WaiSubsite master where instance YesodDispatch WaiSubsite master where
yesodDispatch _master (WaiSubsite app) _tomaster _404 _405 _method _pieces _session = app yesodDispatch _logger _master (WaiSubsite app) _tomaster _404 _405 _method _pieces _session = app

View File

@ -138,11 +138,7 @@ import qualified Network.Wai as W
import qualified Network.HTTP.Types as H import qualified Network.HTTP.Types as H
import Text.Hamlet import Text.Hamlet
#if MIN_VERSION_blaze_html(0, 5, 0)
import qualified Text.Blaze.Html.Renderer.Text as RenderText import qualified Text.Blaze.Html.Renderer.Text as RenderText
#else
import qualified Text.Blaze.Renderer.Text as RenderText
#endif
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8, decodeUtf8With) import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
@ -159,18 +155,18 @@ import Control.Arrow ((***))
import qualified Network.Wai.Parse as NWP import qualified Network.Wai.Parse as NWP
import Data.Monoid (mappend, mempty, Endo (..)) import Data.Monoid (mappend, mempty, Endo (..))
import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Char8 as S8
import Data.ByteString (ByteString)
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import Blaze.ByteString.Builder (toByteString) import Blaze.ByteString.Builder (toByteString)
import Data.Text (Text) import Data.Text (Text)
import Yesod.Message (RenderMessage (..)) import Yesod.Message (RenderMessage (..))
#if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze.Html (toHtml, preEscapedToMarkup) import Text.Blaze.Html (toHtml, preEscapedToMarkup)
#define preEscapedText preEscapedToMarkup #define preEscapedText preEscapedToMarkup
#else
import Text.Blaze (toHtml, preEscapedText) import System.Log.FastLogger
#endif import Control.Monad.Logger
import qualified Yesod.Internal.Cache as Cache import qualified Yesod.Internal.Cache as Cache
import Yesod.Internal.Cache (mkCacheKey, CacheKey) import Yesod.Internal.Cache (mkCacheKey, CacheKey)
@ -181,6 +177,9 @@ import Control.Monad.Trans.Control
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
import Control.Monad.Base import Control.Monad.Base
import Yesod.Routes.Class import Yesod.Routes.Class
import Data.Word (Word64)
import Data.Conduit (Sink)
import Language.Haskell.TH.Syntax (Loc)
class YesodSubRoute s y where class YesodSubRoute s y where
fromSubRoute :: s -> y -> Route s -> Route y fromSubRoute :: s -> y -> Route s -> Route y
@ -193,6 +192,8 @@ data HandlerData sub master = HandlerData
, handlerRender :: Route master -> [(Text, Text)] -> Text , handlerRender :: Route master -> [(Text, Text)] -> Text
, handlerToMaster :: Route sub -> Route master , handlerToMaster :: Route sub -> Route master
, handlerState :: I.IORef GHState , handlerState :: I.IORef GHState
, handlerUpload :: Word64 -> FileUpload
, handlerLog :: Loc -> LogLevel -> LogStr -> IO ()
} }
handlerSubData :: (Route sub -> Route master) handlerSubData :: (Route sub -> Route master)
@ -322,22 +323,36 @@ hcError = liftIO . throwIO . HCError
runRequestBody :: GHandler s m RequestBodyContents runRequestBody :: GHandler s m RequestBodyContents
runRequestBody = do runRequestBody = do
hd <- ask
let getUpload = handlerUpload hd
len = reqBodySize $ handlerRequest hd
upload = getUpload len
x <- get x <- get
case ghsRBC x of case ghsRBC x of
Just rbc -> return rbc Just rbc -> return rbc
Nothing -> do Nothing -> do
rr <- waiRequest rr <- waiRequest
rbc <- lift $ rbHelper rr rbc <- lift $ rbHelper upload rr
put x { ghsRBC = Just rbc } put x { ghsRBC = Just rbc }
return rbc return rbc
rbHelper :: W.Request -> ResourceT IO RequestBodyContents rbHelper :: FileUpload -> W.Request -> ResourceT IO RequestBodyContents
rbHelper req = rbHelper upload =
(map fix1 *** map fix2) <$> (NWP.parseRequestBody NWP.lbsBackEnd req) case upload of
FileUploadMemory s -> rbHelper' s mkFileInfoLBS
FileUploadDisk s -> rbHelper' s mkFileInfoFile
FileUploadSource s -> rbHelper' s mkFileInfoSource
rbHelper' :: Sink S8.ByteString (ResourceT IO) x
-> (Text -> Text -> x -> FileInfo)
-> W.Request
-> ResourceT IO ([(Text, Text)], [(Text, FileInfo)])
rbHelper' sink mkFI req =
(map fix1 *** map fix2) <$> (NWP.parseRequestBody sink req)
where where
fix1 = go *** go fix1 = go *** go
fix2 (x, NWP.FileInfo a b c) = fix2 (x, NWP.FileInfo a b c) =
(go x, FileInfo (go a) (go b) c) (go x, mkFI (go a) (go b) c)
go = decodeUtf8With lenientDecode go = decodeUtf8With lenientDecode
-- | Get the sub application argument. -- | Get the sub application argument.
@ -378,8 +393,10 @@ runHandler :: HasReps c
-> (Route sub -> Route master) -> (Route sub -> Route master)
-> master -> master
-> sub -> sub
-> (Word64 -> FileUpload)
-> (Loc -> LogLevel -> LogStr -> IO ())
-> YesodApp -> YesodApp
runHandler handler mrender sroute tomr master sub = runHandler handler mrender sroute tomr master sub upload log' =
YesodApp $ \eh rr cts initSession -> do YesodApp $ \eh rr cts initSession -> do
let toErrorHandler e = let toErrorHandler e =
case fromException e of case fromException e of
@ -400,6 +417,8 @@ runHandler handler mrender sroute tomr master sub =
, handlerRender = mrender , handlerRender = mrender
, handlerToMaster = tomr , handlerToMaster = tomr
, handlerState = istate , handlerState = istate
, handlerUpload = upload
, handlerLog = log'
} }
contents' <- catch (fmap Right $ unGHandler handler hd) contents' <- catch (fmap Right $ unGHandler handler hd)
(\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id (\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id
@ -772,6 +791,8 @@ getSession = liftM ghsSession get
handlerToYAR :: (HasReps a, HasReps b) handlerToYAR :: (HasReps a, HasReps b)
=> master -- ^ master site foundation => master -- ^ master site foundation
-> sub -- ^ sub site foundation -> sub -- ^ sub site foundation
-> (Word64 -> FileUpload)
-> (Loc -> LogLevel -> LogStr -> IO ())
-> (Route sub -> Route master) -> (Route sub -> Route master)
-> (Route master -> [(Text, Text)] -> Text) -- route renderer -> (Route master -> [(Text, Text)] -> Text) -- route renderer
-> (ErrorResponse -> GHandler sub master a) -> (ErrorResponse -> GHandler sub master a)
@ -780,15 +801,15 @@ handlerToYAR :: (HasReps a, HasReps b)
-> SessionMap -> SessionMap
-> GHandler sub master b -> GHandler sub master b
-> ResourceT IO YesodAppResult -> ResourceT IO YesodAppResult
handlerToYAR y s toMasterRoute render errorHandler rr murl sessionMap h = handlerToYAR y s upload log' toMasterRoute render errorHandler rr murl sessionMap h =
unYesodApp ya eh' rr types sessionMap unYesodApp ya eh' rr types sessionMap
where where
ya = runHandler h render murl toMasterRoute y s ya = runHandler h render murl toMasterRoute y s upload log'
eh' er = runHandler (errorHandler' er) render murl toMasterRoute y s eh' er = runHandler (errorHandler' er) render murl toMasterRoute y s upload log'
types = httpAccept $ reqWaiRequest rr types = httpAccept $ reqWaiRequest rr
errorHandler' = localNoCurrent . errorHandler errorHandler' = localNoCurrent . errorHandler
yarToResponse :: YesodAppResult -> [(CI H.Ascii, H.Ascii)] -> W.Response yarToResponse :: YesodAppResult -> [(CI ByteString, ByteString)] -> W.Response
yarToResponse (YARWai a) _ = a yarToResponse (YARWai a) _ = a
yarToResponse (YARPlain s hs _ c _) extraHeaders = yarToResponse (YARPlain s hs _ c _) extraHeaders =
case c of case c of
@ -810,7 +831,7 @@ httpAccept = parseHttpAccept
-- | Convert Header to a key/value pair. -- | Convert Header to a key/value pair.
headerToPair :: Header headerToPair :: Header
-> (CI H.Ascii, H.Ascii) -> (CI ByteString, ByteString)
headerToPair (AddCookie sc) = headerToPair (AddCookie sc) =
("Set-Cookie", toByteString $ renderSetCookie $ sc) ("Set-Cookie", toByteString $ renderSetCookie $ sc)
headerToPair (DeleteCookie key path) = headerToPair (DeleteCookie key path) =
@ -842,6 +863,7 @@ redirectToPost :: RedirectUrl master url => url -> GHandler sub master a
redirectToPost url = do redirectToPost url = do
urlText <- toTextUrl url urlText <- toTextUrl url
hamletToRepHtml [hamlet| hamletToRepHtml [hamlet|
$newline never
$doctype 5 $doctype 5
<html> <html>
@ -936,3 +958,8 @@ instance MonadResource (GHandler sub master) where
register = lift . register register = lift . register
release = lift . release release = lift . release
resourceMask = lift . resourceMask resourceMask = lift . resourceMask
instance MonadLogger (GHandler sub master) where
monadLoggerLog a b c = do
hd <- ask
liftIO $ handlerLog hd a b (toLogStr c)

View File

@ -27,7 +27,8 @@ module Yesod.Internal
, tokenKey , tokenKey
) where ) where
import Text.Hamlet (HtmlUrl, hamlet, Html) import Text.Hamlet (HtmlUrl, Html)
import Text.Blaze.Html (toHtml)
import Text.Julius (JavascriptUrl) import Text.Julius (JavascriptUrl)
import Data.Monoid (Monoid (..), Last) import Data.Monoid (Monoid (..), Last)
import Data.List (nub) import Data.List (nub)
@ -41,8 +42,8 @@ import qualified Network.HTTP.Types as H
import Data.String (IsString) import Data.String (IsString)
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Text.Lazy.Builder (Builder) import Data.Text.Lazy.Builder (Builder)
import Network.HTTP.Types (Ascii)
import Web.Cookie (SetCookie (..)) import Web.Cookie (SetCookie (..))
import Data.ByteString (ByteString)
-- | Responses to indicate some form of an error occurred. These are different -- | Responses to indicate some form of an error occurred. These are different
-- from 'SpecialResponse' in that they allow for custom error pages. -- from 'SpecialResponse' in that they allow for custom error pages.
@ -59,8 +60,8 @@ instance Exception ErrorResponse
-- | Headers to be added to a 'Result'. -- | Headers to be added to a 'Result'.
data Header = data Header =
AddCookie SetCookie AddCookie SetCookie
| DeleteCookie Ascii Ascii | DeleteCookie ByteString ByteString
| Header Ascii Ascii | Header ByteString ByteString
deriving (Eq, Show) deriving (Eq, Show)
langKey :: IsString a => a langKey :: IsString a => a
@ -69,10 +70,8 @@ langKey = "_LANG"
data Location url = Local url | Remote Text data Location url = Local url | Remote Text
deriving (Show, Eq) deriving (Show, Eq)
locationToHtmlUrl :: Location url -> HtmlUrl url locationToHtmlUrl :: Location url -> HtmlUrl url
locationToHtmlUrl (Local url) = [hamlet|\@{url} locationToHtmlUrl (Local url) render = toHtml $ render url []
|] locationToHtmlUrl (Remote s) _ = toHtml s
locationToHtmlUrl (Remote s) = [hamlet|\#{s}
|]
newtype UniqueList x = UniqueList ([x] -> [x]) newtype UniqueList x = UniqueList ([x] -> [x])
instance Monoid (UniqueList x) where instance Monoid (UniqueList x) where
@ -100,13 +99,14 @@ tokenKey = "_TOKEN"
type CssBuilderUrl a = (a -> [(Text, Text)] -> Text) -> Builder type CssBuilderUrl a = (a -> [(Text, Text)] -> Text) -> Builder
data GWData a = GWData data GWData a = GWData
!(Body a) { gwdBody :: !(Body a)
!(Last Title) , gwdTitle :: !(Last Title)
!(UniqueList (Script a)) , gwdScripts :: !(UniqueList (Script a))
!(UniqueList (Stylesheet a)) , gwdStylesheets :: !(UniqueList (Stylesheet a))
!(Map.Map (Maybe Text) (CssBuilderUrl a)) -- media type , gwdCss :: !(Map.Map (Maybe Text) (CssBuilderUrl a)) -- media type
!(Maybe (JavascriptUrl a)) , gwdJavascript :: !(Maybe (JavascriptUrl a))
!(Head a) , gwdHead :: !(Head a)
}
instance Monoid (GWData a) where instance Monoid (GWData a) where
mempty = GWData mempty mempty mempty mempty mempty mempty mempty mempty = GWData mempty mempty mempty mempty mempty mempty mempty
mappend (GWData a1 a2 a3 a4 a5 a6 a7) mappend (GWData a1 a2 a3 a4 a5 a6 a7)

View File

@ -20,11 +20,6 @@ module Yesod.Internal.Core
, defaultErrorHandler , defaultErrorHandler
-- * Data types -- * Data types
, AuthResult (..) , AuthResult (..)
-- * Logging
, LogLevel (..)
, formatLogMessage
, fileLocationToString
, messageLoggerHandler
-- * Sessions -- * Sessions
, SessionBackend (..) , SessionBackend (..)
, defaultClientSessionBackend , defaultClientSessionBackend
@ -40,6 +35,7 @@ module Yesod.Internal.Core
, yesodRender , yesodRender
, resolveApproot , resolveApproot
, Approot (..) , Approot (..)
, FileUpload (..)
) where ) where
import Yesod.Content import Yesod.Content
@ -47,6 +43,7 @@ import Yesod.Handler hiding (lift, getExpires)
import Yesod.Routes.Class import Yesod.Routes.Class
import Data.Word (Word64)
import Control.Arrow ((***)) import Control.Arrow ((***))
import Control.Monad (forM) import Control.Monad (forM)
import Yesod.Widget import Yesod.Widget
@ -80,26 +77,19 @@ import Blaze.ByteString.Builder.Char.Utf8 (fromText)
import Data.List (foldl') import Data.List (foldl')
import qualified Network.HTTP.Types as H import qualified Network.HTTP.Types as H
import Web.Cookie (SetCookie (..)) import Web.Cookie (SetCookie (..))
import qualified Data.Text.Lazy as TL import Language.Haskell.TH.Syntax (Loc (..))
import qualified Data.Text.Lazy.IO
import qualified Data.Text.Lazy.Builder as TB
import Language.Haskell.TH.Syntax (Loc (..), Lift (..))
#if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze (preEscapedToMarkup) import Text.Blaze (preEscapedToMarkup)
#else
import Text.Blaze (preEscapedLazyText)
#endif
import Data.Aeson (Value (Array, String)) import Data.Aeson (Value (Array, String))
import Data.Aeson.Encode (encode) import Data.Aeson.Encode (encode)
import qualified Data.Vector as Vector import qualified Data.Vector as Vector
import Network.Wai.Middleware.Gzip (GzipSettings, def) import Network.Wai.Middleware.Gzip (GzipSettings, def)
import Network.Wai.Parse (tempFileSink, lbsSink)
import qualified Paths_yesod_core import qualified Paths_yesod_core
import Data.Version (showVersion) import Data.Version (showVersion)
import System.Log.FastLogger (Logger, mkLogger, loggerDate, LogStr (..), loggerPutStr)
#if MIN_VERSION_blaze_html(0, 5, 0) import Control.Monad.Logger (LogLevel (LevelInfo, LevelOther))
preEscapedLazyText :: TL.Text -> Html import System.Log.FastLogger.Date (ZonedDate)
preEscapedLazyText = preEscapedToMarkup import System.IO (stdout)
#endif
yesodVersion :: String yesodVersion :: String
yesodVersion = showVersion Paths_yesod_core.version yesodVersion = showVersion Paths_yesod_core.version
@ -109,7 +99,8 @@ yesodVersion = showVersion Paths_yesod_core.version
class YesodDispatch sub master where class YesodDispatch sub master where
yesodDispatch yesodDispatch
:: Yesod master :: Yesod master
=> master => Logger
-> master
-> sub -> sub
-> (Route sub -> Route master) -> (Route sub -> Route master)
-> (Maybe (SessionBackend master) -> W.Application) -- ^ 404 handler -> (Maybe (SessionBackend master) -> W.Application) -- ^ 404 handler
@ -120,7 +111,8 @@ class YesodDispatch sub master where
-> W.Application -> W.Application
yesodRunner :: Yesod master yesodRunner :: Yesod master
=> GHandler sub master ChooseRep => Logger
-> GHandler sub master ChooseRep
-> master -> master
-> sub -> sub
-> Maybe (Route sub) -> Maybe (Route sub)
@ -170,6 +162,7 @@ class RenderRoute a => Yesod a where
p <- widgetToPageContent w p <- widgetToPageContent w
mmsg <- getMessage mmsg <- getMessage
hamletToRepHtml [hamlet| hamletToRepHtml [hamlet|
$newline never
$doctype 5 $doctype 5
<html> <html>
@ -290,21 +283,28 @@ $doctype 5
cookieDomain _ = Nothing cookieDomain _ = Nothing
-- | Maximum allowed length of the request body, in bytes. -- | Maximum allowed length of the request body, in bytes.
maximumContentLength :: a -> Maybe (Route a) -> Int --
-- Default: 2 megabytes.
maximumContentLength :: a -> Maybe (Route a) -> Word64
maximumContentLength _ _ = 2 * 1024 * 1024 -- 2 megabytes maximumContentLength _ _ = 2 * 1024 * 1024 -- 2 megabytes
-- | Send a message to the log. By default, prints to stdout. -- | Returns a @Logger@ to use for log messages.
--
-- Default: Sends to stdout and automatically flushes on each write.
getLogger :: a -> IO Logger
getLogger _ = mkLogger True stdout
-- | Send a message to the @Logger@ provided by @getLogger@.
messageLogger :: a messageLogger :: a
-> Logger
-> Loc -- ^ position in source code -> Loc -- ^ position in source code
-> LogLevel -> LogLevel
-> Text -- ^ message -> LogStr -- ^ message
-> IO () -> IO ()
messageLogger a loc level msg = messageLogger a logger loc level msg =
if level < logLevel a if level < logLevel a
then return () then return ()
else else formatLogMessage (loggerDate logger) loc level msg >>= loggerPutStr logger
formatLogMessage loc level msg >>=
Data.Text.Lazy.IO.putStrLn
-- | The logging level in place for this application. Any messages below -- | The logging level in place for this application. Any messages below
-- this level will simply be ignored. -- this level will simply be ignored.
@ -332,38 +332,37 @@ $doctype 5
key <- CS.getKey CS.defaultKeyFile key <- CS.getKey CS.defaultKeyFile
return $ Just $ clientSessionBackend key 120 return $ Just $ clientSessionBackend key 120
-- | How to store uploaded files.
--
-- Default: Whe nthe request body is greater than 50kb, store in a temp
-- file. Otherwise, store in memory.
fileUpload :: a
-> Word64 -- ^ request body size
-> FileUpload
fileUpload _ size
| size > 50000 = FileUploadDisk tempFileSink
| otherwise = FileUploadMemory lbsSink
messageLoggerHandler :: Yesod m formatLogMessage :: IO ZonedDate
=> Loc -> LogLevel -> Text -> GHandler s m () -> Loc
messageLoggerHandler loc level msg = do
y <- getYesod
liftIO $ messageLogger y loc level msg
data LogLevel = LevelDebug | LevelInfo | LevelWarn | LevelError | LevelOther Text
deriving (Eq, Show, Read, Ord)
instance Lift LogLevel where
lift LevelDebug = [|LevelDebug|]
lift LevelInfo = [|LevelInfo|]
lift LevelWarn = [|LevelWarn|]
lift LevelError = [|LevelError|]
lift (LevelOther x) = [|LevelOther $ T.pack $(lift $ T.unpack x)|]
formatLogMessage :: Loc
-> LogLevel -> LogLevel
-> Text -- ^ message -> LogStr -- ^ message
-> IO TL.Text -> IO [LogStr]
formatLogMessage loc level msg = do formatLogMessage getdate loc level msg = do
now <- getCurrentTime now <- getdate
return $ TB.toLazyText $ return
TB.fromText (T.pack $ show now) [ LB now
`mappend` TB.fromText " [" , LB " ["
`mappend` TB.fromText (T.pack $ drop 5 $ show level) , LS $
`mappend` TB.fromText "] " case level of
`mappend` TB.fromText msg LevelOther t -> T.unpack t
`mappend` TB.fromText " @(" _ -> drop 5 $ show level
`mappend` TB.fromText (T.pack $ fileLocationToString loc) , LB "] "
`mappend` TB.fromText ") " , msg
, LB " @("
, LS $ fileLocationToString loc
, LB ")\n"
]
-- taken from file-location package -- taken from file-location package
-- turn the TH Loc loaction information into a human readable string -- turn the TH Loc loaction information into a human readable string
@ -376,31 +375,26 @@ fileLocationToString loc = (loc_package loc) ++ ':' : (loc_module loc) ++
char = show . snd . loc_start char = show . snd . loc_start
defaultYesodRunner :: Yesod master defaultYesodRunner :: Yesod master
=> GHandler sub master ChooseRep => Logger
-> GHandler sub master ChooseRep
-> master -> master
-> sub -> sub
-> Maybe (Route sub) -> Maybe (Route sub)
-> (Route sub -> Route master) -> (Route sub -> Route master)
-> Maybe (SessionBackend master) -> Maybe (SessionBackend master)
-> W.Application -> W.Application
defaultYesodRunner _ master _ murl toMaster _ req defaultYesodRunner logger handler master sub murl toMasterRoute msb req
| maximumContentLength master (fmap toMaster murl) < len = | maximumContentLength master (fmap toMasterRoute murl) < len =
return $ W.responseLBS return $ W.responseLBS
(H.Status 413 "Too Large") (H.Status 413 "Too Large")
[("Content-Type", "text/plain")] [("Content-Type", "text/plain")]
"Request body too large to be processed." "Request body too large to be processed."
where | otherwise = do
len = fromMaybe 0 $ lookup "content-length" (W.requestHeaders req) >>= readMay
readMay s =
case reads $ S8.unpack s of
[] -> Nothing
(x, _):_ -> Just x
defaultYesodRunner handler master sub murl toMasterRoute msb req = do
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
let dontSaveSession _ _ = return [] let dontSaveSession _ _ = return []
(session, saveSession) <- liftIO $ (session, saveSession) <- liftIO $
maybe (return ([], dontSaveSession)) (\sb -> sbLoadSession sb master req now) msb maybe (return ([], dontSaveSession)) (\sb -> sbLoadSession sb master req now) msb
rr <- liftIO $ parseWaiRequest req session (isJust msb) rr <- liftIO $ parseWaiRequest req session (isJust msb) len
let h = {-# SCC "h" #-} do let h = {-# SCC "h" #-} do
case murl of case murl of
Nothing -> handler Nothing -> handler
@ -420,7 +414,8 @@ defaultYesodRunner handler master sub murl toMasterRoute msb req = do
handler handler
let sessionMap = Map.fromList . filter ((/=) tokenKey . fst) $ session let sessionMap = Map.fromList . filter ((/=) tokenKey . fst) $ session
let ra = resolveApproot master req let ra = resolveApproot master req
yar <- handlerToYAR master sub toMasterRoute let log' = messageLogger master logger
yar <- handlerToYAR master sub (fileUpload master) log' toMasterRoute
(yesodRender master ra) errorHandler rr murl sessionMap h (yesodRender master ra) errorHandler rr murl sessionMap h
extraHeaders <- case yar of extraHeaders <- case yar of
(YARPlain _ _ ct _ newSess) -> do (YARPlain _ _ ct _ newSess) -> do
@ -432,6 +427,12 @@ defaultYesodRunner handler master sub murl toMasterRoute msb req = do
return $ ("Content-Type", ct) : map headerToPair sessionHeaders return $ ("Content-Type", ct) : map headerToPair sessionHeaders
_ -> return [] _ -> return []
return $ yarToResponse yar extraHeaders return $ yarToResponse yar extraHeaders
where
len = fromMaybe 0 $ lookup "content-length" (W.requestHeaders req) >>= readMay
readMay s =
case reads $ S8.unpack s of
[] -> Nothing
(x, _):_ -> Just x
data AuthResult = Authorized | AuthenticationRequired | Unauthorized Text data AuthResult = Authorized | AuthenticationRequired | Unauthorized Text
deriving (Eq, Show, Read) deriving (Eq, Show, Read)
@ -478,18 +479,21 @@ defaultErrorHandler NotFound = do
let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r
applyLayout' "Not Found" applyLayout' "Not Found"
[hamlet| [hamlet|
$newline never
<h1>Not Found <h1>Not Found
<p>#{path'} <p>#{path'}
|] |]
defaultErrorHandler (PermissionDenied msg) = defaultErrorHandler (PermissionDenied msg) =
applyLayout' "Permission Denied" applyLayout' "Permission Denied"
[hamlet| [hamlet|
$newline never
<h1>Permission denied <h1>Permission denied
<p>#{msg} <p>#{msg}
|] |]
defaultErrorHandler (InvalidArgs ia) = defaultErrorHandler (InvalidArgs ia) =
applyLayout' "Invalid Arguments" applyLayout' "Invalid Arguments"
[hamlet| [hamlet|
$newline never
<h1>Invalid Arguments <h1>Invalid Arguments
<ul> <ul>
$forall msg <- ia $forall msg <- ia
@ -498,12 +502,14 @@ defaultErrorHandler (InvalidArgs ia) =
defaultErrorHandler (InternalError e) = defaultErrorHandler (InternalError e) =
applyLayout' "Internal Server Error" applyLayout' "Internal Server Error"
[hamlet| [hamlet|
$newline never
<h1>Internal Server Error <h1>Internal Server Error
<p>#{e} <p>#{e}
|] |]
defaultErrorHandler (BadMethod m) = defaultErrorHandler (BadMethod m) =
applyLayout' "Bad Method" applyLayout' "Bad Method"
[hamlet| [hamlet|
$newline never
<h1>Method Not Supported <h1>Method Not Supported
<p>Method "#{S8.unpack m}" not supported <p>Method "#{S8.unpack m}" not supported
|] |]
@ -521,7 +527,7 @@ maybeAuthorized r isWrite = do
return $ if x == Authorized then Just r else Nothing return $ if x == Authorized then Just r else Nothing
jsToHtml :: Javascript -> Html jsToHtml :: Javascript -> Html
jsToHtml (Javascript b) = preEscapedLazyText $ toLazyText b jsToHtml (Javascript b) = preEscapedToMarkup $ toLazyText b
jelper :: JavascriptUrl url -> HtmlUrl url jelper :: JavascriptUrl url -> HtmlUrl url
jelper = fmap jsToHtml jelper = fmap jsToHtml
@ -549,7 +555,7 @@ widgetToPageContent w = do
$ encodeUtf8 rendered $ encodeUtf8 rendered
return (mmedia, return (mmedia,
case x of case x of
Nothing -> Left $ preEscapedLazyText rendered Nothing -> Left $ preEscapedToMarkup rendered
Just y -> Right $ either id (uncurry render) y) Just y -> Right $ either id (uncurry render) y)
jsLoc <- jsLoc <-
case jscript of case jscript of
@ -563,6 +569,7 @@ widgetToPageContent w = do
-- the asynchronous loader means your page doesn't have to wait for all the js to load -- the asynchronous loader means your page doesn't have to wait for all the js to load
let (mcomplete, asyncScripts) = asyncHelper render scripts jscript jsLoc let (mcomplete, asyncScripts) = asyncHelper render scripts jscript jsLoc
regularScriptLoad = [hamlet| regularScriptLoad = [hamlet|
$newline never
$forall s <- scripts $forall s <- scripts
^{mkScriptTag s} ^{mkScriptTag s}
$maybe j <- jscript $maybe j <- jscript
@ -573,6 +580,7 @@ $maybe j <- jscript
|] |]
headAll = [hamlet| headAll = [hamlet|
$newline never
\^{head'} \^{head'}
$forall s <- stylesheets $forall s <- stylesheets
^{mkLinkTag s} ^{mkLinkTag s}
@ -595,6 +603,7 @@ $case jsLoader master
^{regularScriptLoad} ^{regularScriptLoad}
|] |]
let bodyScript = [hamlet| let bodyScript = [hamlet|
$newline never
^{body} ^{body}
^{regularScriptLoad} ^{regularScriptLoad}
|] |]
@ -641,6 +650,7 @@ jsonArray = unsafeLazyByteString . encode . Array . Vector.fromList . map String
loadJsYepnope :: Yesod master => Either Text (Route master) -> [Text] -> Maybe (HtmlUrl (Route master)) -> (HtmlUrl (Route master)) loadJsYepnope :: Yesod master => Either Text (Route master) -> [Text] -> Maybe (HtmlUrl (Route master)) -> (HtmlUrl (Route master))
loadJsYepnope eyn scripts mcomplete = loadJsYepnope eyn scripts mcomplete =
[hamlet| [hamlet|
$newline never
$maybe yn <- left eyn $maybe yn <- left eyn
<script src=#{yn}> <script src=#{yn}>
$maybe yn <- right eyn $maybe yn <- right eyn

View File

@ -4,7 +4,15 @@ module Yesod.Internal.Request
( parseWaiRequest ( parseWaiRequest
, Request (..) , Request (..)
, RequestBodyContents , RequestBodyContents
, FileInfo (..) , FileInfo
, fileName
, fileContentType
, fileSource
, fileMove
, mkFileInfoLBS
, mkFileInfoFile
, mkFileInfoSource
, FileUpload (..)
-- The below are exported for testing. -- The below are exported for testing.
, randomString , randomString
, parseWaiRequest' , parseWaiRequest'
@ -28,6 +36,10 @@ import qualified Data.Set as Set
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8With) import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Conduit
import Data.Conduit.List (sourceList)
import Data.Conduit.Binary (sourceFile, sinkFile)
import Data.Word (Word64)
-- | The parsed request information. -- | The parsed request information.
data Request = Request data Request = Request
@ -38,23 +50,27 @@ data Request = Request
, reqLangs :: [Text] , reqLangs :: [Text]
-- | A random, session-specific token used to prevent CSRF attacks. -- | A random, session-specific token used to prevent CSRF attacks.
, reqToken :: Maybe Text , reqToken :: Maybe Text
-- | Size of the request body.
, reqBodySize :: Word64
} }
parseWaiRequest :: W.Request parseWaiRequest :: W.Request
-> [(Text, ByteString)] -- ^ session -> [(Text, ByteString)] -- ^ session
-> Bool -> Bool
-> Word64
-> IO Request -> IO Request
parseWaiRequest env session' useToken = parseWaiRequest env session' useToken bodySize =
parseWaiRequest' env session' useToken <$> newStdGen parseWaiRequest' env session' useToken bodySize <$> newStdGen
parseWaiRequest' :: RandomGen g parseWaiRequest' :: RandomGen g
=> W.Request => W.Request
-> [(Text, ByteString)] -- ^ session -> [(Text, ByteString)] -- ^ session
-> Bool -> Bool
-> Word64
-> g -> g
-> Request -> Request
parseWaiRequest' env session' useToken gen = parseWaiRequest' env session' useToken bodySize gen =
Request gets'' cookies' env langs'' token Request gets'' cookies' env langs'' token bodySize
where where
gets' = queryToQueryText $ W.queryString env gets' = queryToQueryText $ W.queryString env
gets'' = map (second $ fromMaybe "") gets' gets'' = map (second $ fromMaybe "") gets'
@ -116,6 +132,19 @@ type RequestBodyContents =
data FileInfo = FileInfo data FileInfo = FileInfo
{ fileName :: Text { fileName :: Text
, fileContentType :: Text , fileContentType :: Text
, fileContent :: L.ByteString , fileSource :: Source (ResourceT IO) ByteString
, fileMove :: FilePath -> IO ()
} }
deriving (Eq, Show)
mkFileInfoLBS :: Text -> Text -> L.ByteString -> FileInfo
mkFileInfoLBS name ct lbs = FileInfo name ct (sourceList $ L.toChunks lbs) (\fp -> L.writeFile fp lbs)
mkFileInfoFile :: Text -> Text -> FilePath -> FileInfo
mkFileInfoFile name ct fp = FileInfo name ct (sourceFile fp) (\dst -> runResourceT $ sourceFile fp $$ sinkFile dst)
mkFileInfoSource :: Text -> Text -> Source (ResourceT IO) ByteString -> FileInfo
mkFileInfoSource name ct src = FileInfo name ct src (\dst -> runResourceT $ src $$ sinkFile dst)
data FileUpload = FileUploadMemory (Sink ByteString (ResourceT IO) L.ByteString)
| FileUploadDisk (Sink ByteString (ResourceT IO) FilePath)
| FileUploadSource (Sink ByteString (ResourceT IO) (Source (ResourceT IO) ByteString))

View File

@ -1,138 +0,0 @@
{-# LANGUAGE BangPatterns #-}
module Yesod.Logger
( Logger
, handle
, developmentLogger, productionLogger
, defaultDevelopmentLogger, defaultProductionLogger
, toProduction
, flushLogger
, logText
, logLazyText
, logString
, logBS
, logMsg
, formatLogText
, timed
-- * Deprecated
, makeLoggerWithHandle
, makeDefaultLogger
) where
import System.IO (Handle, stdout, hFlush)
import Data.ByteString (ByteString)
import Data.ByteString.Char8 (pack)
import Data.ByteString.Lazy (toChunks)
import qualified Data.Text.Lazy as TL
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import qualified Data.Text.Lazy.Encoding as TLE
import System.Log.FastLogger
import Network.Wai.Logger.Date (DateRef, dateInit, getDate)
-- for timed logging
import Data.Time (getCurrentTime, diffUTCTime)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Text.Printf (printf)
import Data.Text (unpack)
-- for formatter
import Language.Haskell.TH.Syntax (Loc)
import Yesod.Core (LogLevel, fileLocationToString)
data Logger = Logger {
loggerLogFun :: [LogStr] -> IO ()
, loggerHandle :: Handle
, loggerDateRef :: DateRef
}
handle :: Logger -> Handle
handle = loggerHandle
flushLogger :: Logger -> IO ()
flushLogger = hFlush . loggerHandle
makeDefaultLogger :: IO Logger
makeDefaultLogger = defaultDevelopmentLogger
{-# DEPRECATED makeDefaultLogger "Use defaultProductionLogger or defaultDevelopmentLogger instead" #-}
makeLoggerWithHandle, developmentLogger, productionLogger :: Handle -> IO Logger
makeLoggerWithHandle = productionLogger
{-# DEPRECATED makeLoggerWithHandle "Use productionLogger or developmentLogger instead" #-}
-- | uses stdout handle
defaultProductionLogger, defaultDevelopmentLogger :: IO Logger
defaultProductionLogger = productionLogger stdout
defaultDevelopmentLogger = developmentLogger stdout
productionLogger h = mkLogger h (handleToLogFun h)
-- | a development logger gets automatically flushed
developmentLogger h = mkLogger h (\bs -> (handleToLogFun h) bs >> hFlush h)
mkLogger :: Handle -> ([LogStr] -> IO ()) -> IO Logger
mkLogger h logFun = do
initHandle h
dateInit >>= return . Logger logFun h
-- convert (a development) logger to production settings
toProduction :: Logger -> Logger
toProduction (Logger _ h d) = Logger (handleToLogFun h) h d
handleToLogFun :: Handle -> ([LogStr] -> IO ())
handleToLogFun = hPutLogStr
logMsg :: Logger -> [LogStr] -> IO ()
logMsg = hPutLogStr . handle
logLazyText :: Logger -> TL.Text -> IO ()
logLazyText logger msg = loggerLogFun logger $
map LB (toChunks $ TLE.encodeUtf8 msg) ++ [newLine]
logText :: Logger -> Text -> IO ()
logText logger = logBS logger . encodeUtf8
logBS :: Logger -> ByteString -> IO ()
logBS logger msg = loggerLogFun logger $ [LB msg, newLine]
logString :: Logger -> String -> IO ()
logString logger msg = loggerLogFun logger $ [LS msg, newLine]
formatLogText :: Logger -> Loc -> LogLevel -> Text -> IO [LogStr]
formatLogText logger loc level msg = formatLogMsg logger loc level (toLB msg)
toLB :: Text -> LogStr
toLB = LB . encodeUtf8
formatLogMsg :: Logger -> Loc -> LogLevel -> LogStr -> IO [LogStr]
formatLogMsg logger loc level msg = do
date <- liftIO $ getDate $ loggerDateRef logger
return
[ LB date
, LB $ pack" ["
, LS (drop 5 $ show level)
, LB $ pack "] "
, msg
, LB $ pack " @("
, LS (fileLocationToString loc)
, LB $ pack ") "
]
newLine :: LogStr
newLine = LB $ pack "\n"
-- | Execute a monadic action and log the duration
--
timed :: MonadIO m
=> Logger -- ^ Logger
-> Text -- ^ Message
-> m a -- ^ Action
-> m a -- ^ Timed and logged action
timed logger msg action = do
start <- liftIO getCurrentTime
!result <- action
stop <- liftIO getCurrentTime
let diff = fromEnum $ diffUTCTime stop start
ms = diff `div` 10 ^ (9 :: Int)
formatted = printf " [%4dms] %s" ms (unpack msg)
liftIO $ logString logger formatted
return result

View File

@ -16,7 +16,11 @@ module Yesod.Request
-- * Request datatype -- * Request datatype
RequestBodyContents RequestBodyContents
, Request (..) , Request (..)
, FileInfo (..) , FileInfo
, fileName
, fileContentType
, fileSource
, fileMove
-- * Convenience functions -- * Convenience functions
, languages , languages
-- * Lookup parameters -- * Lookup parameters

View File

@ -53,6 +53,7 @@ module Yesod.Widget
, addScriptEither , addScriptEither
-- * Internal -- * Internal
, unGWidget , unGWidget
, whamletFileWithSettings
) where ) where
import Data.Monoid import Data.Monoid
@ -80,20 +81,16 @@ import Control.Monad.Trans.Control (MonadBaseControl (..))
import Control.Exception (throwIO) import Control.Exception (throwIO)
import qualified Text.Hamlet as NP import qualified Text.Hamlet as NP
import Data.Text.Lazy.Builder (fromLazyText) import Data.Text.Lazy.Builder (fromLazyText)
#if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze.Html (toHtml, preEscapedToMarkup) import Text.Blaze.Html (toHtml, preEscapedToMarkup)
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
#else
import Text.Blaze (toHtml, preEscapedLazyText)
#endif
import Control.Monad.Base (MonadBase (liftBase)) import Control.Monad.Base (MonadBase (liftBase))
import Control.Arrow (first) import Control.Arrow (first)
import Control.Monad.Trans.Resource import Control.Monad.Trans.Resource
#if MIN_VERSION_blaze_html(0, 5, 0) import Control.Monad.Logger
preEscapedLazyText :: TL.Text -> Html preEscapedLazyText :: TL.Text -> Html
preEscapedLazyText = preEscapedToMarkup preEscapedLazyText = preEscapedToMarkup
#endif
-- | A generic widget, allowing specification of both the subsite and master -- | A generic widget, allowing specification of both the subsite and master
-- site datatypes. While this is simply a @WriterT@, we define a newtype for -- site datatypes. While this is simply a @WriterT@, we define a newtype for
@ -272,6 +269,9 @@ whamlet = NP.hamletWithSettings rules NP.defaultHamletSettings
whamletFile :: FilePath -> Q Exp whamletFile :: FilePath -> Q Exp
whamletFile = NP.hamletFileWithSettings rules NP.defaultHamletSettings whamletFile = NP.hamletFileWithSettings rules NP.defaultHamletSettings
whamletFileWithSettings :: NP.HamletSettings -> FilePath -> Q Exp
whamletFileWithSettings = NP.hamletFileWithSettings rules
rules :: Q NP.HamletRules rules :: Q NP.HamletRules
rules = do rules = do
ah <- [|toWidget|] ah <- [|toWidget|]
@ -344,3 +344,6 @@ instance MonadResource (GWidget sub master) where
register = lift . register register = lift . register
release = lift . release release = lift . release
resourceMask = lift . resourceMask resourceMask = lift . resourceMask
instance MonadLogger (GWidget sub master) where
monadLoggerLog a b = lift . monadLoggerLog a b

View File

@ -5,7 +5,7 @@
import Yesod.Core import Yesod.Core
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Network.Wai.Handler.Warp (run) import Network.Wai.Handler.Warp (run)
import Data.Text (unpack) import Data.Text (unpack, pack)
import Text.Julius (julius) import Text.Julius (julius)
data Subsite = Subsite String data Subsite = Subsite String
@ -22,13 +22,13 @@ getSubRootR = do
Subsite s <- getYesodSub Subsite s <- getYesodSub
tm <- getRouteToMaster tm <- getRouteToMaster
render <- getUrlRender render <- getUrlRender
$(logDebug) "I'm in SubRootR" $logDebug "I'm in SubRootR"
return $ RepPlain $ toContent $ "Hello Sub World: " ++ s ++ ". " ++ unpack (render (tm SubRootR)) return $ RepPlain $ toContent $ "Hello Sub World: " ++ s ++ ". " ++ unpack (render (tm SubRootR))
handleSubMultiR :: Yesod m => Strings -> GHandler Subsite m RepPlain handleSubMultiR :: Yesod m => Strings -> GHandler Subsite m RepPlain
handleSubMultiR x = do handleSubMultiR x = do
Subsite y <- getYesodSub Subsite y <- getYesodSub
$(logInfo) "In SubMultiR" $logInfo "In SubMultiR"
return . RepPlain . toContent . show $ (x, y) return . RepPlain . toContent . show $ (x, y)
data HelloWorld = HelloWorld { getSubsite :: String -> Subsite } data HelloWorld = HelloWorld { getSubsite :: String -> Subsite }
@ -38,7 +38,7 @@ mkYesod "HelloWorld" [parseRoutes|
|] |]
instance Yesod HelloWorld where instance Yesod HelloWorld where
addStaticContent a b c = do addStaticContent a b c = do
liftIO $ print (a, b, c) $logInfo $ pack $ show (a, b, c)
return Nothing return Nothing
getRootR = do getRootR = do

View File

@ -26,7 +26,7 @@ instance RenderRoute Subsite where
renderRoute (SubsiteRoute x) = (x, []) renderRoute (SubsiteRoute x) = (x, [])
instance YesodDispatch Subsite master where instance YesodDispatch Subsite master where
yesodDispatch _ _ _ _ _ _ pieces _ _ = return $ responseLBS yesodDispatch _ _ _ _ _ _ _ pieces _ _ = return $ responseLBS
status200 status200
[ ("Content-Type", "SUBSITE") [ ("Content-Type", "SUBSITE")
] $ L8.pack $ show pieces ] $ L8.pack $ show pieces

View File

@ -24,7 +24,9 @@ mkYesod "App" [parseRoutes|
instance Yesod App instance Yesod App
getHomeR :: Handler RepHtml getHomeR :: Handler RepHtml
getHomeR = defaultLayout $ toWidget [hamlet| getHomeR = do
$logDebug "Testing logging"
defaultLayout $ toWidget [hamlet|
$doctype 5 $doctype 5
<html> <html>
@ -49,7 +51,7 @@ postFirstThingR = do
postAfterRunRequestBodyR = do postAfterRunRequestBodyR = do
x <- runRequestBody x <- runRequestBody
_ <- error $ show x _ <- error $ show $ fst x
getHomeR getHomeR
errorHandlingTest :: Spec errorHandlingTest :: Spec

View File

@ -40,19 +40,19 @@ tokenSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqToken)"
noDisabledToken :: Bool noDisabledToken :: Bool
noDisabledToken = reqToken r == Nothing where noDisabledToken = reqToken r == Nothing where
r = parseWaiRequest' defaultRequest [] False g r = parseWaiRequest' defaultRequest [] False 0 g
ignoreDisabledToken :: Bool ignoreDisabledToken :: Bool
ignoreDisabledToken = reqToken r == Nothing where ignoreDisabledToken = reqToken r == Nothing where
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] False g r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] False 0 g
useOldToken :: Bool useOldToken :: Bool
useOldToken = reqToken r == Just "old" where useOldToken = reqToken r == Just "old" where
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True g r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True 0 g
generateToken :: Bool generateToken :: Bool
generateToken = reqToken r /= Nothing where generateToken = reqToken r /= Nothing where
r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True g r = parseWaiRequest' defaultRequest [("_TOKEN", "old")] True 0 g
langSpecs :: Spec langSpecs :: Spec
@ -67,21 +67,21 @@ langSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqLangs)"
respectAcceptLangs :: Bool respectAcceptLangs :: Bool
respectAcceptLangs = reqLangs r == ["en-US", "es", "en"] where respectAcceptLangs = reqLangs r == ["en-US", "es", "en"] where
r = parseWaiRequest' defaultRequest r = parseWaiRequest' defaultRequest
{ requestHeaders = [("Accept-Language", "en-US, es")] } [] False g { requestHeaders = [("Accept-Language", "en-US, es")] } [] False 0 g
respectSessionLang :: Bool respectSessionLang :: Bool
respectSessionLang = reqLangs r == ["en"] where respectSessionLang = reqLangs r == ["en"] where
r = parseWaiRequest' defaultRequest [("_LANG", "en")] False g r = parseWaiRequest' defaultRequest [("_LANG", "en")] False 0 g
respectCookieLang :: Bool respectCookieLang :: Bool
respectCookieLang = reqLangs r == ["en"] where respectCookieLang = reqLangs r == ["en"] where
r = parseWaiRequest' defaultRequest r = parseWaiRequest' defaultRequest
{ requestHeaders = [("Cookie", "_LANG=en")] { requestHeaders = [("Cookie", "_LANG=en")]
} [] False g } [] False 0 g
respectQueryLang :: Bool respectQueryLang :: Bool
respectQueryLang = reqLangs r == ["en-US", "en"] where respectQueryLang = reqLangs r == ["en-US", "en"] where
r = parseWaiRequest' defaultRequest { queryString = [("_LANG", Just "en-US")] } [] False g r = parseWaiRequest' defaultRequest { queryString = [("_LANG", Just "en-US")] } [] False 0 g
prioritizeLangs :: Bool prioritizeLangs :: Bool
prioritizeLangs = reqLangs r == ["en-QUERY", "en-COOKIE", "en-SESSION", "en", "es"] where prioritizeLangs = reqLangs r == ["en-QUERY", "en-COOKIE", "en-SESSION", "en", "es"] where
@ -90,7 +90,7 @@ prioritizeLangs = reqLangs r == ["en-QUERY", "en-COOKIE", "en-SESSION", "en", "e
, ("Cookie", "_LANG=en-COOKIE") , ("Cookie", "_LANG=en-COOKIE")
] ]
, queryString = [("_LANG", Just "en-QUERY")] , queryString = [("_LANG", Just "en-QUERY")]
} [("_LANG", "en-SESSION")] False g } [("_LANG", "en-SESSION")] False 0 g
internalRequestTest :: Spec internalRequestTest :: Spec

View File

@ -31,4 +31,4 @@ runner f = toWaiApp Y >>= runSession f
case_linkToHome :: IO () case_linkToHome :: IO ()
case_linkToHome = runner $ do case_linkToHome = runner $ do
res <- request defaultRequest res <- request defaultRequest
assertBody "<!DOCTYPE html>\n<html><head><title></title></head><body><a href=\"/\"></a></body></html>" res assertBody "<!DOCTYPE html>\n<html><head><title></title></head><body><a href=\"/\"></a>\n</body></html>" res

View File

@ -56,12 +56,13 @@ getTowidgetR = defaultLayout $ do
toWidget [lucius|foo{bar:baz}|] toWidget [lucius|foo{bar:baz}|]
toWidgetHead [lucius|foo{bar:baz}|] toWidgetHead [lucius|foo{bar:baz}|]
toWidget [hamlet|<foo>|] :: Widget toWidget [hamlet|<foo>|]
toWidgetHead [hamlet|<foo>|] toWidgetHead [hamlet|<foo>|]
toWidgetBody [hamlet|<foo>|] toWidgetBody [hamlet|<foo>|]
getWhamletR :: Handler RepHtml getWhamletR :: Handler RepHtml
getWhamletR = defaultLayout [whamlet| getWhamletR = defaultLayout [whamlet|
$newline never
<h1>Test <h1>Test
<h2>@{WhamletR} <h2>@{WhamletR}
<h3>_{Goodbye} <h3>_{Goodbye}
@ -69,10 +70,14 @@ getWhamletR = defaultLayout [whamlet|
^{embed} ^{embed}
|] |]
where where
embed = [whamlet|<h4>Embed|] embed = [whamlet|
$newline never
<h4>Embed
|]
getAutoR :: Handler RepHtml getAutoR :: Handler RepHtml
getAutoR = defaultLayout [whamlet| getAutoR = defaultLayout [whamlet|
$newline never
^{someHtml} ^{someHtml}
|] |]
where where

View File

@ -1,5 +1,5 @@
name: yesod-core name: yesod-core
version: 1.0.1.3 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -40,10 +40,6 @@ flag test
description: Build the executable to run unit tests description: Build the executable to run unit tests
default: False default: False
flag blaze_html_0_5
description: use blaze-html 0.5 and blaze-markup 0.5
default: True
library library
-- Work around a bug in cabal. Without this, wai-test doesn't get built and -- Work around a bug in cabal. Without this, wai-test doesn't get built and
-- we have a missing dependency during --enable-tests builds. -- we have a missing dependency during --enable-tests builds.
@ -52,21 +48,21 @@ library
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.3 && < 5
, time >= 1.1.4 , time >= 1.1.4
, yesod-routes >= 1.0 && < 1.1 , yesod-routes >= 1.1 && < 1.2
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, wai-extra >= 1.2 && < 1.3 , wai-extra >= 1.3 && < 1.4
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, text >= 0.7 && < 0.12 , text >= 0.7 && < 0.12
, template-haskell , template-haskell
, path-pieces >= 0.1 && < 0.2 , path-pieces >= 0.1 && < 0.2
, hamlet >= 1.0 && < 1.1 , hamlet >= 1.1 && < 1.2
, shakespeare >= 1.0 && < 1.1 , shakespeare >= 1.0 && < 1.1
, shakespeare-js >= 1.0 && < 1.1 , shakespeare-js >= 1.0 && < 1.1
, shakespeare-css >= 1.0 && < 1.1 , shakespeare-css >= 1.0 && < 1.1
, shakespeare-i18n >= 1.0 && < 1.1 , shakespeare-i18n >= 1.0 && < 1.1
, blaze-builder >= 0.2.1.4 && < 0.4 , blaze-builder >= 0.2.1.4 && < 0.4
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, clientsession >= 0.7.3.1 && < 0.8 , clientsession >= 0.8 && < 0.9
, random >= 1.0.0.2 && < 1.1 , random >= 1.0.0.2 && < 1.1
, cereal >= 0.3 && < 0.4 , cereal >= 0.3 && < 0.4
, old-locale >= 1.0.0.2 && < 1.1 , old-locale >= 1.0.0.2 && < 1.1
@ -75,31 +71,24 @@ library
, monad-control >= 0.3 && < 0.4 , monad-control >= 0.3 && < 0.4
, transformers-base >= 0.4 , transformers-base >= 0.4
, cookie >= 0.4 && < 0.5 , cookie >= 0.4 && < 0.5
, http-types >= 0.6.5 && < 0.7 , http-types >= 0.7 && < 0.8
, case-insensitive >= 0.2 , case-insensitive >= 0.2
, parsec >= 2 && < 3.2 , parsec >= 2 && < 3.2
, directory >= 1 && < 1.2 , directory >= 1 && < 1.2
, vector >= 0.9 && < 0.10 , vector >= 0.9 && < 0.10
, aeson >= 0.5 , aeson >= 0.5
, fast-logger >= 0.0.2 , fast-logger >= 0.2 && < 0.3
, wai-logger >= 0.0.1 , monad-logger >= 0.2 && < 0.3
, conduit >= 0.4 && < 0.5 , conduit >= 0.5 && < 0.6
, resourcet >= 0.3 && < 0.4 , resourcet >= 0.3 && < 0.4
, lifted-base >= 0.1 && < 0.2 , lifted-base >= 0.1 && < 0.2
, blaze-html >= 0.5 && < 0.6
if flag(blaze_html_0_5) , blaze-markup >= 0.5.1 && < 0.6
build-depends:
blaze-html >= 0.5 && < 0.6
, blaze-markup >= 0.5.1 && < 0.6
else
build-depends:
blaze-html >= 0.4 && < 0.5
exposed-modules: Yesod.Content exposed-modules: Yesod.Content
Yesod.Core Yesod.Core
Yesod.Dispatch Yesod.Dispatch
Yesod.Handler Yesod.Handler
Yesod.Logger
Yesod.Request Yesod.Request
Yesod.Widget Yesod.Widget
Yesod.Message Yesod.Message

View File

@ -7,7 +7,6 @@ module Yesod.Default.Main
) where ) where
import Yesod.Default.Config import Yesod.Default.Config
import Yesod.Logger (Logger, defaultDevelopmentLogger, logString)
import Network.Wai (Application) import Network.Wai (Application)
import Network.Wai.Handler.Warp import Network.Wai.Handler.Warp
(runSettings, defaultSettings, settingsPort, settingsHost) (runSettings, defaultSettings, settingsPort, settingsHost)
@ -33,12 +32,11 @@ import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
-- --
defaultMain :: (Show env, Read env) defaultMain :: (Show env, Read env)
=> IO (AppConfig env extra) => IO (AppConfig env extra)
-> (AppConfig env extra -> Logger -> IO Application) -> (AppConfig env extra -> IO Application)
-> IO () -> IO ()
defaultMain load getApp = do defaultMain load getApp = do
config <- load config <- load
logger <- defaultDevelopmentLogger app <- getApp config
app <- getApp config logger
print $ appHost config print $ appHost config
runSettings defaultSettings runSettings defaultSettings
{ settingsPort = appPort config { settingsPort = appPort config
@ -80,12 +78,11 @@ defaultRunner f app = do
defaultDevelApp defaultDevelApp
:: (Show env, Read env) :: (Show env, Read env)
=> IO (AppConfig env extra) -- ^ A means to load your development @'AppConfig'@ => IO (AppConfig env extra) -- ^ A means to load your development @'AppConfig'@
-> (AppConfig env extra -> Logger -> IO Application) -- ^ Get your @Application@ -> (AppConfig env extra -> IO Application) -- ^ Get your @Application@
-> IO (Int, Application) -> IO (Int, Application)
defaultDevelApp load getApp = do defaultDevelApp load getApp = do
conf <- load conf <- load
logger <- defaultDevelopmentLogger
let p = appPort conf let p = appPort conf
logString logger $ "Devel application launched: http://localhost:" ++ show p putStrLn $ "Devel application launched: http://localhost:" ++ show p
app <- getApp conf logger app <- getApp conf
return (p, app) return (p, app)

View File

@ -7,7 +7,11 @@ module Yesod.Default.Util
, globFile , globFile
, widgetFileNoReload , widgetFileNoReload
, widgetFileReload , widgetFileReload
, widgetFileJsCss , TemplateLanguage (..)
, defaultTemplateLanguages
, WidgetFileSettings
, wfsLanguages
, wfsHamletSettings
) where ) where
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
@ -20,7 +24,9 @@ import Language.Haskell.TH.Syntax
import Text.Lucius (luciusFile, luciusFileReload) import Text.Lucius (luciusFile, luciusFileReload)
import Text.Julius (juliusFile, juliusFileReload) import Text.Julius (juliusFile, juliusFileReload)
import Text.Cassius (cassiusFile, cassiusFileReload) import Text.Cassius (cassiusFile, cassiusFileReload)
import Text.Hamlet (HamletSettings, defaultHamletSettings)
import Data.Maybe (catMaybes) import Data.Maybe (catMaybes)
import Data.Default (Default (def))
-- | An implementation of 'addStaticContent' which stores the contents in an -- | An implementation of 'addStaticContent' which stores the contents in an
-- external file. Files are created in the given static folder with names based -- external file. Files are created in the given static folder with names based
@ -57,34 +63,40 @@ addStaticContentExternal minify hash staticDir toRoute ext' _ content = do
globFile :: String -> String -> FilePath globFile :: String -> String -> FilePath
globFile kind x = "templates/" ++ x ++ "." ++ kind globFile kind x = "templates/" ++ x ++ "." ++ kind
widgetFileNoReload :: FilePath -> Q Exp data TemplateLanguage = TemplateLanguage
widgetFileNoReload x = combine "widgetFileNoReload" x { tlRequiresToWidget :: Bool
[ whenExists x False "hamlet" whamletFile , tlExtension :: String
, whenExists x True "cassius" cassiusFile , tlNoReload :: FilePath -> Q Exp
, whenExists x True "julius" juliusFile , tlReload :: FilePath -> Q Exp
, whenExists x True "lucius" luciusFile }
]
widgetFileReload :: FilePath -> Q Exp defaultTemplateLanguages :: HamletSettings -> [TemplateLanguage]
widgetFileReload x = combine "widgetFileReload" x defaultTemplateLanguages hset =
[ whenExists x False "hamlet" whamletFile [ TemplateLanguage False "hamlet" whamletFile' whamletFile'
, whenExists x True "cassius" cassiusFileReload , TemplateLanguage True "cassius" cassiusFile cassiusFileReload
, whenExists x True "julius" juliusFileReload , TemplateLanguage True "julius" juliusFile juliusFileReload
, whenExists x True "lucius" luciusFileReload , TemplateLanguage True "lucius" luciusFile luciusFileReload
] ]
where
whamletFile' = whamletFileWithSettings hset
widgetFileJsCss :: (String, FilePath -> Q Exp) -- ^ JavaScript file extenstion and loading function. example: ("julius", juliusFileReload) data WidgetFileSettings = WidgetFileSettings
-> (String, FilePath -> Q Exp) -- ^ Css file extenstion and loading function. example: ("cassius", cassiusFileReload) { wfsLanguages :: HamletSettings -> [TemplateLanguage]
-> FilePath -> Q Exp , wfsHamletSettings :: HamletSettings
widgetFileJsCss (jsExt, jsLoad) (csExt, csLoad) x = combine "widgetFileJsCss" x }
[ whenExists x False "hamlet" whamletFile
, whenExists x True csExt csLoad
, whenExists x True jsExt jsLoad
]
combine :: String -> String -> [Q (Maybe Exp)] -> Q Exp instance Default WidgetFileSettings where
combine func file qmexps = do def = WidgetFileSettings defaultTemplateLanguages defaultHamletSettings
mexps <- sequence qmexps
widgetFileNoReload :: WidgetFileSettings -> FilePath -> Q Exp
widgetFileNoReload wfs x = combine "widgetFileNoReload" x False $ wfsLanguages wfs $ wfsHamletSettings wfs
widgetFileReload :: WidgetFileSettings -> FilePath -> Q Exp
widgetFileReload wfs x = combine "widgetFileReload" x True $ wfsLanguages wfs $ wfsHamletSettings wfs
combine :: String -> String -> Bool -> [TemplateLanguage] -> Q Exp
combine func file isReload tls = do
mexps <- qmexps
case catMaybes mexps of case catMaybes mexps of
[] -> error $ concat [] -> error $ concat
[ "Called " [ "Called "
@ -94,6 +106,12 @@ combine func file qmexps = do
, ", but no template were found." , ", but no template were found."
] ]
exps -> return $ DoE $ map NoBindS exps exps -> return $ DoE $ map NoBindS exps
where
qmexps :: Q [Maybe Exp]
qmexps = mapM go tls
go :: TemplateLanguage -> Q (Maybe Exp)
go tl = whenExists file (tlRequiresToWidget tl) (tlExtension tl) ((if isReload then tlReload else tlNoReload) tl)
whenExists :: String whenExists :: String
-> Bool -- ^ requires toWidget wrap -> Bool -- ^ requires toWidget wrap

View File

@ -1,5 +1,5 @@
name: yesod-default name: yesod-default
version: 1.0.1.1 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Patrick Brisbin author: Patrick Brisbin
@ -18,10 +18,10 @@ library
cpp-options: -DWINDOWS cpp-options: -DWINDOWS
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, warp >= 1.2 && < 1.3 , warp >= 1.3 && < 1.4
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, wai-extra >= 1.2 && < 1.3 , wai-extra >= 1.3 && < 1.4
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, text >= 0.9 , text >= 0.9
@ -29,9 +29,11 @@ library
, shakespeare-css >= 1.0 && < 1.1 , shakespeare-css >= 1.0 && < 1.1
, shakespeare-js >= 1.0 && < 1.1 , shakespeare-js >= 1.0 && < 1.1
, template-haskell , template-haskell
, yaml >= 0.7 && < 0.8 , yaml >= 0.8 && < 0.9
, network-conduit >= 0.4 && < 0.5 , network-conduit >= 0.5 && < 0.6
, unordered-containers , unordered-containers
, hamlet >= 1.1 && < 1.2
, data-default
if !os(windows) if !os(windows)
build-depends: unix build-depends: unix

View File

@ -111,6 +111,7 @@ intField = Field
_ -> Left $ MsgInvalidInteger s _ -> Left $ MsgInvalidInteger s
, fieldView = \theId name attrs val isReq -> toWidget [hamlet| , fieldView = \theId name attrs val isReq -> toWidget [hamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="number" :isReq:required="" value="#{showVal val}"> <input id="#{theId}" name="#{name}" *{attrs} type="number" :isReq:required="" value="#{showVal val}">
|] |]
} }
@ -126,6 +127,7 @@ doubleField = Field
_ -> Left $ MsgInvalidNumber s _ -> Left $ MsgInvalidNumber s
, fieldView = \theId name attrs val isReq -> toWidget [hamlet| , fieldView = \theId name attrs val isReq -> toWidget [hamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required="" value="#{showVal val}"> <input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required="" value="#{showVal val}">
|] |]
} }
@ -135,6 +137,7 @@ dayField :: RenderMessage master FormMessage => Field sub master Day
dayField = Field dayField = Field
{ fieldParse = blank $ parseDate . unpack { fieldParse = blank $ parseDate . unpack
, fieldView = \theId name attrs val isReq -> toWidget [hamlet| , fieldView = \theId name attrs val isReq -> toWidget [hamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="date" :isReq:required="" value="#{showVal val}"> <input id="#{theId}" name="#{name}" *{attrs} type="date" :isReq:required="" value="#{showVal val}">
|] |]
} }
@ -144,6 +147,7 @@ timeField :: RenderMessage master FormMessage => Field sub master TimeOfDay
timeField = Field timeField = Field
{ fieldParse = blank $ parseTime . unpack { fieldParse = blank $ parseTime . unpack
, fieldView = \theId name attrs val isReq -> toWidget [hamlet| , fieldView = \theId name attrs val isReq -> toWidget [hamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} :isReq:required="" value="#{showVal val}"> <input id="#{theId}" name="#{name}" *{attrs} :isReq:required="" value="#{showVal val}">
|] |]
} }
@ -158,6 +162,7 @@ htmlField :: RenderMessage master FormMessage => Field sub master Html
htmlField = Field htmlField = Field
{ fieldParse = blank $ Right . preEscapedText . sanitizeBalance { fieldParse = blank $ Right . preEscapedText . sanitizeBalance
, fieldView = \theId name attrs val _isReq -> toWidget [hamlet| , fieldView = \theId name attrs val _isReq -> toWidget [hamlet|
$newline never
$# FIXME: There was a class="html" attribute, for what purpose? $# FIXME: There was a class="html" attribute, for what purpose?
<textarea id="#{theId}" name="#{name}" *{attrs}>#{showVal val} <textarea id="#{theId}" name="#{name}" *{attrs}>#{showVal val}
|] |]
@ -186,6 +191,7 @@ textareaField :: RenderMessage master FormMessage => Field sub master Textarea
textareaField = Field textareaField = Field
{ fieldParse = blank $ Right . Textarea { fieldParse = blank $ Right . Textarea
, fieldView = \theId name attrs val _isReq -> toWidget [hamlet| , fieldView = \theId name attrs val _isReq -> toWidget [hamlet|
$newline never
<textarea id="#{theId}" name="#{name}" *{attrs}>#{either id unTextarea val} <textarea id="#{theId}" name="#{name}" *{attrs}>#{either id unTextarea val}
|] |]
} }
@ -195,6 +201,7 @@ hiddenField :: (PathPiece p, RenderMessage master FormMessage)
hiddenField = Field hiddenField = Field
{ fieldParse = blank $ maybe (Left MsgValueRequired) Right . fromPathPiece { fieldParse = blank $ maybe (Left MsgValueRequired) Right . fromPathPiece
, fieldView = \theId name attrs val _isReq -> toWidget [hamlet| , fieldView = \theId name attrs val _isReq -> toWidget [hamlet|
$newline never
<input type="hidden" id="#{theId}" name="#{name}" *{attrs} value="#{either id toPathPiece val}"> <input type="hidden" id="#{theId}" name="#{name}" *{attrs} value="#{either id toPathPiece val}">
|] |]
} }
@ -204,6 +211,7 @@ textField = Field
{ fieldParse = blank $ Right { fieldParse = blank $ Right
, fieldView = \theId name attrs val isReq -> , fieldView = \theId name attrs val isReq ->
[whamlet| [whamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required value="#{either id id val}"> <input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required value="#{either id id val}">
|] |]
} }
@ -212,6 +220,7 @@ passwordField :: RenderMessage master FormMessage => Field sub master Text
passwordField = Field passwordField = Field
{ fieldParse = blank $ Right { fieldParse = blank $ Right
, fieldView = \theId name attrs val isReq -> toWidget [hamlet| , fieldView = \theId name attrs val isReq -> toWidget [hamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="password" :isReq:required="" value="#{either id id val}"> <input id="#{theId}" name="#{name}" *{attrs} type="password" :isReq:required="" value="#{either id id val}">
|] |]
} }
@ -261,6 +270,7 @@ emailField = Field
then Right s then Right s
else Left $ MsgInvalidEmail s else Left $ MsgInvalidEmail s
, fieldView = \theId name attrs val isReq -> toWidget [hamlet| , fieldView = \theId name attrs val isReq -> toWidget [hamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="email" :isReq:required="" value="#{either id id val}"> <input id="#{theId}" name="#{name}" *{attrs} type="email" :isReq:required="" value="#{either id id val}">
|] |]
} }
@ -271,11 +281,15 @@ searchField autoFocus = Field
{ fieldParse = blank Right { fieldParse = blank Right
, fieldView = \theId name attrs val isReq -> do , fieldView = \theId name attrs val isReq -> do
[whamlet|\ [whamlet|\
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="search" :isReq:required="" :autoFocus:autofocus="" value="#{either id id val}"> <input id="#{theId}" name="#{name}" *{attrs} type="search" :isReq:required="" :autoFocus:autofocus="" value="#{either id id val}">
|] |]
when autoFocus $ do when autoFocus $ do
-- we want this javascript to be placed immediately after the field -- we want this javascript to be placed immediately after the field
[whamlet|<script>if (!('autofocus' in document.createElement('input'))) {document.getElementById('#{theId}').focus();}|] [whamlet|
$newline never
<script>if (!('autofocus' in document.createElement('input'))) {document.getElementById('#{theId}').focus();}
|]
toWidget [cassius| toWidget [cassius|
#{theId} #{theId}
-webkit-appearance: textfield -webkit-appearance: textfield
@ -290,6 +304,7 @@ urlField = Field
Just _ -> Right s Just _ -> Right s
, fieldView = \theId name attrs val isReq -> , fieldView = \theId name attrs val isReq ->
[whamlet| [whamlet|
$newline never
<input ##{theId} name=#{name} *{attrs} type=url :isReq:required value=#{either id id val}> <input ##{theId} name=#{name} *{attrs} type=url :isReq:required value=#{either id id val}>
|] |]
} }
@ -299,9 +314,18 @@ selectFieldList = selectField . optionsPairs
selectField :: (Eq a, RenderMessage master FormMessage) => GHandler sub master (OptionList a) -> Field sub master a selectField :: (Eq a, RenderMessage master FormMessage) => GHandler sub master (OptionList a) -> Field sub master a
selectField = selectFieldHelper selectField = selectFieldHelper
(\theId name inside -> [whamlet|<select ##{theId} name=#{name}>^{inside}|]) -- outside (\theId name inside -> [whamlet|
(\_theId _name isSel -> [whamlet|<option value=none :isSel:selected>_{MsgSelectNone}|]) -- onOpt $newline never
(\_theId _name attrs value isSel text -> [whamlet|<option value=#{value} :isSel:selected *{attrs}>#{text}|]) -- inside <select ##{theId} name=#{name}>^{inside}
|]) -- outside
(\_theId _name isSel -> [whamlet|
$newline never
<option value=none :isSel:selected>_{MsgSelectNone}
|]) -- onOpt
(\_theId _name attrs value isSel text -> [whamlet|
$newline never
<option value=#{value} :isSel:selected *{attrs}>#{text}
|]) -- inside
multiSelectFieldList :: (Eq a, RenderMessage master FormMessage, RenderMessage master msg) => [(msg, a)] -> Field sub master [a] multiSelectFieldList :: (Eq a, RenderMessage master FormMessage, RenderMessage master msg) => [(msg, a)] -> Field sub master [a]
multiSelectFieldList = multiSelectField . optionsPairs multiSelectFieldList = multiSelectField . optionsPairs
@ -323,6 +347,7 @@ multiSelectField ioptlist =
opts <- fmap olOptions $ lift ioptlist opts <- fmap olOptions $ lift ioptlist
let selOpts = map (id &&& (optselected val)) opts let selOpts = map (id &&& (optselected val)) opts
[whamlet| [whamlet|
$newline never
<select ##{theId} name=#{name} :isReq:required multiple *{attrs}> <select ##{theId} name=#{name} :isReq:required multiple *{attrs}>
$forall (opt, optsel) <- selOpts $forall (opt, optsel) <- selOpts
<option value=#{optionExternalValue opt} :optsel:selected>#{optionDisplay opt} <option value=#{optionExternalValue opt} :optsel:selected>#{optionDisplay opt}
@ -336,13 +361,18 @@ radioFieldList = radioField . optionsPairs
radioField :: (Eq a, RenderMessage master FormMessage) => GHandler sub master (OptionList a) -> Field sub master a radioField :: (Eq a, RenderMessage master FormMessage) => GHandler sub master (OptionList a) -> Field sub master a
radioField = selectFieldHelper radioField = selectFieldHelper
(\theId _name inside -> [whamlet|<div ##{theId}>^{inside}|]) (\theId _name inside -> [whamlet|
$newline never
<div ##{theId}>^{inside}
|])
(\theId name isSel -> [whamlet| (\theId name isSel -> [whamlet|
$newline never
<div> <div>
<input id=#{theId}-none type=radio name=#{name} value=none :isSel:checked> <input id=#{theId}-none type=radio name=#{name} value=none :isSel:checked>
<label for=#{theId}-none>_{MsgSelectNone} <label for=#{theId}-none>_{MsgSelectNone}
|]) |])
(\theId name attrs value isSel text -> [whamlet| (\theId name attrs value isSel text -> [whamlet|
$newline never
<div> <div>
<input id=#{theId}-#{value} type=radio name=#{name} value=#{value} :isSel:checked *{attrs}> <input id=#{theId}-#{value} type=radio name=#{name} value=#{value} :isSel:checked *{attrs}>
<label for=#{theId}-#{value}>#{text} <label for=#{theId}-#{value}>#{text}
@ -352,6 +382,7 @@ boolField :: RenderMessage master FormMessage => Field sub master Bool
boolField = Field boolField = Field
{ fieldParse = return . boolParser { fieldParse = return . boolParser
, fieldView = \theId name attrs val isReq -> [whamlet| , fieldView = \theId name attrs val isReq -> [whamlet|
$newline never
$if not isReq $if not isReq
<input id=#{theId}-none *{attrs} type=radio name=#{name} value=none checked> <input id=#{theId}-none *{attrs} type=radio name=#{name} value=none checked>
<label for=#{theId}-none>_{MsgSelectNone} <label for=#{theId}-none>_{MsgSelectNone}
@ -386,6 +417,7 @@ checkBoxField :: RenderMessage m FormMessage => Field s m Bool
checkBoxField = Field checkBoxField = Field
{ fieldParse = return . checkBoxParser { fieldParse = return . checkBoxParser
, fieldView = \theId name attrs val _ -> [whamlet| , fieldView = \theId name attrs val _ -> [whamlet|
$newline never
<input id=#{theId} *{attrs} type=checkbox name=#{name} value=yes :showVal id val:checked> <input id=#{theId} *{attrs} type=checkbox name=#{name} value=yes :showVal id val:checked>
|] |]
} }
@ -501,6 +533,7 @@ fileAFormReq fs = AForm $ \(master, langs) menvs ints -> do
, fvTooltip = fmap (toHtml . renderMessage master langs) $ fsTooltip fs , fvTooltip = fmap (toHtml . renderMessage master langs) $ fsTooltip fs
, fvId = id' , fvId = id'
, fvInput = [whamlet| , fvInput = [whamlet|
$newline never
<input type=file name=#{name} ##{id'} *{fsAttrs fs}> <input type=file name=#{name} ##{id'} *{fsAttrs fs}>
|] |]
, fvErrors = errs , fvErrors = errs
@ -529,6 +562,7 @@ fileAFormOpt fs = AForm $ \(master, langs) menvs ints -> do
, fvTooltip = fmap (toHtml . renderMessage master langs) $ fsTooltip fs , fvTooltip = fmap (toHtml . renderMessage master langs) $ fsTooltip fs
, fvId = id' , fvId = id'
, fvInput = [whamlet| , fvInput = [whamlet|
$newline never
<input type=file name=#{name} ##{id'} *{fsAttrs fs}> <input type=file name=#{name} ##{id'} *{fsAttrs fs}>
|] |]
, fvErrors = errs , fvErrors = errs

View File

@ -55,14 +55,13 @@ import Text.Blaze (Html, toHtml)
import Yesod.Handler (GHandler, getRequest, runRequestBody, newIdent, getYesod) import Yesod.Handler (GHandler, getRequest, runRequestBody, newIdent, getYesod)
import Yesod.Core (RenderMessage, SomeMessage (..)) import Yesod.Core (RenderMessage, SomeMessage (..))
import Yesod.Widget (GWidget, whamlet) import Yesod.Widget (GWidget, whamlet)
import Yesod.Request (reqToken, reqWaiRequest, reqGetParams, languages, FileInfo (..)) import Yesod.Request (reqToken, reqWaiRequest, reqGetParams, languages)
import Network.Wai (requestMethod) import Network.Wai (requestMethod)
import Text.Hamlet (shamlet) import Text.Hamlet (shamlet)
import Data.Monoid (mempty) import Data.Monoid (mempty)
import Data.Maybe (listToMaybe, fromMaybe) import Data.Maybe (listToMaybe, fromMaybe)
import Yesod.Message (RenderMessage (..)) import Yesod.Message (RenderMessage (..))
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.ByteString.Lazy as L
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))
import Control.Arrow (first) import Control.Arrow (first)
@ -188,7 +187,10 @@ postHelper form env = do
let token = let token =
case reqToken req of case reqToken req of
Nothing -> mempty Nothing -> mempty
Just n -> [shamlet|<input type=hidden name=#{tokenKey} value=#{n}>|] Just n -> [shamlet|
$newline never
<input type=hidden name=#{tokenKey} value=#{n}>
|]
m <- getYesod m <- getYesod
langs <- languages langs <- languages
((res, xml), enctype) <- runFormGeneric (form token) m langs env ((res, xml), enctype) <- runFormGeneric (form token) m langs env
@ -218,9 +220,7 @@ postEnv = do
else do else do
(p, f) <- runRequestBody (p, f) <- runRequestBody
let p' = Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) p let p' = Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) p
return $ Just (p', Map.fromList $ filter (notEmpty . snd) f) return $ Just (p', Map.fromList f)
where
notEmpty = not . L.null . fileContent
runFormPostNoToken :: (Html -> MForm sub master (FormResult a, xml)) -> GHandler sub master ((FormResult a, xml), Enctype) runFormPostNoToken :: (Html -> MForm sub master (FormResult a, xml)) -> GHandler sub master ((FormResult a, xml), Enctype)
runFormPostNoToken form = do runFormPostNoToken form = do
@ -246,7 +246,10 @@ getKey = "_hasdata"
getHelper :: (Html -> MForm sub master a) -> Maybe (Env, FileEnv) -> GHandler sub master (a, Enctype) getHelper :: (Html -> MForm sub master a) -> Maybe (Env, FileEnv) -> GHandler sub master (a, Enctype)
getHelper form env = do getHelper form env = do
let fragment = [shamlet|<input type=hidden name=#{getKey}>|] let fragment = [shamlet|
$newline never
<input type=hidden name=#{getKey}>
|]
langs <- languages langs <- languages
m <- getYesod m <- getYesod
runFormGeneric (form fragment) m langs env runFormGeneric (form fragment) m langs env
@ -262,6 +265,7 @@ renderTable aform fragment = do
let views = views' [] let views = views' []
-- FIXME non-valid HTML -- FIXME non-valid HTML
let widget = [whamlet| let widget = [whamlet|
$newline never
\#{fragment} \#{fragment}
$forall view <- views $forall view <- views
<tr :fvRequired view:.required :not $ fvRequired view:.optional> <tr :fvRequired view:.required :not $ fvRequired view:.optional>
@ -286,6 +290,7 @@ renderDivsMaybeLabels withLabels aform fragment = do
(res, views') <- aFormToForm aform (res, views') <- aFormToForm aform
let views = views' [] let views = views' []
let widget = [whamlet| let widget = [whamlet|
$newline never
\#{fragment} \#{fragment}
$forall view <- views $forall view <- views
<div :fvRequired view:.required :not $ fvRequired view:.optional> <div :fvRequired view:.required :not $ fvRequired view:.optional>
@ -321,6 +326,7 @@ renderBootstrap aform fragment = do
has (Just _) = True has (Just _) = True
has Nothing = False has Nothing = False
let widget = [whamlet| let widget = [whamlet|
$newline never
\#{fragment} \#{fragment}
$forall view <- views $forall view <- views
<div .control-group .clearfix :fvRequired view:.required :not $ fvRequired view:.optional :has $ fvErrors view:.error> <div .control-group .clearfix :fvRequired view:.required :not $ fvRequired view:.optional :has $ fvErrors view:.error>
@ -347,13 +353,21 @@ checkM :: RenderMessage master msg
=> (a -> GHandler sub master (Either msg a)) => (a -> GHandler sub master (Either msg a))
-> Field sub master a -> Field sub master a
-> Field sub master a -> Field sub master a
checkM f field = field checkM f = checkM' f id
checkM' :: RenderMessage master msg
=> (a -> GHandler sub master (Either msg b))
-> (b -> a)
-> Field sub master a
-> Field sub master b
checkM' f inv field = field
{ fieldParse = \ts -> do { fieldParse = \ts -> do
e1 <- fieldParse field ts e1 <- fieldParse field ts
case e1 of case e1 of
Left msg -> return $ Left msg Left msg -> return $ Left msg
Right Nothing -> return $ Right Nothing Right Nothing -> return $ Right Nothing
Right (Just a) -> fmap (either (Left . SomeMessage) (Right . Just)) $ f a Right (Just a) -> fmap (either (Left . SomeMessage) (Right . Just)) $ f a
, fieldView = \i n a eres req -> fieldView field i n a (fmap inv eres) req
} }
-- | Allows you to overwrite the error message on parse error. -- | Allows you to overwrite the error message on parse error.

View File

@ -70,6 +70,7 @@ jqueryDayField jds = Field
. unpack . unpack
, fieldView = \theId name attrs val isReq -> do , fieldView = \theId name attrs val isReq -> do
toWidget [shamlet| toWidget [shamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="date" :isReq:required="" value="#{showVal val}"> <input id="#{theId}" name="#{name}" *{attrs} type="date" :isReq:required="" value="#{showVal val}">
|] |]
addScript' urlJqueryJs addScript' urlJqueryJs
@ -109,6 +110,7 @@ jqueryAutocompleteField src = Field
{ fieldParse = blank $ Right { fieldParse = blank $ Right
, fieldView = \theId name attrs val isReq -> do , fieldView = \theId name attrs val isReq -> do
toWidget [shamlet| toWidget [shamlet|
$newline never
<input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required="" value="#{either id id val}" .autocomplete> <input id="#{theId}" name="#{name}" *{attrs} type="text" :isReq:required="" value="#{either id id val}" .autocomplete>
|] |]
addScript' urlJqueryJs addScript' urlJqueryJs

View File

@ -80,6 +80,7 @@ inputList label fixXml single mdef = formToAForm $ do
, fvTooltip = Nothing , fvTooltip = Nothing
, fvId = theId , fvId = theId
, fvInput = [whamlet| , fvInput = [whamlet|
$newline never
^{fixXml views} ^{fixXml views}
<p> <p>
$forall xml <- xmls $forall xml <- xmls
@ -100,7 +101,10 @@ withDelete af = do
deleteName <- newFormIdent deleteName <- newFormIdent
(menv, _, _) <- ask (menv, _, _) <- ask
res <- case menv >>= Map.lookup deleteName . fst of res <- case menv >>= Map.lookup deleteName . fst of
Just ("yes":_) -> return $ Left [whamlet|<input type=hidden name=#{deleteName} value=yes>|] Just ("yes":_) -> return $ Left [whamlet|
$newline never
<input type=hidden name=#{deleteName} value=yes>
|]
_ -> do _ -> do
(_, xml2) <- aFormToForm $ areq boolField FieldSettings (_, xml2) <- aFormToForm $ areq boolField FieldSettings
{ fsLabel = SomeMessage MsgDelete { fsLabel = SomeMessage MsgDelete
@ -127,6 +131,7 @@ massDivs, massTable
:: [[FieldView sub master]] :: [[FieldView sub master]]
-> GWidget sub master () -> GWidget sub master ()
massDivs viewss = [whamlet| massDivs viewss = [whamlet|
$newline never
$forall views <- viewss $forall views <- viewss
<fieldset> <fieldset>
$forall view <- views $forall view <- views
@ -140,6 +145,7 @@ $forall views <- viewss
|] |]
massTable viewss = [whamlet| massTable viewss = [whamlet|
$newline never
$forall views <- viewss $forall views <- viewss
<fieldset> <fieldset>
<table> <table>

View File

@ -38,6 +38,7 @@ nicHtmlField = Field
{ fieldParse = return . Right . fmap (preEscapedText . sanitizeBalance) . listToMaybe { fieldParse = return . Right . fmap (preEscapedText . sanitizeBalance) . listToMaybe
, fieldView = \theId name attrs val _isReq -> do , fieldView = \theId name attrs val _isReq -> do
toWidget [shamlet| toWidget [shamlet|
$newline never
<textarea id="#{theId}" *{attrs} name="#{name}" .html>#{showVal val} <textarea id="#{theId}" *{attrs} name="#{name}" .html>#{showVal val}
|] |]
addScript' urlNicEdit addScript' urlNicEdit

View File

@ -1,5 +1,5 @@
name: yesod-form name: yesod-form
version: 1.0.0.4 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -12,19 +12,15 @@ build-type: Simple
homepage: http://www.yesodweb.com/ homepage: http://www.yesodweb.com/
description: Form handling support for Yesod Web Framework description: Form handling support for Yesod Web Framework
flag blaze_html_0_5
description: use blaze-html 0.5 and blaze-markup 0.5
default: True
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, yesod-persistent >= 1.0 && < 1.1 , yesod-persistent >= 1.1 && < 1.2
, time >= 1.1.4 , time >= 1.1.4
, hamlet >= 1.0 && < 1.1 , hamlet >= 1.1 && < 1.2
, shakespeare-css >= 1.0 && < 1.1 , shakespeare-css >= 1.0 && < 1.1
, shakespeare-js >= 1.0 && < 1.1 , shakespeare-js >= 1.0 && < 1.1
, persistent >= 0.9 && < 0.10 , persistent >= 1.0 && < 1.1
, template-haskell , template-haskell
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, data-default , data-default
@ -34,16 +30,10 @@ library
, email-validate >= 0.2.6 && < 0.3 , email-validate >= 0.2.6 && < 0.3
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, text >= 0.9 && < 1.0 , text >= 0.9 && < 1.0
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, containers >= 0.2 , containers >= 0.2
, blaze-html >= 0.5 && < 0.6
if flag(blaze_html_0_5) , blaze-markup >= 0.5.1 && < 0.6
build-depends:
blaze-html >= 0.5 && < 0.6
, blaze-markup >= 0.5.1 && < 0.6
else
build-depends:
blaze-html >= 0.4 && < 0.5
exposed-modules: Yesod.Form exposed-modules: Yesod.Form
Yesod.Form.Class Yesod.Form.Class

View File

@ -1,5 +1,5 @@
name: yesod-json name: yesod-json
version: 1.0.1.0 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -14,19 +14,19 @@ description: Generate content for Yesod using the aeson package.
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, yesod-routes >= 1.0 && < 1.1 , yesod-routes >= 1.1 && < 1.2
, aeson >= 0.5 , aeson >= 0.5
, text >= 0.8 && < 1.0 , text >= 0.8 && < 1.0
, shakespeare-js >= 1.0 && < 1.1 , shakespeare-js >= 1.0 && < 1.1
, vector >= 0.9 , vector >= 0.9
, containers >= 0.2 , containers >= 0.2
, blaze-builder , blaze-builder
, attoparsec-conduit >= 0.4 && < 0.5 , attoparsec-conduit >= 0.5 && < 0.6
, conduit >= 0.4 && < 0.5 , conduit >= 0.5 && < 0.6
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, wai-extra >= 1.2 && < 1.3 , wai-extra >= 1.3 && < 1.4
, bytestring >= 0.9 , bytestring >= 0.9
, safe >= 0.2 && < 0.4 , safe >= 0.2 && < 0.4
exposed-modules: Yesod.Json exposed-modules: Yesod.Json

View File

@ -31,11 +31,8 @@ import qualified Data.ByteString.Char8 as S8
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Lazy (toStrict) import Data.Text.Lazy (toStrict)
import Text.XML import Text.XML
#if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze.Html.Renderer.Text (renderHtml) import Text.Blaze.Html.Renderer.Text (renderHtml)
#else import qualified Data.Map as Map
import Text.Blaze.Renderer.Text (renderHtml)
#endif
newtype RepAtom = RepAtom Content newtype RepAtom = RepAtom Content
instance HasReps RepAtom where instance HasReps RepAtom where
@ -55,21 +52,22 @@ template Feed {..} render =
addNS' n = n addNS' n = n
namespace = "http://www.w3.org/2005/Atom" namespace = "http://www.w3.org/2005/Atom"
root = Element "feed" [] $ map NodeElement root = Element "feed" Map.empty $ map NodeElement
$ Element "title" [] [NodeContent feedTitle] $ Element "title" Map.empty [NodeContent feedTitle]
: Element "link" [("rel", "self"), ("href", render feedLinkSelf)] [] : Element "link" (Map.fromList [("rel", "self"), ("href", render feedLinkSelf)]) []
: Element "link" [("href", render feedLinkHome)] [] : Element "link" (Map.singleton "href" $ render feedLinkHome) []
: Element "updated" [] [NodeContent $ formatW3 feedUpdated] : Element "updated" Map.empty [NodeContent $ formatW3 feedUpdated]
: Element "id" [] [NodeContent $ render feedLinkHome] : Element "id" Map.empty [NodeContent $ render feedLinkHome]
: Element "author" Map.empty [NodeContent feedAuthor]
: map (flip entryTemplate render) feedEntries : map (flip entryTemplate render) feedEntries
entryTemplate :: FeedEntry url -> (url -> Text) -> Element entryTemplate :: FeedEntry url -> (url -> Text) -> Element
entryTemplate FeedEntry {..} render = Element "entry" [] $ map NodeElement entryTemplate FeedEntry {..} render = Element "entry" Map.empty $ map NodeElement
[ Element "id" [] [NodeContent $ render feedEntryLink] [ Element "id" Map.empty [NodeContent $ render feedEntryLink]
, Element "link" [("href", render feedEntryLink)] [] , Element "link" (Map.singleton "href" $ render feedEntryLink) []
, Element "updated" [] [NodeContent $ formatW3 feedEntryUpdated] , Element "updated" Map.empty [NodeContent $ formatW3 feedEntryUpdated]
, Element "title" [] [NodeContent feedEntryTitle] , Element "title" Map.empty [NodeContent feedEntryTitle]
, Element "content" [("type", "html")] [NodeContent $ toStrict $ renderHtml feedEntryContent] , Element "content" (Map.singleton "type" "html") [NodeContent $ toStrict $ renderHtml feedEntryContent]
] ]
-- | Generates a link tag in the head of a widget. -- | Generates a link tag in the head of a widget.
@ -77,5 +75,6 @@ atomLink :: Route m
-> Text -- ^ title -> Text -- ^ title
-> GWidget s m () -> GWidget s m ()
atomLink r title = toWidgetHead [hamlet| atomLink r title = toWidgetHead [hamlet|
$newline never
<link href=@{r} type=#{S8.unpack typeAtom} rel="alternate" title=#{title}> <link href=@{r} type=#{S8.unpack typeAtom} rel="alternate" title=#{title}>
|] |]

View File

@ -12,6 +12,7 @@ data Feed url = Feed
{ feedTitle :: Text { feedTitle :: Text
, feedLinkSelf :: url , feedLinkSelf :: url
, feedLinkHome :: url , feedLinkHome :: url
, feedAuthor :: Text
-- | note: currently only used for Rss -- | note: currently only used for Rss

View File

@ -27,11 +27,8 @@ import qualified Data.ByteString.Char8 as S8
import Data.Text (Text, pack) import Data.Text (Text, pack)
import Data.Text.Lazy (toStrict) import Data.Text.Lazy (toStrict)
import Text.XML import Text.XML
#if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze.Html.Renderer.Text (renderHtml) import Text.Blaze.Html.Renderer.Text (renderHtml)
#else import qualified Data.Map as Map
import Text.Blaze.Renderer.Text (renderHtml)
#endif
newtype RepRss = RepRss Content newtype RepRss = RepRss Content
instance HasReps RepRss where instance HasReps RepRss where
@ -47,26 +44,26 @@ template :: Feed url -> (url -> Text) -> Document
template Feed {..} render = template Feed {..} render =
Document (Prologue [] Nothing []) root [] Document (Prologue [] Nothing []) root []
where where
root = Element "rss" [("version", "2.0")] $ return $ NodeElement $ Element "channel" [] $ map NodeElement root = Element "rss" (Map.singleton "version" "2.0") $ return $ NodeElement $ Element "channel" Map.empty $ map NodeElement
$ Element "{http://www.w3.org/2005/Atom}link" $ Element "{http://www.w3.org/2005/Atom}link" (Map.fromList
[ ("href", render feedLinkSelf) [ ("href", render feedLinkSelf)
, ("rel", "self") , ("rel", "self")
, ("type", pack $ S8.unpack typeRss) , ("type", pack $ S8.unpack typeRss)
] [] ]) []
: Element "title" [] [NodeContent feedTitle] : Element "title" Map.empty [NodeContent feedTitle]
: Element "link" [] [NodeContent $ render feedLinkHome] : Element "link" Map.empty [NodeContent $ render feedLinkHome]
: Element "description" [] [NodeContent $ toStrict $ renderHtml feedDescription] : Element "description" Map.empty [NodeContent $ toStrict $ renderHtml feedDescription]
: Element "lastBuildDate" [] [NodeContent $ formatRFC822 feedUpdated] : Element "lastBuildDate" Map.empty [NodeContent $ formatRFC822 feedUpdated]
: Element "language" [] [NodeContent feedLanguage] : Element "language" Map.empty [NodeContent feedLanguage]
: map (flip entryTemplate render) feedEntries : map (flip entryTemplate render) feedEntries
entryTemplate :: FeedEntry url -> (url -> Text) -> Element entryTemplate :: FeedEntry url -> (url -> Text) -> Element
entryTemplate FeedEntry {..} render = Element "item" [] $ map NodeElement entryTemplate FeedEntry {..} render = Element "item" Map.empty $ map NodeElement
[ Element "title" [] [NodeContent feedEntryTitle] [ Element "title" Map.empty [NodeContent feedEntryTitle]
, Element "link" [] [NodeContent $ render feedEntryLink] , Element "link" Map.empty [NodeContent $ render feedEntryLink]
, Element "guid" [] [NodeContent $ render feedEntryLink] , Element "guid" Map.empty [NodeContent $ render feedEntryLink]
, Element "pubDate" [] [NodeContent $ formatRFC822 feedEntryUpdated] , Element "pubDate" Map.empty [NodeContent $ formatRFC822 feedEntryUpdated]
, Element "description" [] [NodeContent $ toStrict $ renderHtml feedEntryContent] , Element "description" Map.empty [NodeContent $ toStrict $ renderHtml feedEntryContent]
] ]
-- | Generates a link tag in the head of a widget. -- | Generates a link tag in the head of a widget.
@ -74,5 +71,6 @@ rssLink :: Route m
-> Text -- ^ title -> Text -- ^ title
-> GWidget s m () -> GWidget s m ()
rssLink r title = toWidgetHead [hamlet| rssLink r title = toWidgetHead [hamlet|
$newline never
<link href=@{r} type=#{S8.unpack typeRss} rel="alternate" title=#{title}> <link href=@{r} type=#{S8.unpack typeRss} rel="alternate" title=#{title}>
|] |]

View File

@ -1,5 +1,5 @@
name: yesod-newsfeed name: yesod-newsfeed
version: 1.0.0.2 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman, Patrick Brisbin author: Michael Snoyman, Patrick Brisbin
@ -12,26 +12,17 @@ build-type: Simple
homepage: http://www.yesodweb.com/ homepage: http://www.yesodweb.com/
description: Helper functions and data types for producing News feeds. description: Helper functions and data types for producing News feeds.
flag blaze_html_0_5
description: use blaze-html 0.5 and blaze-markup 0.5
default: True
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, time >= 1.1.4 , time >= 1.1.4
, hamlet >= 1.0 && < 1.1 , hamlet >= 1.1 && < 1.2
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, text >= 0.9 && < 0.12 , text >= 0.9 && < 0.12
, xml-conduit >= 0.7 && < 0.8 , xml-conduit >= 1.0 && < 1.1
, blaze-html >= 0.5 && < 0.6
if flag(blaze_html_0_5) , blaze-markup >= 0.5.1 && < 0.6
build-depends: , containers
blaze-html >= 0.5 && < 0.6
, blaze-markup >= 0.5.1 && < 0.6
else
build-depends:
blaze-html >= 0.4 && < 0.5
exposed-modules: Yesod.AtomFeed exposed-modules: Yesod.AtomFeed
, Yesod.RssFeed , Yesod.RssFeed

View File

@ -1,5 +1,5 @@
name: yesod-persistent name: yesod-persistent
version: 1.0.0.1 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -14,9 +14,9 @@ description: Some helpers for using Persistent from Yesod.
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, persistent >= 0.9 && < 0.10 , persistent >= 1.0 && < 1.1
, persistent-template >= 0.9 && < 0.10 , persistent-template >= 1.0 && < 1.1
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
exposed-modules: Yesod.Persist exposed-modules: Yesod.Persist
ghc-options: -Wall ghc-options: -Wall

View File

@ -2,27 +2,41 @@
module Yesod.Routes.Overlap module Yesod.Routes.Overlap
( findOverlaps ( findOverlaps
, findOverlapNames , findOverlapNames
, Overlap (..)
) where ) where
import Yesod.Routes.TH.Types import Yesod.Routes.TH.Types
import Control.Arrow ((***)) import Data.List (intercalate)
import Data.Maybe (mapMaybe)
findOverlaps :: [Resource t] -> [(Resource t, Resource t)] data Overlap t = Overlap
findOverlaps [] = [] { overlapParents :: [String] -> [String] -- ^ parent resource trees
findOverlaps (x:xs) = mapMaybe (findOverlap x) xs ++ findOverlaps xs , overlap1 :: ResourceTree t
, overlap2 :: ResourceTree t
}
findOverlap :: Resource t -> Resource t -> Maybe (Resource t, Resource t) findOverlaps :: ([String] -> [String]) -> [ResourceTree t] -> [Overlap t]
findOverlap x y findOverlaps _ [] = []
| overlaps (resourcePieces x) (resourcePieces y) (hasSuffix x) (hasSuffix y) = Just (x, y) findOverlaps front (x:xs) = concatMap (findOverlap front x) xs ++ findOverlaps front xs
| otherwise = Nothing
hasSuffix :: Resource t -> Bool findOverlap :: ([String] -> [String]) -> ResourceTree t -> ResourceTree t -> [Overlap t]
hasSuffix r = findOverlap front x y =
here rest
where
here
| overlaps (resourceTreePieces x) (resourceTreePieces y) (hasSuffix x) (hasSuffix y) = (Overlap front x y:)
| otherwise = id
rest =
case x of
ResourceParent name _ children -> findOverlaps (front . (name:)) children
ResourceLeaf{} -> []
hasSuffix :: ResourceTree t -> Bool
hasSuffix (ResourceLeaf r) =
case resourceDispatch r of case resourceDispatch r of
Subsite{} -> True Subsite{} -> True
Methods Just{} _ -> True Methods Just{} _ -> True
Methods Nothing _ -> False Methods Nothing _ -> False
hasSuffix ResourceParent{} = True
overlaps :: [(CheckOverlap, Piece t)] -> [(CheckOverlap, Piece t)] -> Bool -> Bool -> Bool overlaps :: [(CheckOverlap, Piece t)] -> [(CheckOverlap, Piece t)] -> Bool -> Bool -> Bool
@ -50,9 +64,14 @@ piecesOverlap :: Piece t -> Piece t -> Bool
piecesOverlap (Static x) (Static y) = x == y piecesOverlap (Static x) (Static y) = x == y
piecesOverlap _ _ = True piecesOverlap _ _ = True
findOverlapNames :: [Resource t] -> [(String, String)] findOverlapNames :: [ResourceTree t] -> [(String, String)]
findOverlapNames = map (resourceName *** resourceName) . findOverlaps findOverlapNames =
map go . findOverlaps id
where
go (Overlap front x y) =
(go' $ resourceTreeName x, go' $ resourceTreeName y)
where
go' = intercalate "/" . front . return
{- {-
-- n^2, should be a way to speed it up -- n^2, should be a way to speed it up
findOverlaps :: [Resource a] -> [[Resource a]] findOverlaps :: [Resource a] -> [[Resource a]]

View File

@ -10,7 +10,6 @@ module Yesod.Routes.Parse
) where ) where
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import Data.Maybe
import Data.Char (isUpper) import Data.Char (isUpper)
import Language.Haskell.TH.Quote import Language.Haskell.TH.Quote
import qualified System.IO as SIO import qualified System.IO as SIO
@ -55,18 +54,29 @@ parseRoutesNoCheck = QuasiQuoter
-- | Convert a multi-line string to a set of resources. See documentation for -- | Convert a multi-line string to a set of resources. See documentation for
-- the format of this string. This is a partial function which calls 'error' on -- the format of this string. This is a partial function which calls 'error' on
-- invalid input. -- invalid input.
resourcesFromString :: String -> [Resource String] resourcesFromString :: String -> [ResourceTree String]
resourcesFromString = resourcesFromString =
mapMaybe go . lines fst . parse 0 . lines
where where
go s = parse _ [] = ([], [])
case takeWhile (/= "--") $ words s of parse indent (thisLine:otherLines)
(pattern:constr:rest) -> | length spaces < indent = ([], thisLine : otherLines)
let (pieces, mmulti) = piecesFromString $ drop1Slash pattern | otherwise = (this others, remainder)
disp = dispatchFromString rest mmulti where
in Just $ Resource constr pieces disp spaces = takeWhile (== ' ') thisLine
[] -> Nothing (others, remainder) = parse indent otherLines'
_ -> error $ "Invalid resource line: " ++ s (this, otherLines') =
case takeWhile (/= "--") $ words thisLine of
[pattern, constr] | last constr == ':' ->
let (children, otherLines'') = parse (length spaces + 1) otherLines
(pieces, Nothing) = piecesFromString $ drop1Slash pattern
in ((ResourceParent (init constr) pieces children :), otherLines'')
(pattern:constr:rest) ->
let (pieces, mmulti) = piecesFromString $ drop1Slash pattern
disp = dispatchFromString rest mmulti
in ((ResourceLeaf (Resource constr pieces disp):), otherLines)
[] -> (id, otherLines)
_ -> error $ "Invalid resource line: " ++ thisLine
dispatchFromString :: [String] -> Maybe String -> Dispatch String dispatchFromString :: [String] -> Maybe String -> Dispatch String
dispatchFromString rest mmulti dispatchFromString rest mmulti

View File

@ -17,6 +17,16 @@ import Web.PathPieces (PathPiece (..), PathMultiPiece (..))
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))
import Data.List (foldl') import Data.List (foldl')
data FlatResource a = FlatResource [(String, [(CheckOverlap, Piece a)])] String [(CheckOverlap, Piece a)] (Dispatch a)
flatten :: [ResourceTree a] -> [FlatResource a]
flatten =
concatMap (go id)
where
go front (ResourceLeaf (Resource a b c)) = [FlatResource (front []) a b c]
go front (ResourceParent name pieces children) =
concatMap (go (front . ((name, pieces):))) children
-- | -- |
-- --
-- This function will generate a single clause that will address all -- This function will generate a single clause that will address all
@ -83,9 +93,9 @@ import Data.List (foldl')
mkDispatchClause :: Q Exp -- ^ runHandler function mkDispatchClause :: Q Exp -- ^ runHandler function
-> Q Exp -- ^ dispatcher function -> Q Exp -- ^ dispatcher function
-> Q Exp -- ^ fixHandler function -> Q Exp -- ^ fixHandler function
-> [Resource a] -> [ResourceTree a]
-> Q Clause -> Q Clause
mkDispatchClause runHandler dispatcher fixHandler ress = do mkDispatchClause runHandler dispatcher fixHandler ress' = do
-- Allocate the names to be used. Start off with the names passed to the -- Allocate the names to be used. Start off with the names passed to the
-- function itself (with a 0 suffix). -- function itself (with a 0 suffix).
-- --
@ -130,22 +140,25 @@ mkDispatchClause runHandler dispatcher fixHandler ress = do
Nothing -> $(return $ VarE app4040) Nothing -> $(return $ VarE app4040)
|] |]
return $ Clause pats (NormalB u) $ dispatchFun : methodMaps return $ Clause pats (NormalB u) $ dispatchFun : methodMaps
where
ress = flatten ress'
-- | Determine the name of the method map for a given resource name. -- | Determine the name of the method map for a given resource name.
methodMapName :: String -> Name methodMapName :: String -> Name
methodMapName s = mkName $ "methods" ++ s methodMapName s = mkName $ "methods" ++ s
buildMethodMap :: Q Exp -- ^ fixHandler buildMethodMap :: Q Exp -- ^ fixHandler
-> Resource a -> FlatResource a
-> Q (Maybe Dec) -> Q (Maybe Dec)
buildMethodMap _ (Resource _ _ (Methods _ [])) = return Nothing -- single handle function buildMethodMap _ (FlatResource _ _ _ (Methods _ [])) = return Nothing -- single handle function
buildMethodMap fixHandler (Resource name pieces (Methods mmulti methods)) = do buildMethodMap fixHandler (FlatResource parents name pieces' (Methods mmulti methods)) = do
fromList <- [|Map.fromList|] fromList <- [|Map.fromList|]
methods' <- mapM go methods methods' <- mapM go methods
let exp = fromList `AppE` ListE methods' let exp = fromList `AppE` ListE methods'
let fun = FunD (methodMapName name) [Clause [] (NormalB exp) []] let fun = FunD (methodMapName name) [Clause [] (NormalB exp) []]
return $ Just fun return $ Just fun
where where
pieces = concat $ map snd parents ++ [pieces']
go method = do go method = do
fh <- fixHandler fh <- fixHandler
let func = VarE $ mkName $ map toLower method ++ name let func = VarE $ mkName $ map toLower method ++ name
@ -156,28 +169,31 @@ buildMethodMap fixHandler (Resource name pieces (Methods mmulti methods)) = do
xs <- replicateM argCount $ newName "arg" xs <- replicateM argCount $ newName "arg"
let rhs = LamE (map VarP xs) $ fh `AppE` (foldl' AppE func $ map VarE xs) let rhs = LamE (map VarP xs) $ fh `AppE` (foldl' AppE func $ map VarE xs)
return $ TupE [pack' `AppE` LitE (StringL method), rhs] return $ TupE [pack' `AppE` LitE (StringL method), rhs]
buildMethodMap _ (Resource _ _ Subsite{}) = return Nothing buildMethodMap _ (FlatResource _ _ _ Subsite{}) = return Nothing
-- | Build a single 'D.Route' expression. -- | Build a single 'D.Route' expression.
buildRoute :: Q Exp -> Q Exp -> Q Exp -> Resource a -> Q Exp buildRoute :: Q Exp -> Q Exp -> Q Exp -> FlatResource a -> Q Exp
buildRoute runHandler dispatcher fixHandler (Resource name resPieces resDisp) = do buildRoute runHandler dispatcher fixHandler (FlatResource parents name resPieces resDisp) = do
-- First two arguments to D.Route -- First two arguments to D.Route
routePieces <- ListE <$> mapM (convertPiece . snd) resPieces routePieces <- ListE <$> mapM (convertPiece . snd) allPieces
isMulti <- isMulti <-
case resDisp of case resDisp of
Methods Nothing _ -> [|False|] Methods Nothing _ -> [|False|]
_ -> [|True|] _ -> [|True|]
[|D.Route $(return routePieces) $(return isMulti) $(routeArg3 runHandler dispatcher fixHandler name (map snd resPieces) resDisp)|] [|D.Route $(return routePieces) $(return isMulti) $(routeArg3 runHandler dispatcher fixHandler parents name (map snd allPieces) resDisp)|]
where
allPieces = concat $ map snd parents ++ [resPieces]
routeArg3 :: Q Exp -- ^ runHandler routeArg3 :: Q Exp -- ^ runHandler
-> Q Exp -- ^ dispatcher -> Q Exp -- ^ dispatcher
-> Q Exp -- ^ fixHandler -> Q Exp -- ^ fixHandler
-> [(String, [(CheckOverlap, Piece a)])]
-> String -- ^ name of resource -> String -- ^ name of resource
-> [Piece a] -> [Piece a]
-> Dispatch a -> Dispatch a
-> Q Exp -> Q Exp
routeArg3 runHandler dispatcher fixHandler name resPieces resDisp = do routeArg3 runHandler dispatcher fixHandler parents name resPieces resDisp = do
pieces <- newName "pieces" pieces <- newName "pieces"
-- Allocate input piece variables (xs) and variables that have been -- Allocate input piece variables (xs) and variables that have been
@ -216,7 +232,7 @@ routeArg3 runHandler dispatcher fixHandler name resPieces resDisp = do
_ -> return ([], []) _ -> return ([], [])
-- The final expression that actually uses the values we've computed -- The final expression that actually uses the values we've computed
caller <- buildCaller runHandler dispatcher fixHandler xrest name resDisp $ map snd ys ++ yrest' caller <- buildCaller runHandler dispatcher fixHandler xrest parents name resDisp $ map snd ys ++ yrest'
-- Put together all the statements -- Put together all the statements
just <- [|Just|] just <- [|Just|]
@ -239,11 +255,12 @@ buildCaller :: Q Exp -- ^ runHandler
-> Q Exp -- ^ dispatcher -> Q Exp -- ^ dispatcher
-> Q Exp -- ^ fixHandler -> Q Exp -- ^ fixHandler
-> Name -- ^ xrest -> Name -- ^ xrest
-> [(String, [(CheckOverlap, Piece a)])]
-> String -- ^ name of resource -> String -- ^ name of resource
-> Dispatch a -> Dispatch a
-> [Name] -- ^ ys -> [Name] -- ^ ys
-> Q Exp -> Q Exp
buildCaller runHandler dispatcher fixHandler xrest name resDisp ys = do buildCaller runHandler dispatcher fixHandler xrest parents name resDisp ys = do
master <- newName "master" master <- newName "master"
sub <- newName "sub" sub <- newName "sub"
toMaster <- newName "toMaster" toMaster <- newName "toMaster"
@ -254,7 +271,7 @@ buildCaller runHandler dispatcher fixHandler xrest name resDisp ys = do
let pat = map VarP [master, sub, toMaster, app404, handler405, method] let pat = map VarP [master, sub, toMaster, app404, handler405, method]
-- Create the route -- Create the route
let route = foldl' (\a b -> a `AppE` VarE b) (ConE $ mkName name) ys let route = routeFromDynamics parents name ys
exp <- exp <-
case resDisp of case resDisp of
@ -309,3 +326,16 @@ buildCaller runHandler dispatcher fixHandler xrest name resDisp ys = do
convertPiece :: Piece a -> Q Exp convertPiece :: Piece a -> Q Exp
convertPiece (Static s) = [|D.Static (pack $(lift s))|] convertPiece (Static s) = [|D.Static (pack $(lift s))|]
convertPiece (Dynamic _) = [|D.Dynamic|] convertPiece (Dynamic _) = [|D.Dynamic|]
routeFromDynamics :: [(String, [(CheckOverlap, Piece a)])] -- ^ parents
-> String -- ^ constructor name
-> [Name]
-> Exp
routeFromDynamics [] name ys = foldl' (\a b -> a `AppE` VarE b) (ConE $ mkName name) ys
routeFromDynamics ((parent, pieces):rest) name ys =
foldl' (\a b -> a `AppE` b) (ConE $ mkName parent) here
where
(here', ys') = splitAt (length $ filter (isDynamic . snd) pieces) ys
isDynamic Dynamic{} = True
isDynamic _ = False
here = map VarE here' ++ [routeFromDynamics rest name ys']

View File

@ -14,17 +14,19 @@ import Control.Monad (replicateM)
import Data.Text (pack) import Data.Text (pack)
import Web.PathPieces (PathPiece (..), PathMultiPiece (..)) import Web.PathPieces (PathPiece (..), PathMultiPiece (..))
import Yesod.Routes.Class import Yesod.Routes.Class
import Data.Monoid (mconcat)
-- | Generate the constructors of a route data type. -- | Generate the constructors of a route data type.
mkRouteCons :: [Resource Type] -> [Con] mkRouteCons :: [ResourceTree Type] -> ([Con], [Dec])
mkRouteCons = mkRouteCons =
map mkRouteCon mconcat . map mkRouteCon
where where
mkRouteCon res = mkRouteCon (ResourceLeaf res) =
NormalC (mkName $ resourceName res) ([con], [])
where
con = NormalC (mkName $ resourceName res)
$ map (\x -> (NotStrict, x)) $ map (\x -> (NotStrict, x))
$ concat [singles, multi, sub] $ concat [singles, multi, sub]
where
singles = concatMap (toSingle . snd) $ resourcePieces res singles = concatMap (toSingle . snd) $ resourcePieces res
toSingle Static{} = [] toSingle Static{} = []
toSingle (Dynamic typ) = [typ] toSingle (Dynamic typ) = [typ]
@ -35,16 +37,53 @@ mkRouteCons =
case resourceDispatch res of case resourceDispatch res of
Subsite { subsiteType = typ } -> [ConT ''Route `AppT` typ] Subsite { subsiteType = typ } -> [ConT ''Route `AppT` typ]
_ -> [] _ -> []
mkRouteCon (ResourceParent name pieces children) =
([con], dec : decs)
where
(cons, decs) = mkRouteCons children
con = NormalC (mkName name)
$ map (\x -> (NotStrict, x))
$ concat [singles, [ConT $ mkName name]]
dec = DataD [] (mkName name) [] cons [''Show, ''Read, ''Eq]
singles = concatMap (toSingle . snd) pieces
toSingle Static{} = []
toSingle (Dynamic typ) = [typ]
-- | Clauses for the 'renderRoute' method. -- | Clauses for the 'renderRoute' method.
mkRenderRouteClauses :: [Resource Type] -> Q [Clause] mkRenderRouteClauses :: [ResourceTree Type] -> Q [Clause]
mkRenderRouteClauses = mkRenderRouteClauses =
mapM go mapM go
where where
isDynamic Dynamic{} = True isDynamic Dynamic{} = True
isDynamic _ = False isDynamic _ = False
go res = do go (ResourceParent name pieces children) = do
let cnt = length $ filter (isDynamic . snd) pieces
dyns <- replicateM cnt $ newName "dyn"
child <- newName "child"
let pat = ConP (mkName name) $ map VarP $ dyns ++ [child]
pack' <- [|pack|]
tsp <- [|toPathPiece|]
let piecesSingle = mkPieces (AppE pack' . LitE . StringL) tsp (map snd pieces) dyns
childRender <- newName "childRender"
let rr = VarE childRender
childClauses <- mkRenderRouteClauses children
a <- newName "a"
b <- newName "b"
colon <- [|(:)|]
let cons y ys = InfixE (Just y) colon (Just ys)
let pieces' = foldr cons (VarE a) piecesSingle
let body = LamE [TupP [VarP a, VarP b]] (TupE [pieces', VarE b]) `AppE` (rr `AppE` VarE child)
return $ Clause [pat] (NormalB body) [FunD childRender childClauses]
go (ResourceLeaf res) = do
let cnt = length (filter (isDynamic . snd) $ resourcePieces res) + maybe 0 (const 1) (resourceMulti res) let cnt = length (filter (isDynamic . snd) $ resourcePieces res) + maybe 0 (const 1) (resourceMulti res)
dyns <- replicateM cnt $ newName "dyn" dyns <- replicateM cnt $ newName "dyn"
sub <- sub <-
@ -93,18 +132,19 @@ mkRenderRouteClauses =
-- This includes both the 'Route' associated type and the -- This includes both the 'Route' associated type and the
-- 'renderRoute' method. This function uses both 'mkRouteCons' and -- 'renderRoute' method. This function uses both 'mkRouteCons' and
-- 'mkRenderRouteClasses'. -- 'mkRenderRouteClasses'.
mkRenderRouteInstance :: Type -> [Resource Type] -> Q Dec mkRenderRouteInstance :: Type -> [ResourceTree Type] -> Q [Dec]
mkRenderRouteInstance = mkRenderRouteInstance' [] mkRenderRouteInstance = mkRenderRouteInstance' []
-- | A more general version of 'mkRenderRouteInstance' which takes an -- | A more general version of 'mkRenderRouteInstance' which takes an
-- additional context. -- additional context.
mkRenderRouteInstance' :: Cxt -> Type -> [Resource Type] -> Q Dec mkRenderRouteInstance' :: Cxt -> Type -> [ResourceTree Type] -> Q [Dec]
mkRenderRouteInstance' cxt typ ress = do mkRenderRouteInstance' cxt typ ress = do
cls <- mkRenderRouteClauses ress cls <- mkRenderRouteClauses ress
let (cons, decs) = mkRouteCons ress
return $ InstanceD cxt (ConT ''RenderRoute `AppT` typ) return $ InstanceD cxt (ConT ''RenderRoute `AppT` typ)
[ DataInstD [] ''Route [typ] (mkRouteCons ress) clazzes [ DataInstD [] ''Route [typ] cons clazzes
, FunD (mkName "renderRoute") cls , FunD (mkName "renderRoute") cls
] ] : decs
where where
clazzes = [''Show, ''Eq, ''Read] clazzes = [''Show, ''Eq, ''Read]

View File

@ -2,16 +2,37 @@
module Yesod.Routes.TH.Types module Yesod.Routes.TH.Types
( -- * Data types ( -- * Data types
Resource (..) Resource (..)
, ResourceTree (..)
, Piece (..) , Piece (..)
, Dispatch (..) , Dispatch (..)
, CheckOverlap , CheckOverlap
-- ** Helper functions -- ** Helper functions
, resourceMulti , resourceMulti
, resourceTreePieces
, resourceTreeName
) where ) where
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import Control.Arrow (second) import Control.Arrow (second)
data ResourceTree typ = ResourceLeaf (Resource typ) | ResourceParent String [(CheckOverlap, Piece typ)] [ResourceTree typ]
resourceTreePieces :: ResourceTree typ -> [(CheckOverlap, Piece typ)]
resourceTreePieces (ResourceLeaf r) = resourcePieces r
resourceTreePieces (ResourceParent _ x _) = x
resourceTreeName :: ResourceTree typ -> String
resourceTreeName (ResourceLeaf r) = resourceName r
resourceTreeName (ResourceParent x _ _) = x
instance Functor ResourceTree where
fmap f (ResourceLeaf r) = ResourceLeaf (fmap f r)
fmap f (ResourceParent a b c) = ResourceParent a (map (second $ fmap f) b) $ map (fmap f) c
instance Lift t => Lift (ResourceTree t) where
lift (ResourceLeaf r) = [|ResourceLeaf $(lift r)|]
lift (ResourceParent a b c) = [|ResourceParent $(lift a) $(lift b) $(lift c)|]
data Resource typ = Resource data Resource typ = Resource
{ resourceName :: String { resourceName :: String
, resourcePieces :: [(CheckOverlap, Piece typ)] , resourcePieces :: [(CheckOverlap, Piece typ)]

View File

@ -0,0 +1,103 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
module Hierarchy
( hierarchy
, Dispatcher (..)
, RunHandler (..)
, Handler
, App
, toText
) where
import Test.Hspec.Monadic
import Test.Hspec.HUnit ()
import Test.HUnit
import Yesod.Routes.Parse
import Yesod.Routes.TH
import Yesod.Routes.Class
import Language.Haskell.TH.Syntax
import qualified Yesod.Routes.Class as YRC
import Data.Text (Text, pack, append)
class ToText a where
toText :: a -> Text
instance ToText Text where toText = id
instance ToText String where toText = pack
type Handler sub master = Text
type App sub master = (Text, Maybe (YRC.Route master))
class Dispatcher sub master where
dispatcher
:: master
-> sub
-> (YRC.Route sub -> YRC.Route master)
-> App sub master -- ^ 404 page
-> (YRC.Route sub -> App sub master) -- ^ 405 page
-> Text -- ^ method
-> [Text]
-> App sub master
class RunHandler sub master where
runHandler
:: Handler sub master
-> master
-> sub
-> Maybe (YRC.Route sub)
-> (YRC.Route sub -> YRC.Route master)
-> App sub master
data Hierarchy = Hierarchy
do
let resources = [parseRoutes|
/ HomeR GET
/admin/#Int AdminR:
/ AdminRootR GET
/login LoginR GET POST
/table/#Text TableR GET
|]
rrinst <- mkRenderRouteInstance (ConT ''Hierarchy) $ map (fmap parseType) resources
dispatch <- mkDispatchClause [|runHandler|] [|dispatcher|] [|toText|] resources
return
$ InstanceD
[]
(ConT ''Dispatcher
`AppT` ConT ''Hierarchy
`AppT` ConT ''Hierarchy)
[FunD (mkName "dispatcher") [dispatch]]
: rrinst
getHomeR :: Handler sub master
getHomeR = "home"
getAdminRootR :: Int -> Handler sub master
getAdminRootR i = pack $ "admin root: " ++ show i
getLoginR :: Int -> Handler sub master
getLoginR i = pack $ "login: " ++ show i
postLoginR :: Int -> Handler sub master
postLoginR i = pack $ "post login: " ++ show i
getTableR :: Int -> Text -> Handler sub master
getTableR _ t = append "TableR " t
instance RunHandler Hierarchy master where
runHandler h _ _ subRoute toMaster = (h, fmap toMaster subRoute)
hierarchy :: Specs
hierarchy = describe "hierarchy" $ do
it "renders root correctly" $
renderRoute (AdminR 5 AdminRootR) @?= (["admin", "5"], [])
it "renders table correctly" $
renderRoute (AdminR 6 $ TableR "foo") @?= (["admin", "6", "table", "foo"], [])
let disp m ps = dispatcher Hierarchy Hierarchy id (pack "404", Nothing) (\route -> (pack "405", Just route)) (pack m) (map pack ps)
it "dispatches root correctly" $ disp "GET" ["admin", "7"] @?= ("admin root: 7", Just $ AdminR 7 AdminRootR)
it "dispatches table correctly" $ disp "GET" ["admin", "8", "table", "bar"] @?= ("TableR bar", Just $ AdminR 8 $ TableR "bar")

View File

@ -20,12 +20,7 @@ import Yesod.Routes.Parse (parseRoutesNoCheck)
import Yesod.Routes.Overlap (findOverlapNames) import Yesod.Routes.Overlap (findOverlapNames)
import Yesod.Routes.TH hiding (Dispatch) import Yesod.Routes.TH hiding (Dispatch)
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import Hierarchy
class ToText a where
toText :: a -> Text
instance ToText Text where toText = id
instance ToText String where toText = pack
result :: ([Text] -> Maybe Int) -> Dispatch Int result :: ([Text] -> Maybe Int) -> Dispatch Int
result f ts = f ts result f ts = f ts
@ -101,32 +96,9 @@ instance RenderRoute MySubParam where
getMySubParam :: MyApp -> Int -> MySubParam getMySubParam :: MyApp -> Int -> MySubParam
getMySubParam _ = MySubParam getMySubParam _ = MySubParam
type Handler sub master = Text
type App sub master = (Text, Maybe (YRC.Route master))
class Dispatcher sub master where
dispatcher
:: master
-> sub
-> (YRC.Route sub -> YRC.Route master)
-> App sub master -- ^ 404 page
-> (YRC.Route sub -> App sub master) -- ^ 405 page
-> Text -- ^ method
-> [Text]
-> App sub master
class RunHandler sub master where
runHandler
:: Handler sub master
-> master
-> sub
-> Maybe (YRC.Route sub)
-> (YRC.Route sub -> YRC.Route master)
-> App sub master
do do
texts <- [t|[Text]|] texts <- [t|[Text]|]
let ress = let ress = map ResourceLeaf
[ Resource "RootR" [] $ Methods Nothing ["GET"] [ Resource "RootR" [] $ Methods Nothing ["GET"]
, Resource "BlogPostR" (addCheck [Static "blog", Dynamic $ ConT ''Text]) $ Methods Nothing ["GET", "POST"] , Resource "BlogPostR" (addCheck [Static "blog", Dynamic $ ConT ''Text]) $ Methods Nothing ["GET", "POST"]
, Resource "WikiR" (addCheck [Static "wiki"]) $ Methods (Just texts) [] , Resource "WikiR" (addCheck [Static "wiki"]) $ Methods (Just texts) []
@ -137,14 +109,13 @@ do
rrinst <- mkRenderRouteInstance (ConT ''MyApp) ress rrinst <- mkRenderRouteInstance (ConT ''MyApp) ress
dispatch <- mkDispatchClause [|runHandler|] [|dispatcher|] [|toText|] ress dispatch <- mkDispatchClause [|runHandler|] [|dispatcher|] [|toText|] ress
return return
[ rrinst $ InstanceD
, InstanceD
[] []
(ConT ''Dispatcher (ConT ''Dispatcher
`AppT` ConT ''MyApp `AppT` ConT ''MyApp
`AppT` ConT ''MyApp) `AppT` ConT ''MyApp)
[FunD (mkName "dispatcher") [dispatch]] [FunD (mkName "dispatcher") [dispatch]]
] : rrinst
instance RunHandler MyApp master where instance RunHandler MyApp master where
runHandler h _ _ subRoute toMaster = (h, fmap toMaster subRoute) runHandler h _ _ subRoute toMaster = (h, fmap toMaster subRoute)
@ -328,6 +299,7 @@ main = hspecX $ do
/bar/baz Foo3 /bar/baz Foo3
|] |]
findOverlapNames routes @?= [] findOverlapNames routes @?= []
hierarchy
getRootR :: Text getRootR :: Text
getRootR = pack "this is the root" getRootR = pack "this is the root"

View File

@ -1,5 +1,5 @@
name: yesod-routes name: yesod-routes
version: 1.0.1.2 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -36,12 +36,13 @@ test-suite runtests
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
main-is: main.hs main-is: main.hs
hs-source-dirs: test hs-source-dirs: test
other-modules: Hierarchy
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.3 && < 5
, yesod-routes , yesod-routes
, text >= 0.5 && < 0.12 , text >= 0.5 && < 0.12
, HUnit >= 1.2 && < 1.3 , HUnit >= 1.2 && < 1.3
, hspec >= 0.6 && < 1.2 , hspec >= 1.2 && < 1.3
, containers , containers
, template-haskell , template-haskell
, path-pieces , path-pieces

View File

@ -30,6 +30,7 @@ import Data.Time (UTCTime)
import Data.Monoid (mappend) import Data.Monoid (mappend)
import Text.XML import Text.XML
import Data.Text (Text, pack) import Data.Text (Text, pack)
import qualified Data.Map as Map
data SitemapChangeFreq = Always data SitemapChangeFreq = Always
| Hourly | Hourly
@ -66,13 +67,13 @@ template urls render =
addNS' n = n addNS' n = n
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9" namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
root = Element "urlset" [] $ map go urls root = Element "urlset" Map.empty $ map go urls
go SitemapUrl {..} = NodeElement $ Element "url" [] $ map NodeElement go SitemapUrl {..} = NodeElement $ Element "url" Map.empty $ map NodeElement
[ Element "loc" [] [NodeContent $ render sitemapLoc] [ Element "loc" Map.empty [NodeContent $ render sitemapLoc]
, Element "lastmod" [] [NodeContent $ formatW3 sitemapLastMod] , Element "lastmod" Map.empty [NodeContent $ formatW3 sitemapLastMod]
, Element "changefreq" [] [NodeContent $ showFreq sitemapChangeFreq] , Element "changefreq" Map.empty [NodeContent $ showFreq sitemapChangeFreq]
, Element "priority" [] [NodeContent $ pack $ show sitemapPriority] , Element "priority" Map.empty [NodeContent $ pack $ show sitemapPriority]
] ]
sitemap :: [SitemapUrl (Route master)] -> GHandler sub master RepXml sitemap :: [SitemapUrl (Route master)] -> GHandler sub master RepXml

View File

@ -1,5 +1,5 @@
name: yesod-sitemap name: yesod-sitemap
version: 1.0.0.1 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -14,10 +14,11 @@ description: Generate XML sitemaps.
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, time >= 1.1.4 , time >= 1.1.4
, xml-conduit >= 0.7 && < 0.8 , xml-conduit >= 1.0 && < 1.1
, text , text
, containers
exposed-modules: Yesod.Sitemap exposed-modules: Yesod.Sitemap
ghc-options: -Wall ghc-options: -Wall

View File

@ -78,19 +78,15 @@ import System.Posix.Types (EpochTime)
import Data.Conduit (($$)) import Data.Conduit (($$))
import Data.Conduit.List (sourceList) import Data.Conduit.List (sourceList)
import Data.Functor.Identity (runIdentity) import Data.Functor.Identity (runIdentity)
import qualified Filesystem.Path.CurrentOS as F
import Network.Wai.Application.Static import Network.Wai.Application.Static
( StaticSettings (..) ( StaticSettings (..)
, defaultWebAppSettings
, staticApp , staticApp
, embeddedLookup
, toEmbedded
, toFilePath
, fromFilePath
, FilePath
, ETagLookup
, webAppSettingsWithLookup , webAppSettingsWithLookup
, embeddedSettings
) )
import WaiAppStatic.Storage.Filesystem (ETagLookup)
-- | Type used for the subsite with static contents. -- | Type used for the subsite with static contents.
newtype Static = Static StaticSettings newtype Static = Static StaticSettings
@ -106,7 +102,7 @@ type StaticRoute = Route Static
static :: Prelude.FilePath -> IO Static static :: Prelude.FilePath -> IO Static
static dir = do static dir = do
hashLookup <- cachedETagLookup dir hashLookup <- cachedETagLookup dir
return $ Static $ webAppSettingsWithLookup (toFilePath dir) hashLookup return $ Static $ webAppSettingsWithLookup (F.decodeString dir) hashLookup
-- | Same as 'static', but does not assumes that the files do not -- | Same as 'static', but does not assumes that the files do not
-- change and checks their modification time whenever a request -- change and checks their modification time whenever a request
@ -114,7 +110,7 @@ static dir = do
staticDevel :: Prelude.FilePath -> IO Static staticDevel :: Prelude.FilePath -> IO Static
staticDevel dir = do staticDevel dir = do
hashLookup <- cachedETagLookupDevel dir hashLookup <- cachedETagLookupDevel dir
return $ Static $ webAppSettingsWithLookup (toFilePath dir) hashLookup return $ Static $ webAppSettingsWithLookup (F.decodeString dir) hashLookup
-- | Produce a 'Static' based on embedding all of the static -- | Produce a 'Static' based on embedding all of the static
-- files' contents in the executable at compile time. -- files' contents in the executable at compile time.
@ -126,10 +122,7 @@ staticDevel dir = do
-- You can easily change @addStaticContent@ to @\_ _ _ -> return Nothing@ as a workaround. -- You can easily change @addStaticContent@ to @\_ _ _ -> return Nothing@ as a workaround.
-- This will cause yesod to embed those assets into the generated HTML file itself. -- This will cause yesod to embed those assets into the generated HTML file itself.
embed :: Prelude.FilePath -> Q Exp embed :: Prelude.FilePath -> Q Exp
embed fp = embed fp = [|Static (embeddedSettings $(embedDir fp))|]
[|Static (defaultWebAppSettings
{ ssFolder = embeddedLookup (toEmbedded $(embedDir fp))
})|]
instance RenderRoute Static where instance RenderRoute Static where
-- | A route on the static subsite (see also 'staticFiles'). -- | A route on the static subsite (see also 'staticFiles').
@ -152,10 +145,10 @@ instance RenderRoute Static where
instance Yesod master => YesodDispatch Static master where instance Yesod master => YesodDispatch Static master where
-- Need to append trailing slash to make relative links work -- Need to append trailing slash to make relative links work
yesodDispatch _ _ _ _ _ _ [] _ req = yesodDispatch _ _ _ _ _ _ _ [] _ req =
return $ responseLBS status301 [("Location", rawPathInfo req `S.append` "/")] "" return $ responseLBS status301 [("Location", rawPathInfo req `S.append` "/")] ""
yesodDispatch _ (Static set) _ _ _ _ textPieces _ req = yesodDispatch _ _ (Static set) _ _ _ _ textPieces _ req =
staticApp set req { pathInfo = textPieces } staticApp set req { pathInfo = textPieces }
notHidden :: Prelude.FilePath -> Bool notHidden :: Prelude.FilePath -> Bool
@ -233,18 +226,18 @@ publicFiles :: Prelude.FilePath -> Q [Dec]
publicFiles dir = mkStaticFiles' dir "StaticRoute" False publicFiles dir = mkStaticFiles' dir "StaticRoute" False
mkHashMap :: Prelude.FilePath -> IO (M.Map FilePath S8.ByteString) mkHashMap :: Prelude.FilePath -> IO (M.Map F.FilePath S8.ByteString)
mkHashMap dir = do mkHashMap dir = do
fs <- getFileListPieces dir fs <- getFileListPieces dir
hashAlist fs >>= return . M.fromList hashAlist fs >>= return . M.fromList
where where
hashAlist :: [[String]] -> IO [(FilePath, S8.ByteString)] hashAlist :: [[String]] -> IO [(F.FilePath, S8.ByteString)]
hashAlist fs = mapM hashPair fs hashAlist fs = mapM hashPair fs
where where
hashPair :: [String] -> IO (FilePath, S8.ByteString) hashPair :: [String] -> IO (F.FilePath, S8.ByteString)
hashPair pieces = do let file = pathFromRawPieces dir pieces hashPair pieces = do let file = pathFromRawPieces dir pieces
h <- base64md5File file h <- base64md5File file
return (toFilePath file, S8.pack h) return (F.decodeString file, S8.pack h)
pathFromRawPieces :: Prelude.FilePath -> [String] -> Prelude.FilePath pathFromRawPieces :: Prelude.FilePath -> [String] -> Prelude.FilePath
pathFromRawPieces = pathFromRawPieces =
@ -255,12 +248,12 @@ pathFromRawPieces =
cachedETagLookupDevel :: Prelude.FilePath -> IO ETagLookup cachedETagLookupDevel :: Prelude.FilePath -> IO ETagLookup
cachedETagLookupDevel dir = do cachedETagLookupDevel dir = do
etags <- mkHashMap dir etags <- mkHashMap dir
mtimeVar <- newIORef (M.empty :: M.Map FilePath EpochTime) mtimeVar <- newIORef (M.empty :: M.Map F.FilePath EpochTime)
return $ \f -> return $ \f ->
case M.lookup f etags of case M.lookup f etags of
Nothing -> return Nothing Nothing -> return Nothing
Just checksum -> do Just checksum -> do
fs <- getFileStatus $ fromFilePath f fs <- getFileStatus $ F.encodeString f
let newt = modificationTime fs let newt = modificationTime fs
mtimes <- readIORef mtimeVar mtimes <- readIORef mtimeVar
oldt <- case M.lookup f mtimes of oldt <- case M.lookup f mtimes of

View File

@ -1,5 +1,5 @@
name: yesod-static name: yesod-static
version: 1.0.0.3 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -19,22 +19,23 @@ library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, containers >= 0.2 , containers >= 0.2
, old-time >= 1.0 , old-time >= 1.0
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, base64-bytestring >= 0.1.0.1 && < 0.2 , base64-bytestring >= 0.1.0.1 && < 0.2
, cereal >= 0.3 && < 0.4 , cereal >= 0.3 && < 0.4
, bytestring >= 0.9.1.4 , bytestring >= 0.9.1.4
, template-haskell , template-haskell
, directory >= 1.0 && < 1.2 , directory >= 1.0 && < 1.2
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, wai-app-static >= 1.2 && < 1.3 , wai-app-static >= 1.3 && < 1.4
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, text >= 0.9 && < 1.0 , text >= 0.9 && < 1.0
, file-embed >= 0.0.4.1 && < 0.5 , file-embed >= 0.0.4.1 && < 0.5
, http-types >= 0.6.5 && < 0.7 , http-types >= 0.7 && < 0.8
, unix-compat >= 0.2 , unix-compat >= 0.2
, conduit >= 0.4 && < 0.5 , conduit >= 0.5 && < 0.6
, crypto-conduit >= 0.3 && < 0.4 , crypto-conduit >= 0.4 && < 0.5
, cryptohash >= 0.6.1 , cryptohash >= 0.6.1
, system-filepath >= 0.4.6 && < 0.5
exposed-modules: Yesod.Static exposed-modules: Yesod.Static
ghc-options: -Wall ghc-options: -Wall
@ -60,11 +61,12 @@ test-suite tests
, wai , wai
, text >= 0.9 && < 1.0 , text >= 0.9 && < 1.0
, file-embed >= 0.0.4.1 && < 0.5 , file-embed >= 0.0.4.1 && < 0.5
, http-types >= 0.6.5 && < 0.7 , http-types
, unix-compat >= 0.2 , unix-compat >= 0.2
, conduit , conduit
, crypto-conduit , crypto-conduit
, cryptohash >= 0.6.1 , cryptohash >= 0.6.1
, system-filepath
ghc-options: -Wall ghc-options: -Wall

View File

@ -69,6 +69,7 @@ import qualified Test.Hspec.Runner as Runner
import qualified Data.List as DL import qualified Data.List as DL
import qualified Data.Maybe as DY import qualified Data.Maybe as DY
import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Char8 as BS8
import Data.ByteString (ByteString)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding as TE
import qualified Data.ByteString.Lazy.Char8 as BSL8 import qualified Data.ByteString.Lazy.Char8 as BSL8
@ -77,7 +78,6 @@ import qualified Test.Hspec.HUnit ()
import qualified Network.HTTP.Types as H import qualified Network.HTTP.Types as H
import qualified Network.Socket.Internal as Sock import qualified Network.Socket.Internal as Sock
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
import Text.XML.HXT.Core hiding (app, err)
import Network.Wai import Network.Wai
import Network.Wai.Test hiding (assertHeader, assertNoHeader) import Network.Wai.Test hiding (assertHeader, assertNoHeader)
import qualified Control.Monad.Trans.State as ST import qualified Control.Monad.Trans.State as ST
@ -88,6 +88,8 @@ import Database.Persist.GenericSql
import Data.Monoid (mappend) import Data.Monoid (mappend)
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import Data.Text.Lazy.Encoding (encodeUtf8, decodeUtf8) import Data.Text.Lazy.Encoding (encodeUtf8, decodeUtf8)
import Text.XML.Cursor hiding (element)
import qualified Text.HTML.DOM as HD
-- | The state used in 'describe' to build a list of specs -- | The state used in 'describe' to build a list of specs
data SpecsData = SpecsData Application ConnectionPool [Core.Spec] data SpecsData = SpecsData Application ConnectionPool [Core.Spec]
@ -105,8 +107,8 @@ data RequestBuilderData = RequestBuilderData [RequestPart] (Maybe SResponse)
-- | Request parts let us discern regular key/values from files sent in the request. -- | Request parts let us discern regular key/values from files sent in the request.
data RequestPart data RequestPart
= ReqPlainPart String String = ReqPlainPart T.Text T.Text
| ReqFilePart String FilePath BSL8.ByteString String | ReqFilePart T.Text FilePath BSL8.ByteString T.Text
-- | The RequestBuilder state monad constructs an url encoded string of arguments -- | The RequestBuilder state monad constructs an url encoded string of arguments
-- to send with your requests. Some of the functions that run on it use the current -- to send with your requests. Some of the functions that run on it use the current
@ -123,7 +125,7 @@ instance HoldsResponse OneSpecData where
instance HoldsResponse RequestBuilderData where instance HoldsResponse RequestBuilderData where
readResponse (RequestBuilderData _ x) = x readResponse (RequestBuilderData _ x) = x
type CookieValue = H.Ascii type CookieValue = ByteString
-- | Runs your test suite, using you wai 'Application' and 'ConnectionPool' for performing -- | Runs your test suite, using you wai 'Application' and 'ConnectionPool' for performing
-- the database queries in your tests. -- the database queries in your tests.
@ -136,12 +138,7 @@ type CookieValue = H.Ascii
runTests :: Application -> ConnectionPool -> Specs -> IO () runTests :: Application -> ConnectionPool -> Specs -> IO ()
runTests app connection specsDef = do runTests app connection specsDef = do
(SpecsData _ _ specs) <- ST.execStateT specsDef (SpecsData app connection []) (SpecsData _ _ specs) <- ST.execStateT specsDef (SpecsData app connection [])
#if MIN_VERSION_hspec(1,2,0) Runner.hspec specs
Runner.hspec
#else
Runner.hspecX
#endif
specs
-- | Start describing a Tests suite keeping cookies and a reference to the tested 'Application' -- | Start describing a Tests suite keeping cookies and a reference to the tested 'Application'
-- and 'ConnectionPool' -- and 'ConnectionPool'
@ -168,14 +165,14 @@ withResponse f = maybe err f =<< fmap readResponse ST.get
-- | Use HXT to parse a value from an html tag. -- | Use HXT to parse a value from an html tag.
-- Check for usage examples in this module's source. -- Check for usage examples in this module's source.
parseHTML :: Html -> LA XmlTree a -> [a] parseHTML :: Html -> (Cursor -> [a]) -> [a]
parseHTML html p = runLA (hread >>> p ) (TL.unpack $ decodeUtf8 html) parseHTML html p = p $ fromDocument $ HD.parseLBS html
-- | Query the last response using css selectors, returns a list of matched fragments -- | Query the last response using css selectors, returns a list of matched fragments
htmlQuery :: HoldsResponse a => Query -> ST.StateT a IO [Html] htmlQuery :: HoldsResponse a => Query -> ST.StateT a IO [Html]
htmlQuery query = withResponse $ \ res -> htmlQuery query = withResponse $ \ res ->
case findBySelector (simpleBody res) query of case findBySelector (simpleBody res) query of
Left err -> failure $ T.unpack query ++ " did not parse: " ++ (show err) Left err -> failure $ query <> " did not parse: " <> T.pack (show err)
Right matches -> return $ map (encodeUtf8 . TL.pack) matches Right matches -> return $ map (encodeUtf8 . TL.pack) matches
-- | Asserts that the two given values are equal. -- | Asserts that the two given values are equal.
@ -194,7 +191,7 @@ statusIs number = withResponse $ \ SResponse { simpleStatus = s } ->
assertHeader :: HoldsResponse a => CI BS8.ByteString -> BS8.ByteString -> ST.StateT a IO () assertHeader :: HoldsResponse a => CI BS8.ByteString -> BS8.ByteString -> ST.StateT a IO ()
assertHeader header value = withResponse $ \ SResponse { simpleHeaders = h } -> assertHeader header value = withResponse $ \ SResponse { simpleHeaders = h } ->
case lookup header h of case lookup header h of
Nothing -> failure $ concat Nothing -> failure $ T.pack $ concat
[ "Expected header " [ "Expected header "
, show header , show header
, " to be " , " to be "
@ -215,7 +212,7 @@ assertNoHeader :: HoldsResponse a => CI BS8.ByteString -> ST.StateT a IO ()
assertNoHeader header = withResponse $ \ SResponse { simpleHeaders = h } -> assertNoHeader header = withResponse $ \ SResponse { simpleHeaders = h } ->
case lookup header h of case lookup header h of
Nothing -> return () Nothing -> return ()
Just s -> failure $ concat Just s -> failure $ T.pack $ concat
[ "Unexpected header " [ "Unexpected header "
, show header , show header
, " containing " , " containing "
@ -245,7 +242,7 @@ htmlAllContain :: HoldsResponse a => Query -> String -> ST.StateT a IO ()
htmlAllContain query search = do htmlAllContain query search = do
matches <- htmlQuery query matches <- htmlQuery query
case matches of case matches of
[] -> failure $ "Nothing matched css query: "++T.unpack query [] -> failure $ "Nothing matched css query: " <> query
_ -> liftIO $ HUnit.assertBool ("Not all "++T.unpack query++" contain "++search) $ _ -> liftIO $ HUnit.assertBool ("Not all "++T.unpack query++" contain "++search) $
DL.all (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches) DL.all (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches)
@ -269,7 +266,7 @@ printMatches query = do
liftIO $ hPutStrLn stderr $ show matches liftIO $ hPutStrLn stderr $ show matches
-- | Add a parameter with the given name and value. -- | Add a parameter with the given name and value.
byName :: String -> String -> RequestBuilder () byName :: T.Text -> T.Text -> RequestBuilder ()
byName name value = do byName name value = do
RequestBuilderData parts r <- ST.get RequestBuilderData parts r <- ST.get
ST.put $ RequestBuilderData ((ReqPlainPart name value):parts) r ST.put $ RequestBuilderData ((ReqPlainPart name value):parts) r
@ -277,50 +274,67 @@ byName name value = do
-- | Add a file to be posted with the current request -- | Add a file to be posted with the current request
-- --
-- Adding a file will automatically change your request content-type to be multipart/form-data -- Adding a file will automatically change your request content-type to be multipart/form-data
fileByName :: String -> FilePath -> String -> RequestBuilder () fileByName :: T.Text -> FilePath -> T.Text -> RequestBuilder ()
fileByName name path mimetype = do fileByName name path mimetype = do
RequestBuilderData parts r <- ST.get RequestBuilderData parts r <- ST.get
contents <- liftIO $ BSL8.readFile path contents <- liftIO $ BSL8.readFile path
ST.put $ RequestBuilderData ((ReqFilePart name path contents mimetype):parts) r ST.put $ RequestBuilderData ((ReqFilePart name path contents mimetype):parts) r
-- This looks up the name of a field based on the contents of the label pointing to it. -- This looks up the name of a field based on the contents of the label pointing to it.
nameFromLabel :: String -> RequestBuilder String nameFromLabel :: T.Text -> RequestBuilder T.Text
nameFromLabel label = withResponse $ \ res -> do nameFromLabel label = withResponse $ \ res -> do
let let
body = simpleBody res body = simpleBody res
escaped = escapeHtmlEntities label escaped = escapeHtmlEntities label
mfor = parseHTML body $ deep $ hasName "label" mfor = parseHTML body $ \c -> c
>>> filterA (xshow this >>> mkText >>> hasText (DL.isInfixOf escaped)) $// attributeIs "name" "label"
>>> getAttrValue "for" >=> contentContains escaped
>=> attribute "for"
contentContains x c
| x `T.isInfixOf` T.concat (c $// content) = [c]
| otherwise = []
case mfor of case mfor of
for:[] -> do for:[] -> do
let mname = parseHTML body $ deep $ hasAttrValue "id" (==for) >>> getAttrValue "name" let mname = parseHTML body $ \c -> c
$// attributeIs "id" for
>=> attribute "name"
case mname of case mname of
"":_ -> failure $ "Label "++label++" resolved to id "++for++" which was not found. " "":_ -> failure $ T.concat
[ "Label "
, label
, " resolved to id "
, for
, " which was not found. "
]
name:_ -> return name name:_ -> return name
_ -> failure $ "More than one input with id " ++ for _ -> failure $ "More than one input with id " <> for
[] -> failure $ "No label contained: "++label [] -> failure $ "No label contained: " <> label
_ -> failure $ "More than one label contained "++label _ -> failure $ "More than one label contained " <> label
(<>) :: T.Text -> T.Text -> T.Text
(<>) = T.append
-- | Escape HTML entities in a string, so you can write the text you want in -- | Escape HTML entities in a string, so you can write the text you want in
-- label lookups without worrying about the fact that yesod escapes some characters. -- label lookups without worrying about the fact that yesod escapes some characters.
escapeHtmlEntities :: String -> String escapeHtmlEntities :: T.Text -> T.Text
escapeHtmlEntities "" = "" escapeHtmlEntities =
escapeHtmlEntities (c:cs) = case c of T.concatMap go
'<' -> '&' : 'l' : 't' : ';' : escapeHtmlEntities cs where
'>' -> '&' : 'g' : 't' : ';' : escapeHtmlEntities cs go '<' = "&lt;"
'&' -> '&' : 'a' : 'm' : 'p' : ';' : escapeHtmlEntities cs go '>' = "&gt;"
'"' -> '&' : 'q' : 'u' : 'o' : 't' : ';' : escapeHtmlEntities cs go '&' = "&amp;"
'\'' -> '&' : '#' : '3' : '9' : ';' : escapeHtmlEntities cs go '"' = "&quot;"
x -> x : escapeHtmlEntities cs go '\'' = "&#39;"
go x = T.singleton x
byLabel :: String -> String -> RequestBuilder () byLabel :: T.Text -> T.Text -> RequestBuilder ()
byLabel label value = do byLabel label value = do
name <- nameFromLabel label name <- nameFromLabel label
byName name value byName name value
fileByLabel :: String -> FilePath -> String -> RequestBuilder () fileByLabel :: T.Text -> FilePath -> T.Text -> RequestBuilder ()
fileByLabel label path mime = do fileByLabel label path mime = do
name <- nameFromLabel label name <- nameFromLabel label
fileByName name path mime fileByName name path mime
@ -332,7 +346,7 @@ addNonce_ scope = do
matches <- htmlQuery $ scope `mappend` "input[name=_token][type=hidden][value]" matches <- htmlQuery $ scope `mappend` "input[name=_token][type=hidden][value]"
case matches of case matches of
[] -> failure $ "No nonce found in the current page" [] -> failure $ "No nonce found in the current page"
element:[] -> byName "_token" $ head $ parseHTML element $ getAttrValue "value" element:[] -> byName "_token" $ head $ parseHTML element $ attribute "value"
_ -> failure $ "More than one nonce found in the page" _ -> failure $ "More than one nonce found in the page"
-- | For responses that display a single form, just lookup the only nonce available. -- | For responses that display a single form, just lookup the only nonce available.
@ -384,22 +398,22 @@ doRequest method url paramsBuild = do
BS8.concat $ separator : [BS8.concat [multipartPart p, separator] | p <- parts] BS8.concat $ separator : [BS8.concat [multipartPart p, separator] | p <- parts]
multipartPart (ReqPlainPart k v) = BS8.concat multipartPart (ReqPlainPart k v) = BS8.concat
[ "Content-Disposition: form-data; " [ "Content-Disposition: form-data; "
, "name=\"", (BS8.pack k), "\"\r\n\r\n" , "name=\"", TE.encodeUtf8 k, "\"\r\n\r\n"
, (BS8.pack v), "\r\n"] , TE.encodeUtf8 v, "\r\n"]
multipartPart (ReqFilePart k v bytes mime) = BS8.concat multipartPart (ReqFilePart k v bytes mime) = BS8.concat
[ "Content-Disposition: form-data; " [ "Content-Disposition: form-data; "
, "name=\"", BS8.pack k, "\"; " , "name=\"", TE.encodeUtf8 k, "\"; "
, "filename=\"", BS8.pack v, "\"\r\n" , "filename=\"", BS8.pack v, "\"\r\n"
, "Content-Type: ", BS8.pack mime, "\r\n\r\n" , "Content-Type: ", TE.encodeUtf8 mime, "\r\n\r\n"
, BS8.concat $ BSL8.toChunks bytes, "\r\n"] , BS8.concat $ BSL8.toChunks bytes, "\r\n"]
-- For building the regular non-multipart requests -- For building the regular non-multipart requests
makeSinglepart cookie parts = SRequest (mkRequest makeSinglepart cookie parts = SRequest (mkRequest
[("Cookie",cookie), ("Content-Type", "application/x-www-form-urlencoded")]) $ [("Cookie",cookie), ("Content-Type", "application/x-www-form-urlencoded")]) $
BSL8.pack $ DL.concat $ DL.intersperse "&" $ map singlepartPart parts BSL8.fromChunks $ return $ TE.encodeUtf8 $ T.intercalate "&" $ map singlepartPart parts
singlepartPart (ReqFilePart _ _ _ _) = "" singlepartPart (ReqFilePart _ _ _ _) = ""
singlepartPart (ReqPlainPart k v) = concat [k,"=",v] singlepartPart (ReqPlainPart k v) = T.concat [k,"=",v]
-- General request making -- General request making
mkRequest headers = defaultRequest mkRequest headers = defaultRequest
@ -418,5 +432,5 @@ runDB query = do
liftIO $ runSqlPool query pool liftIO $ runSqlPool query pool
-- Yes, just a shortcut -- Yes, just a shortcut
failure :: (MonadIO a) => String -> a b failure :: (MonadIO a) => T.Text -> a b
failure reason = (liftIO $ HUnit.assertFailure reason) >> error "" failure reason = (liftIO $ HUnit.assertFailure $ T.unpack reason) >> error ""

View File

@ -1,14 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- | Parse an HTML document into xml-conduit's Document.
--
-- Assumes UTF-8 encoding.
module Yesod.Test.HtmlParse
( parseHtml
) where
import qualified Data.ByteString.Lazy as L
import Text.XML (Document)
import qualified Text.HTML.DOM as HD
parseHtml :: L.ByteString -> Either String Document
parseHtml = Right . HD.parseLBS

View File

@ -41,11 +41,11 @@ where
import Yesod.Test.CssQuery import Yesod.Test.CssQuery
import qualified Data.Text as T import qualified Data.Text as T
import Yesod.Test.HtmlParse (parseHtml)
import Control.Applicative ((<$>), (<*>)) import Control.Applicative ((<$>), (<*>))
import Text.XML import Text.XML
import Text.XML.Cursor import Text.XML.Cursor
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import qualified Text.HTML.DOM as HD
#if MIN_VERSION_blaze_html(0, 5, 0) #if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze.Html (toHtml) import Text.Blaze.Html (toHtml)
import Text.Blaze.Html.Renderer.String (renderHtml) import Text.Blaze.Html.Renderer.String (renderHtml)
@ -53,7 +53,6 @@ import Text.Blaze.Html.Renderer.String (renderHtml)
import Text.Blaze (toHtml) import Text.Blaze (toHtml)
import Text.Blaze.Renderer.String (renderHtml) import Text.Blaze.Renderer.String (renderHtml)
#endif #endif
import Text.XML.Xml2Html ()
type Query = T.Text type Query = T.Text
type Html = L.ByteString type Html = L.ByteString
@ -65,7 +64,7 @@ type Html = L.ByteString
-- * Right: List of matching Html fragments. -- * Right: List of matching Html fragments.
findBySelector :: Html -> Query -> Either String [String] findBySelector :: Html -> Query -> Either String [String]
findBySelector html query = (\x -> map (renderHtml . toHtml . node) . runQuery x) findBySelector html query = (\x -> map (renderHtml . toHtml . node) . runQuery x)
<$> (fromDocument <$> parseHtml html) <$> (Right $ fromDocument $ HD.parseLBS html)
<*> parseQuery query <*> parseQuery query
-- Run a compiled query on Html, returning a list of matching Html fragments. -- Run a compiled query on Html, returning a list of matching Html fragments.

View File

@ -6,23 +6,18 @@ import Test.Hspec.HUnit ()
import Yesod.Test.CssQuery import Yesod.Test.CssQuery
import Yesod.Test.TransversingCSS import Yesod.Test.TransversingCSS
import Yesod.Test.HtmlParse
import Text.XML import Text.XML
import Data.ByteString.Lazy.Char8 () import Data.ByteString.Lazy.Char8 ()
import qualified Data.Map as Map
import qualified Text.HTML.DOM as HD
parseQuery_ = either error id . parseQuery parseQuery_ = either error id . parseQuery
findBySelector_ x = either error id . findBySelector x findBySelector_ x = either error id . findBySelector x
parseHtml_ = either error id . parseHtml parseHtml_ = HD.parseLBS
main :: IO () main :: IO ()
main = main = hspec $ do
#if MIN_VERSION_hspec(1,2,0)
hspec
#else
hspecX
#endif
$ do
describe "CSS selector parsing" $ do describe "CSS selector parsing" $ do
it "elements" $ parseQuery_ "strong" @?= [[DeepChildren [ByTagName "strong"]]] it "elements" $ parseQuery_ "strong" @?= [[DeepChildren [ByTagName "strong"]]]
it "child elements" $ parseQuery_ "strong > i" @?= [[DeepChildren [ByTagName "strong"], DirectChildren [ByTagName "i"]]] it "child elements" $ parseQuery_ "strong > i" @?= [[DeepChildren [ByTagName "strong"], DirectChildren [ByTagName "i"]]]
@ -40,13 +35,13 @@ main =
it "XHTML" $ it "XHTML" $
let html = "<html><head><title>foo</title></head><body><p>Hello World</p></body></html>" let html = "<html><head><title>foo</title></head><body><p>Hello World</p></body></html>"
doc = Document (Prologue [] Nothing []) root [] doc = Document (Prologue [] Nothing []) root []
root = Element "html" [] root = Element "html" Map.empty
[ NodeElement $ Element "head" [] [ NodeElement $ Element "head" Map.empty
[ NodeElement $ Element "title" [] [ NodeElement $ Element "title" Map.empty
[NodeContent "foo"] [NodeContent "foo"]
] ]
, NodeElement $ Element "body" [] , NodeElement $ Element "body" Map.empty
[ NodeElement $ Element "p" [] [ NodeElement $ Element "p" Map.empty
[NodeContent "Hello World"] [NodeContent "Hello World"]
] ]
] ]
@ -54,14 +49,14 @@ main =
it "HTML" $ it "HTML" $
let html = "<html><head><title>foo</title></head><body><br><p>Hello World</p></body></html>" let html = "<html><head><title>foo</title></head><body><br><p>Hello World</p></body></html>"
doc = Document (Prologue [] Nothing []) root [] doc = Document (Prologue [] Nothing []) root []
root = Element "html" [] root = Element "html" Map.empty
[ NodeElement $ Element "head" [] [ NodeElement $ Element "head" Map.empty
[ NodeElement $ Element "title" [] [ NodeElement $ Element "title" Map.empty
[NodeContent "foo"] [NodeContent "foo"]
] ]
, NodeElement $ Element "body" [] , NodeElement $ Element "body" Map.empty
[ NodeElement $ Element "br" [] [] [ NodeElement $ Element "br" Map.empty []
, NodeElement $ Element "p" [] , NodeElement $ Element "p" Map.empty
[NodeContent "Hello World"] [NodeContent "Hello World"]
] ]
] ]

View File

@ -1,5 +1,5 @@
name: yesod-test name: yesod-test
version: 0.2.1 version: 0.3.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Nubis <nubis@woobiz.com.ar> author: Nubis <nubis@woobiz.com.ar>
@ -13,44 +13,30 @@ homepage: http://www.yesodweb.com
description: Behaviour Oriented integration Testing for Yesod Applications description: Behaviour Oriented integration Testing for Yesod Applications
extra-source-files: README.md, LICENSE, test/main.hs extra-source-files: README.md, LICENSE, test/main.hs
flag blaze_html_0_5
description: use blaze-html 0.5 and blaze-markup 0.5
default: True
library library
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.3 && < 5
, hxt >= 9.1.6
, attoparsec >= 0.10 && < 0.11 , attoparsec >= 0.10 && < 0.11
, persistent >= 0.9 && < 0.10 , persistent >= 1.0 && < 1.1
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, wai-test >= 1.2 && < 1.3 , wai-test >= 1.3 && < 1.4
, network >= 2.2 && < 2.4 , network >= 2.2 && < 2.4
, http-types >= 0.6 && < 0.7 , http-types >= 0.7 && < 0.8
, HUnit >= 1.2 && < 1.3 , HUnit >= 1.2 && < 1.3
, hspec >= 1.1 && < 1.3 , hspec >= 1.2 && < 1.3
, bytestring >= 0.9 , bytestring >= 0.9
, case-insensitive >= 0.2 , case-insensitive >= 0.2
, text , text
, xml-conduit >= 0.7 && < 0.8 , xml-conduit >= 1.0 && < 1.1
, xml-types >= 0.3 && < 0.4 , xml-types >= 0.3 && < 0.4
, containers , containers
, xml2html >= 0.1.2.3 && < 0.2 , html-conduit >= 0.1 && < 0.2
, html-conduit >= 0.0.1 && < 0.1 , blaze-html >= 0.5 && < 0.6
, blaze-markup >= 0.5.1 && < 0.6
if flag(blaze_html_0_5)
build-depends:
blaze-html >= 0.5 && < 0.6
, blaze-markup >= 0.5.1 && < 0.6
else
build-depends:
blaze-html >= 0.4 && < 0.5
exposed-modules: Yesod.Test exposed-modules: Yesod.Test
Yesod.Test.CssQuery Yesod.Test.CssQuery
Yesod.Test.TransversingCSS Yesod.Test.TransversingCSS
Yesod.Test.HtmlParse
ghc-options: -Wall ghc-options: -Wall
test-suite test test-suite test
@ -63,6 +49,8 @@ test-suite test
, HUnit , HUnit
, xml-conduit , xml-conduit
, bytestring , bytestring
, containers
, html-conduit
source-repository head source-repository head
type: git type: git

113
yesod/AddHandler.hs Normal file
View File

@ -0,0 +1,113 @@
module AddHandler (addHandler) where
import Prelude hiding (readFile)
import System.IO (hFlush, stdout)
import Data.Char (isLower, toLower, isSpace)
import Data.List (isPrefixOf, isSuffixOf)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import System.Directory (getDirectoryContents)
-- strict readFile
readFile :: FilePath -> IO String
readFile = fmap T.unpack . TIO.readFile
addHandler :: IO ()
addHandler = do
allFiles <- getDirectoryContents "."
cabal <-
case filter (".cabal" `isSuffixOf`) allFiles of
[x] -> return x
[] -> error "No cabal file found"
_ -> error "Too many cabal files found"
putStr "Name of route (without trailing R): "
hFlush stdout
name <- getLine
case name of
[] -> error "Please provide a name"
c:_
| isLower c -> error "Name must start with an upper case letter"
| otherwise -> return ()
putStr "Enter route pattern: "
hFlush stdout
pattern <- getLine
putStr "Enter space-separated list of methods: "
hFlush stdout
methods <- getLine
let modify fp f = readFile fp >>= writeFile fp . f
modify "Application.hs" $ fixApp name
modify cabal $ fixCabal name
modify "config/routes" $ fixRoutes name pattern methods
writeFile ("Handler/" ++ name ++ ".hs") $ mkHandler name pattern methods
fixApp :: String -> String -> String
fixApp name =
unlines . reverse . go . reverse . lines
where
l = "import Handler." ++ name
go [] = [l]
go (x:xs)
| "import Handler." `isPrefixOf` x = l : x : xs
| otherwise = x : go xs
fixCabal :: String -> String -> String
fixCabal name =
unlines . reverse . go . reverse . lines
where
l = "import Handler." ++ name
go [] = [l]
go (x:xs)
| "Handler." `isPrefixOf` x' = (spaces ++ "Handler." ++ name) : x : xs
| otherwise = x : go xs
where
(spaces, x') = span isSpace x
fixRoutes :: String -> String -> String -> String -> String
fixRoutes name pattern methods =
(++ l)
where
l = concat
[ pattern
, " "
, name
, "R "
, methods
, "\n"
]
mkHandler :: String -> String -> String -> String
mkHandler name pattern methods = unlines
$ ("module Handler." ++ name ++ " where")
: ""
: "import Import"
: concatMap go (words methods)
where
go method =
[ ""
, concat $ func : " :: " : map toArrow types ++ ["Handler RepHtml"]
, concat
[ func
, " = error \"Not yet implemented: "
, func
, "\""
]
]
where
func = concat [map toLower method, name, "R"]
types = getTypes pattern
toArrow t = concat [t, " -> "]
getTypes "" = []
getTypes ('/':rest) = getTypes rest
getTypes ('#':rest) =
typ : getTypes rest'
where
(typ, rest') = break (== '/') rest
getTypes rest = getTypes $ dropWhile (/= '/') rest

View File

@ -143,6 +143,7 @@ scaffold = do
Mysql -> writeFile' ("config/" ++ backendLower ++ ".yml") $(codegen "config/mysql.yml") Mysql -> writeFile' ("config/" ++ backendLower ++ ".yml") $(codegen "config/mysql.yml")
writeFile' "config/settings.yml" $(codegen "config/settings.yml") writeFile' "config/settings.yml" $(codegen "config/settings.yml")
writeFile' "config/keter.yaml" $(codegen "config/keter.yaml")
writeFile' "main.hs" $(codegen "main.hs") writeFile' "main.hs" $(codegen "main.hs")
writeFile' "devel.hs" $(codegen "devel.hs") writeFile' "devel.hs" $(codegen "devel.hs")
writeFile' (project ++ ".cabal") $(codegen "project.cabal") writeFile' (project ++ ".cabal") $(codegen "project.cabal")

View File

@ -45,15 +45,13 @@ import Text.Julius
import Yesod.Form import Yesod.Form
import Yesod.Json import Yesod.Json
import Yesod.Persist import Yesod.Persist
import Network.HTTP.Types (status200)
import Control.Monad.IO.Class (liftIO, MonadIO(..)) import Control.Monad.IO.Class (liftIO, MonadIO(..))
import Control.Monad.Trans.Control (MonadBaseControl) import Control.Monad.Trans.Control (MonadBaseControl)
import Network.Wai import Network.Wai
import Network.Wai.Logger import Network.Wai.Middleware.RequestLogger (logStdout)
import Network.Wai.Handler.Warp (run) import Network.Wai.Handler.Warp (run)
import System.IO (stderr, stdout, hFlush, hPutStrLn) import System.IO (stderr, hPutStrLn)
import System.Log.FastLogger
#if MIN_VERSION_blaze_html(0, 5, 0) #if MIN_VERSION_blaze_html(0, 5, 0)
import Text.Blaze.Html (toHtml) import Text.Blaze.Html (toHtml)
#else #else
@ -80,23 +78,7 @@ warpDebug :: (Yesod a, YesodDispatch a a) => Int -> a -> IO ()
warpDebug port app = do warpDebug port app = do
hPutStrLn stderr $ "Application launched, listening on port " ++ show port hPutStrLn stderr $ "Application launched, listening on port " ++ show port
waiApp <- toWaiApp app waiApp <- toWaiApp app
dateRef <- dateInit run port $ logStdout waiApp
run port $ (logStdout dateRef) waiApp
logStdout :: DateRef -> Middleware
logStdout dateRef waiApp =
\req -> do
logRequest dateRef req
waiApp req
logRequest :: Control.Monad.IO.Class.MonadIO m =>
DateRef -> Network.Wai.Request -> m ()
logRequest dateRef req = do
date <- liftIO $ getDate dateRef
let status = status200
len = 4
liftIO $ hPutLogStr stdout $ apacheFormat FromSocket date req status (Just len)
liftIO $ hFlush stdout
-- | Run a development server, where your code changes are automatically -- | Run a development server, where your code changes are automatically
-- reloaded. -- reloaded.

View File

@ -11,6 +11,7 @@ import Control.Monad (unless)
import Build (touch) import Build (touch)
#endif #endif
import Devel (devel) import Devel (devel)
import AddHandler (addHandler)
windowsWarning :: String windowsWarning :: String
#ifdef WINDOWS #ifdef WINDOWS
@ -46,6 +47,7 @@ main = do
rawSystem' cmd ["test"] rawSystem' cmd ["test"]
["version"] -> putStrLn $ "yesod-core version:" ++ yesodVersion ["version"] -> putStrLn $ "yesod-core version:" ++ yesodVersion
"configure":rest -> rawSystem cmd ("configure":rest) >>= exitWith "configure":rest -> rawSystem cmd ("configure":rest) >>= exitWith
["add-handler"] -> addHandler
_ -> do _ -> do
putStrLn "Usage: yesod <command>" putStrLn "Usage: yesod <command>"
putStrLn "Available commands:" putStrLn "Available commands:"
@ -59,6 +61,7 @@ main = do
putStrLn " use --dev devel to build with cabal-dev" putStrLn " use --dev devel to build with cabal-dev"
putStrLn " test Build and run the integration tests" putStrLn " test Build and run the integration tests"
putStrLn " use --dev devel to build with cabal-dev" putStrLn " use --dev devel to build with cabal-dev"
putStrLn " add-handler Add a new handler and module to your project"
putStrLn " version Print the version of Yesod" putStrLn " version Print the version of Yesod"
-- | Like @rawSystem@, but exits if it receives a non-success result. -- | Like @rawSystem@, but exits if it receives a non-success result.

View File

@ -11,8 +11,7 @@ import Yesod.Auth
import Yesod.Default.Config import Yesod.Default.Config
import Yesod.Default.Main import Yesod.Default.Main
import Yesod.Default.Handlers import Yesod.Default.Handlers
import Yesod.Logger (Logger, logBS, toProduction) import Network.Wai.Middleware.RequestLogger (logStdout, logStdoutDev)
import Network.Wai.Middleware.RequestLogger (logCallback, logCallbackDev)
import qualified Database.Persist.Store~importMigration~ import qualified Database.Persist.Store~importMigration~
import Network.HTTP.Conduit (newManager, def) import Network.HTTP.Conduit (newManager, def)
@ -29,25 +28,24 @@ mkYesodDispatch "~sitearg~" resources~sitearg~
-- performs initialization and creates a WAI application. This is also the -- performs initialization and creates a WAI application. This is also the
-- place to put your migrate statements to have automatic database -- place to put your migrate statements to have automatic database
-- migrations handled by Yesod. -- migrations handled by Yesod.
makeApplication :: AppConfig DefaultEnv Extra -> Logger -> IO Application makeApplication :: AppConfig DefaultEnv Extra -> IO Application
makeApplication conf logger = do makeApplication conf = do
foundation <- makeFoundation conf setLogger foundation <- makeFoundation conf
app <- toWaiAppPlain foundation app <- toWaiAppPlain foundation
return $ logWare app return $ logWare app
where where
setLogger = if development then logger else toProduction logger logWare = if development then logStdoutDev
logWare = if development then logCallbackDev (logBS setLogger) else logStdout
else logCallback (logBS setLogger)
makeFoundation :: AppConfig DefaultEnv Extra -> Logger -> IO ~sitearg~ makeFoundation :: AppConfig DefaultEnv Extra -> IO ~sitearg~
makeFoundation conf setLogger = do makeFoundation conf = do
manager <- newManager def manager <- newManager def
s <- staticSite s <- staticSite
dbconf <- withYamlEnvironment "config/~dbConfigFile~.yml" (appEnv conf) dbconf <- withYamlEnvironment "config/~dbConfigFile~.yml" (appEnv conf)
Database.Persist.Store.loadConfig >>= Database.Persist.Store.loadConfig >>=
Database.Persist.Store.applyEnv Database.Persist.Store.applyEnv
p <- Database.Persist.Store.createPoolConfig (dbconf :: Settings.PersistConfig)~runMigration~ p <- Database.Persist.Store.createPoolConfig (dbconf :: Settings.PersistConfig)~runMigration~
return $ ~sitearg~ conf setLogger s p manager dbconf return $ ~sitearg~ conf s p manager dbconf
-- for yesod devel -- for yesod devel
getApplicationDev :: IO (Int, Application) getApplicationDev :: IO (Int, Application)

View File

@ -20,7 +20,6 @@ import Yesod.Auth.BrowserId
import Yesod.Auth.GoogleEmail import Yesod.Auth.GoogleEmail
import Yesod.Default.Config import Yesod.Default.Config
import Yesod.Default.Util (addStaticContentExternal) import Yesod.Default.Util (addStaticContentExternal)
import Yesod.Logger (Logger, logMsg, formatLogText)
import Network.HTTP.Conduit (Manager) import Network.HTTP.Conduit (Manager)
import qualified Settings import qualified Settings
import qualified Database.Persist.Store import qualified Database.Persist.Store
@ -38,7 +37,6 @@ import Text.Hamlet (hamletFile)
-- access to the data present here. -- access to the data present here.
data ~sitearg~ = ~sitearg~ data ~sitearg~ = ~sitearg~
{ settings :: AppConfig DefaultEnv Extra { settings :: AppConfig DefaultEnv Extra
, getLogger :: Logger
, getStatic :: Static -- ^ Settings for static file serving. , getStatic :: Static -- ^ Settings for static file serving.
, connPool :: Database.Persist.Store.PersistConfigPool Settings.PersistConfig -- ^ Database connection pool. , connPool :: Database.Persist.Store.PersistConfigPool Settings.PersistConfig -- ^ Database connection pool.
, httpManager :: Manager , httpManager :: Manager
@ -107,9 +105,6 @@ instance Yesod ~sitearg~ where
-- The page to be redirected to when authentication is required. -- The page to be redirected to when authentication is required.
authRoute _ = Just $ AuthR LoginR authRoute _ = Just $ AuthR LoginR
messageLogger y loc level msg =
formatLogText (getLogger y) loc level msg >>= logMsg (getLogger y)
-- This function creates static content files in the static folder -- This function creates static content files in the static folder
-- and names them based on a hash of their content. This allows -- and names them based on a hash of their content. This allows
-- expiration dates to be set far in the future without worry of -- expiration dates to be set far in the future without worry of

View File

@ -17,11 +17,13 @@ import Text.Shakespeare.Text (st)
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import Database.Persist.~importPersist~ (~configPersist~) import Database.Persist.~importPersist~ (~configPersist~)
import Yesod.Default.Config import Yesod.Default.Config
import qualified Yesod.Default.Util import Yesod.Default.Util
import Data.Text (Text) import Data.Text (Text)
import Data.Yaml import Data.Yaml
import Control.Applicative import Control.Applicative
import Settings.Development import Settings.Development
import Data.Default (def)
import Text.Hamlet
-- | Which Persistent backend this site is using. -- | Which Persistent backend this site is using.
type PersistConfig = ~configPersist~ type PersistConfig = ~configPersist~
@ -49,13 +51,22 @@ staticDir = "static"
staticRoot :: AppConfig DefaultEnv x -> Text staticRoot :: AppConfig DefaultEnv x -> Text
staticRoot conf = [st|#{appRoot conf}/static|] staticRoot conf = [st|#{appRoot conf}/static|]
-- | Settings for 'widgetFile', such as which template languages to support and
-- default Hamlet settings.
widgetFileSettings :: WidgetFileSettings
widgetFileSettings = def
{ wfsHamletSettings = defaultHamletSettings
{ hamletNewlines = AlwaysNewlines
}
}
-- The rest of this file contains settings which rarely need changing by a -- The rest of this file contains settings which rarely need changing by a
-- user. -- user.
widgetFile :: String -> Q Exp widgetFile :: String -> Q Exp
widgetFile = if development then Yesod.Default.Util.widgetFileReload widgetFile = (if development then widgetFileReload
else Yesod.Default.Util.widgetFileNoReload else widgetFileNoReload)
widgetFileSettings
data Extra = Extra data Extra = Extra
{ extraCopyright :: Text { extraCopyright :: Text

View File

@ -0,0 +1,4 @@
exec: ../dist/build/~project~/~project~
args:
- production
host: ~project~.yesodweb.com

View File

@ -50,31 +50,32 @@ library
NoMonomorphismRestriction NoMonomorphismRestriction
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-platform >= 1.0 && < 1.1 -- , yesod-platform >= 1.1 && < 1.2
, yesod >= 1.0 && < 1.1 , yesod >= 1.1 && < 1.2
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, yesod-auth >= 1.0 && < 1.1 , yesod-auth >= 1.1 && < 1.2
, yesod-static >= 1.0 && < 1.1 , yesod-static >= 1.1 && < 1.2
, yesod-default >= 1.0 && < 1.1 , yesod-default >= 1.1 && < 1.2
, yesod-form >= 1.0 && < 1.1 , yesod-form >= 1.1 && < 1.2
, yesod-test >= 0.2 && < 0.3 , yesod-test >= 0.3 && < 0.4
, clientsession >= 0.7.3 && < 0.8 , clientsession >= 0.8 && < 0.9
, bytestring >= 0.9 && < 0.10 , bytestring >= 0.9 && < 0.10
, text >= 0.11 && < 0.12 , text >= 0.11 && < 0.12
, persistent >= 0.9 && < 0.10 , persistent >= 1.0 && < 1.1
, persistent-~backendLower~ >= 0.9 && < 0.10 , persistent-~backendLower~ >= 1.0 && < 1.1
, template-haskell , template-haskell
, hamlet >= 1.0 && < 1.1 , hamlet >= 1.1 && < 1.2
, shakespeare-css >= 1.0 && < 1.1 , shakespeare-css >= 1.0 && < 1.1
, shakespeare-js >= 1.0 && < 1.1 , shakespeare-js >= 1.0 && < 1.1
, shakespeare-text >= 1.0 && < 1.1 , shakespeare-text >= 1.0 && < 1.1
, hjsmin >= 0.1 && < 0.2 , hjsmin >= 0.1 && < 0.2
, monad-control >= 0.3 && < 0.4 , monad-control >= 0.3 && < 0.4
, wai-extra >= 1.2 && < 1.3 , wai-extra >= 1.3 && < 1.4
, yaml >= 0.7 && < 0.8 , yaml >= 0.8 && < 0.9
, http-conduit >= 1.4 && < 1.5 , http-conduit >= 1.5 && < 1.6
, directory >= 1.1 && < 1.2 , directory >= 1.1 && < 1.2
, warp >= 1.2 && < 1.3 , warp >= 1.3 && < 1.4
, data-default
executable ~project~ executable ~project~
if flag(library-only) if flag(library-only)
@ -93,17 +94,6 @@ test-suite test
main-is: main.hs main-is: main.hs
hs-source-dirs: tests hs-source-dirs: tests
ghc-options: -Wall ghc-options: -Wall
extensions: TemplateHaskell
QuasiQuotes
OverloadedStrings
NoImplicitPrelude
CPP
OverloadedStrings
MultiParamTypeClasses
TypeFamilies
GADTs
GeneralizedNewtypeDeriving
FlexibleContexts
build-depends: base build-depends: base
, ~project~ , ~project~

View File

@ -1,3 +1,4 @@
$newline never
\<!doctype html> \<!doctype html>
\<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]--> \<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
\<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]--> \<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->

View File

@ -1,8 +1,8 @@
{-# LANGUAGE OverloadedStrings #-}
module HomeTest module HomeTest
( homeSpecs ( homeSpecs
) where ) where
import Import
import Yesod.Test import Yesod.Test
homeSpecs :: Specs homeSpecs :: Specs

View File

@ -13,7 +13,7 @@ import Application (makeFoundation)
import HomeTest import HomeTest
main :: IO a main :: IO ()
main = do main = do
conf <- loadConfig $ (configSettings Testing) { csParseExtra = parseExtra } conf <- loadConfig $ (configSettings Testing) { csParseExtra = parseExtra }
logger <- defaultDevelopmentLogger logger <- defaultDevelopmentLogger

View File

@ -1,5 +1,5 @@
name: yesod name: yesod
version: 1.0.1.6 version: 1.1.0
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -51,6 +51,7 @@ extra-source-files:
scaffold/templates/boilerplate-wrapper.hamlet.cg scaffold/templates/boilerplate-wrapper.hamlet.cg
scaffold/templates/homepage.lucius.cg scaffold/templates/homepage.lucius.cg
scaffold/messages/en.msg.cg scaffold/messages/en.msg.cg
scaffold/config/keter.yaml.cg
scaffold/config/models.cg scaffold/config/models.cg
scaffold/config/mysql.yml.cg scaffold/config/mysql.yml.cg
scaffold/config/sqlite.yml.cg scaffold/config/sqlite.yml.cg
@ -62,34 +63,23 @@ extra-source-files:
scaffold/config/mongoDB.yml.cg scaffold/config/mongoDB.yml.cg
scaffold/devel.hs.cg scaffold/devel.hs.cg
flag blaze_html_0_5
description: use blaze-html 0.5 and blaze-markup 0.5
default: True
library library
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.3 && < 5
, yesod-core >= 1.0 && < 1.1 , yesod-core >= 1.1 && < 1.2
, yesod-auth >= 1.0 && < 1.1 , yesod-auth >= 1.1 && < 1.2
, yesod-json >= 1.0 && < 1.1 , yesod-json >= 1.1 && < 1.2
, yesod-persistent >= 1.0 && < 1.1 , yesod-persistent >= 1.1 && < 1.2
, yesod-form >= 1.0 && < 1.1 , yesod-form >= 1.1 && < 1.2
, monad-control >= 0.3 && < 0.4 , monad-control >= 0.3 && < 0.4
, transformers >= 0.2.2 && < 0.4 , transformers >= 0.2.2 && < 0.4
, wai >= 1.2 && < 1.3 , wai >= 1.3 && < 1.4
, wai-extra >= 1.2 && < 1.3 , wai-extra >= 1.3 && < 1.4
, wai-logger >= 0.1.2 , hamlet >= 1.1 && < 1.2
, hamlet >= 1.0 && < 1.1
, shakespeare-js >= 1.0 && < 1.1 , shakespeare-js >= 1.0 && < 1.1
, shakespeare-css >= 1.0 && < 1.1 , shakespeare-css >= 1.0 && < 1.1
, warp >= 1.2 && < 1.3 , warp >= 1.3 && < 1.4
, blaze-html >= 0.5 && < 0.6
if flag(blaze_html_0_5) , blaze-markup >= 0.5.1 && < 0.6
build-depends:
blaze-html >= 0.5 && < 0.6
, blaze-markup >= 0.5.1 && < 0.6
else
build-depends:
blaze-html >= 0.4 && < 0.5
exposed-modules: Yesod exposed-modules: Yesod
ghc-options: -Wall ghc-options: -Wall
@ -109,10 +99,9 @@ executable yesod
, unix-compat >= 0.2 && < 0.4 , unix-compat >= 0.2 && < 0.4
, containers >= 0.2 , containers >= 0.2
, attoparsec >= 0.10 , attoparsec >= 0.10
, http-types >= 0.6.1 && < 0.7 , http-types >= 0.7 && < 0.8
, blaze-builder >= 0.2.1.4 && < 0.4 , blaze-builder >= 0.2.1.4 && < 0.4
, filepath >= 1.1 , filepath >= 1.1
, fast-logger >= 0.0.2 && < 0.1
, process , process
ghc-options: -Wall -threaded ghc-options: -Wall -threaded
main-is: main.hs main-is: main.hs
@ -120,6 +109,7 @@ executable yesod
Scaffolding.Scaffolder Scaffolding.Scaffolder
Devel Devel
Build Build
AddHandler
source-repository head source-repository head
type: git type: git