Merge branch 'master' of github.com:yesodweb/yesod

This commit is contained in:
Michael 2011-12-20 15:48:59 +02:00
commit 430e724eeb
159 changed files with 2825 additions and 1804 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
*.o *.o
*.o_p
*.hi *.hi
dist dist
*.swp *.swp
client_session_key.aes client_session_key.aes
cabal-dev/

View File

@ -1,5 +1,4 @@
A next generation web framework using the Haskell programming language, An advanced web framework using the Haskell programming language. Featuring:
featuring:
* safety & security guaranteed at compile time * safety & security guaranteed at compile time
* performance * performance
@ -12,25 +11,108 @@ featuring:
## Installation: http://www.yesodweb.com/page/five-minutes ## Installation: http://www.yesodweb.com/page/five-minutes
cabal update && cabal install yesod
## Create a new project after installing ## Create a new project after installing
yesod init yesod init
## Using cabal-dev
cabal-dev creates a sandboxed environment for an individual cabal package.
Your application is a cabal package and you should use cabal-dev with your Yesod application.
Instead of using the `cabal` command, use the `cabal-dev` command.
Use `yesod-devel --dev` when developing your application.
## Installing the latest development version from github ## Installing the latest development version from github
Yesod is built upon many smaller packages, all of which can be installed Yesod is broken up into 4 separate code repositories each built upon many smaller packages.
with:
Install conflicts are unfortunately common in Haskell development.
However, we can prevent most of them by using some extra tools.
This will require a little up-front reading and learning, but save you from a lot of misery in the long-run.
See the above explanation of cabal-dev, and below of virthualenv.
Please note that cabal-dev will not work in a virthualenv shell - you can't use both at the same time.
### virthualenv
To just install Yesod from github, we only need cabal-dev. However, cabal-dev may be more hassle than it is worth when hacking on Yesod.
We recommend using [virthualenv](http://hackage.haskell.org/package/virthualenv) when hacking on Yesod.
This is optional, but prevents your custom build of Yesod from interfering with your currently installed cabal packages.
virthualenv creates an isolated environment like cabal-dev.
cabal-dev isolates a single cabal package, but virthualenv isolates multiple packages together.
virthualenv works at the shell level, so every shell must activate the virthualenv.
### cabal-src
Michael just released the cabal-src tool. Whenever you would use `cabal install` for a local package, use `cabal-src-install` instead.
Our installer script now uses cabal-src-install when it is available.
### Building Yesod
~~~ { .bash } ~~~ { .bash }
# update your package database if you haven't recently
cabal update cabal update
# install required libraries
cabal install Cabal cabal-install cabal-src virthualenv
# clone and install all repos
# see below about first using virthualenv before running ./scripts/install
for repo in hamlet persistent wai yesod; do for repo in hamlet persistent wai yesod; do
git clone http://github.com/yesodweb/$repo git clone http://github.com/yesodweb/$repo
( (
cd $repo cd $repo
git submodule update --init git submodule update --init
./script/install ./scripts/install
) )
done done
~~~ ~~~
### Hacking on Yesod
To prevent Yesod from conflicting with your other installs, you should use virthualenv, although it is optional.
#### virthualenv
~~~ { .bash }
cabal update
cabal install virthualenv
cd yesodweb
virthualenv --name=yesod
. .virthualenv/bin/activate
~~~
#### individual cabal packages
~~~ { .bash }
# install and test all packages
./scripts/install
# move to the individual package you are working on
cd shakespeare-text
# build and test the individual package
cabal configure -ftest --enable-tests
cabal build
cabal test
~~~
#### cabal-dev
cabal-dev works very well if you are working on a single package, but it can be very cumbersome to work on multiple packages at once.
### Use your development version of Yesod in your application
Note that we have told you to install Yesod into a sandboxed virthualenv environment.
This means it is not available through your user/global cabal database for your application.
Instead you should use `cabal-dev install` to retrieve these packages.
cd to your application directory, and the reference the source list.
~~~ { .bash }
cabal-dev install /path/to/yesodweb/yesod/*(/)
~~~

1
input Symbolic link
View File

@ -0,0 +1 @@
yesod/input

12
package-list.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/bash
pkgs=( ./yesod-core
./yesod-json
./yesod-static
./yesod-persistent
./yesod-newsfeed
./yesod-form
./yesod-auth
./yesod-sitemap
./yesod-default
./yesod )

1
scaffold Symbolic link
View File

@ -0,0 +1 @@
yesod/scaffold

@ -1 +1 @@
Subproject commit f56426fada59012329f23c928a2d7f9c3a515d75 Subproject commit 713588bcf3526aad8a809215fb34c314334a5ffd

10
sources.txt Normal file
View File

@ -0,0 +1,10 @@
yesod-core
yesod-json
yesod-static
yesod-persistent
yesod-newsfeed
yesod-form
yesod-auth
yesod-sitemap
yesod-default
yesod

1
test/en.msg Symbolic link
View File

@ -0,0 +1 @@
../yesod-core/test/en.msg

1
test/fs Symbolic link
View File

@ -0,0 +1 @@
../yesod-static/test/fs

9
test/main.hs Normal file
View File

@ -0,0 +1,9 @@
import Test.Hspec
import qualified YesodCoreTest
import qualified YesodStaticTest
main :: IO ()
main = hspecX $ descriptions [
concat YesodCoreTest.specs
, concat YesodStaticTest.specs
]

View File

@ -34,7 +34,11 @@ import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
#if MIN_VERSION_aeson(0, 4, 0)
import qualified Data.HashMap.Lazy as Map
#else
import qualified Data.Map as Map import qualified Data.Map as Map
#endif
import Language.Haskell.TH.Syntax hiding (lift) import Language.Haskell.TH.Syntax hiding (lift)
@ -96,6 +100,11 @@ class (Yesod m, SinglePiece (AuthId m), RenderMessage m FormMessage) => YesodAut
-> AuthMessage -> Text -> AuthMessage -> Text
renderAuthMessage _ _ = defaultMessage renderAuthMessage _ _ = defaultMessage
-- | After login and logout, redirect to the referring page, instead of
-- 'loginDest' and 'logoutDest'. Default is 'False'.
redirectToReferer :: m -> Bool
redirectToReferer _ = False
mkYesodSub "Auth" mkYesodSub "Auth"
[ ClassP ''YesodAuth [VarT $ mkName "master"] [ ClassP ''YesodAuth [VarT $ mkName "master"]
] ]
@ -134,7 +143,7 @@ getCheckR = do
creds <- maybeAuthId creds <- maybeAuthId
defaultLayoutJson (do defaultLayoutJson (do
setTitle "Authentication Status" setTitle "Authentication Status"
addHtml $ html' creds) (json' creds) addHtml $ html' creds) (jsonCreds creds)
where where
html' creds = html' creds =
[QQ(shamlet)| [QQ(shamlet)|
@ -144,16 +153,21 @@ $maybe _ <- creds
$nothing $nothing
<p>Not logged in. <p>Not logged in.
|] |]
json' creds = jsonCreds creds =
Object $ Map.fromList Object $ Map.fromList
[ (T.pack "logged_in", Bool $ maybe False (const True) creds) [ (T.pack "logged_in", Bool $ maybe False (const True) creds)
] ]
setUltDestReferer' :: YesodAuth master => GHandler sub master ()
setUltDestReferer' = do
m <- getYesod
when (redirectToReferer m) setUltDestReferer
getLoginR :: YesodAuth m => GHandler Auth m RepHtml getLoginR :: YesodAuth m => GHandler Auth m RepHtml
getLoginR = setUltDestReferer >> loginHandler getLoginR = setUltDestReferer' >> loginHandler
getLogoutR :: YesodAuth m => GHandler Auth m () getLogoutR :: YesodAuth m => GHandler Auth m ()
getLogoutR = setUltDestReferer >> postLogoutR -- FIXME redirect to post getLogoutR = setUltDestReferer' >> postLogoutR -- FIXME redirect to post
postLogoutR :: YesodAuth m => GHandler Auth m () postLogoutR :: YesodAuth m => GHandler Auth m ()
postLogoutR = do postLogoutR = do

View File

@ -203,7 +203,7 @@ getPasswordR = do
Just _ -> return () Just _ -> return ()
Nothing -> do Nothing -> do
setMessageI Msg.BadSetPass setMessageI Msg.BadSetPass
redirect RedirectTemporary $ toMaster loginR redirect RedirectTemporary $ toMaster LoginR
defaultLayout $ do defaultLayout $ do
setTitleI Msg.SetPassTitle setTitleI Msg.SetPassTitle
addWidget addWidget
@ -238,7 +238,7 @@ postPasswordR = do
aid <- case maid of aid <- case maid of
Nothing -> do Nothing -> do
setMessageI Msg.BadSetPass setMessageI Msg.BadSetPass
redirect RedirectTemporary $ toMaster loginR redirect RedirectTemporary $ toMaster LoginR
Just aid -> return aid Just aid -> return aid
salted <- liftIO $ saltPass new salted <- liftIO $ saltPass new
setPassword aid salted setPassword aid salted

View File

@ -3,7 +3,10 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.Facebook module Yesod.Auth.Facebook
( authFacebook ( authFacebook
, facebookLogin
, facebookUrl , facebookUrl
, facebookLogout
, getFacebookAccessToken
) where ) where
#include "qq.h" #include "qq.h"
@ -17,20 +20,48 @@ import Data.Maybe (fromMaybe)
import Yesod.Form import Yesod.Form
import Yesod.Handler import Yesod.Handler
import Yesod.Widget import Yesod.Widget
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Class (lift)
import Data.Text (Text) import Data.Text (Text)
import Control.Monad (mzero) import Control.Monad (liftM, mzero, when)
import Data.Monoid (mappend) import Data.Monoid (mappend)
import qualified Data.Aeson.Types import qualified Data.Aeson.Types
import qualified Yesod.Auth.Message as Msg import qualified Yesod.Auth.Message as Msg
facebookUrl :: AuthRoute -- | Route for login using this authentication plugin.
facebookUrl = PluginR "facebook" ["forward"] facebookLogin :: AuthRoute
facebookLogin = PluginR "facebook" ["forward"]
-- | This is just a synonym of 'facebookLogin'. Deprecated since
-- @yesod-auth 0.7.8@, please use 'facebookLogin' instead.
facebookUrl :: AuthRoute
facebookUrl = facebookLogin
{-# DEPRECATED facebookUrl "Please use facebookLogin instead." #-}
-- | Route for logout using this authentication plugin. Per
-- Facebook's policies
-- (<https://developers.facebook.com/policy/>), the user needs to
-- logout from Facebook itself as well.
facebookLogout :: AuthRoute
facebookLogout = PluginR "facebook" ["logout"]
-- | Get Facebook's access token from the session. Returns
-- @Nothing@ if it's not found (probably because the user is not
-- logged in via Facebook). Note that the returned access token
-- may have expired.
getFacebookAccessToken :: MonadIO mo => GGHandler sub master mo (Maybe Facebook.AccessToken)
getFacebookAccessToken =
liftM (fmap Facebook.AccessToken) (lookupSession facebookAccessTokenKey)
-- | Key used to store Facebook's access token in the client
-- session.
facebookAccessTokenKey :: Text
facebookAccessTokenKey = "_FB"
-- | Authentication plugin using Facebook.
authFacebook :: YesodAuth m authFacebook :: YesodAuth m
=> Text -- ^ Application ID => Text -- ^ Application ID
-> Text -- ^ Application secret -> Text -- ^ Application secret
-> [Text] -- ^ Requested permissions -> [Text] -- ^ Requested permissions
-> AuthPlugin m -> AuthPlugin m
authFacebook cid secret perms = authFacebook cid secret perms =
@ -49,10 +80,24 @@ authFacebook cid secret perms =
code <- runInputGet $ ireq textField "code" code <- runInputGet $ ireq textField "code"
at <- liftIO $ Facebook.getAccessToken fb code at <- liftIO $ Facebook.getAccessToken fb code
let Facebook.AccessToken at' = at let Facebook.AccessToken at' = at
setSession facebookAccessTokenKey at'
so <- liftIO $ Facebook.getGraphData at "me" so <- liftIO $ Facebook.getGraphData at "me"
let c = fromMaybe (error "Invalid response from Facebook") let c = fromMaybe (error "Invalid response from Facebook")
$ parseMaybe (parseCreds at') $ either error id so $ parseMaybe (parseCreds at') $ either error id so
setCreds True c setCreds True c
dispatch "GET" ["logout"] = do
m <- getYesod
tm <- getRouteToMaster
mtoken <- getFacebookAccessToken
when (redirectToReferer m) setUltDestReferer
case mtoken of
Nothing -> do
-- Well... then just logout from our app.
redirect RedirectTemporary (tm LogoutR)
Just at -> do
render <- getUrlRender
let logout = Facebook.getLogoutUrl at (render $ tm LogoutR)
redirectText RedirectTemporary logout
dispatch _ _ = notFound dispatch _ _ = notFound
login tm = do login tm = do
render <- lift getUrlRender render <- lift getUrlRender
@ -67,8 +112,8 @@ parseCreds :: Text -> Value -> Data.Aeson.Types.Parser (Creds m)
parseCreds at' (Object m) = do parseCreds at' (Object m) = do
id' <- m .: "id" id' <- m .: "id"
let id'' = "http://graph.facebook.com/" `mappend` id' let id'' = "http://graph.facebook.com/" `mappend` id'
name <- m .: "name" name <- m .:? "name"
email <- m .: "email" email <- m .:? "email"
return return
$ Creds "facebook" id'' $ Creds "facebook" id''
$ maybe id (\x -> (:) ("verifiedEmail", x)) email $ maybe id (\x -> (:) ("verifiedEmail", x)) email

View File

@ -0,0 +1,99 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Use an email address as an identifier via Google's OpenID login system.
--
-- This backend will not use the OpenID identifier at all. It only uses OpenID
-- as a login system. By using this plugin, you are trusting Google to validate
-- an email address, and requiring users to have a Google account. On the plus
-- side, you get to use email addresses as the identifier, many users have
-- existing Google accounts, the login system has been long tested (as opposed
-- to BrowserID), and it requires no credential managing or setup (as opposed
-- to Email).
module Yesod.Auth.GoogleEmail
( authGoogleEmail
, forwardUrl
) where
import Yesod.Auth
import qualified Web.Authenticate.OpenId as OpenId
import Control.Monad.Attempt
import Yesod.Form
import Yesod.Handler
import Yesod.Widget
import Yesod.Request
import Text.Blaze (toHtml)
import Data.Text (Text)
import qualified Yesod.Auth.Message as Msg
import qualified Data.Text as T
forwardUrl :: AuthRoute
forwardUrl = PluginR "googleemail" ["forward"]
authGoogleEmail :: YesodAuth m => AuthPlugin m
authGoogleEmail =
AuthPlugin "googleemail" dispatch login
where
complete = PluginR "googleemail" ["complete"]
name = "openid_identifier"
login tm = do
[whamlet|
<form method=get action=@{tm forwardUrl}>
<input type=hidden name=openid_identifier value=https://www.google.com/accounts/o8/id>
<input type=submit value=_{Msg.LoginTitle}>
|]
dispatch "GET" ["forward"] = do
roid <- runInputGet $ iopt textField name
case roid of
Just oid -> do
render <- getUrlRender
toMaster <- getRouteToMaster
let complete' = render $ toMaster complete
res <- runAttemptT $ OpenId.getForwardUrl oid complete' Nothing
[ ("openid.ax.type.email", "http://schema.openid.net/contact/email")
, ("openid.ns.ax", "http://openid.net/srv/ax/1.0")
, ("openid.ns.ax.required", "email")
, ("openid.ax.mode", "fetch_request")
, ("openid.ax.required", "email")
, ("openid.ui.icon", "true")
]
attempt
(\err -> do
setMessage $ toHtml $ show err
redirect RedirectTemporary $ toMaster LoginR
)
(redirectText RedirectTemporary)
res
Nothing -> do
toMaster <- getRouteToMaster
setMessageI Msg.NoOpenID
redirect RedirectTemporary $ toMaster LoginR
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
dispatch "GET" ["complete"] = do
rr <- getRequest
completeHelper $ reqGetParams rr
dispatch "POST" ["complete", ""] = dispatch "POST" ["complete"] -- compatibility issues
dispatch "POST" ["complete"] = do
(posts, _) <- runRequestBody
completeHelper posts
dispatch _ _ = notFound
completeHelper :: YesodAuth m => [(Text, Text)] -> GHandler Auth m ()
completeHelper gets' = do
res <- runAttemptT $ OpenId.authenticate gets'
toMaster <- getRouteToMaster
let onFailure err = do
setMessage $ toHtml $ show err
redirect RedirectTemporary $ toMaster LoginR
let onSuccess (OpenId.Identifier ident, _) = do
memail <- lookupGetParam "openid.ext1.value.email"
case (memail, "https://www.google.com/accounts/o8/id" `T.isPrefixOf` ident) of
(Just email, True) -> setCreds True $ Creds "openid" email []
(_, False) -> do
setMessage "Only Google login is supported"
redirect RedirectTemporary $ toMaster LoginR
(Nothing, _) -> do
setMessage "No email address provided"
redirect RedirectTemporary $ toMaster LoginR
attempt onFailure onSuccess res

View File

@ -98,10 +98,18 @@ class HashDBUser user where
userPasswordHash :: user -> Maybe Text userPasswordHash :: user -> Maybe Text
-- | Retrieve salt for password -- | Retrieve salt for password
userPasswordSalt :: user -> Maybe Text userPasswordSalt :: user -> Maybe Text
-- | Set hash and password
-- | Deprecated for the better named setSaltAndPasswordHash
setUserHashAndSalt :: Text -- ^ Salt setUserHashAndSalt :: Text -- ^ Salt
-> Text -- ^ Password hash -> Text -- ^ Password hash
-> user -> user -> user -> user
setUserHashAndSalt = setSaltAndPasswordHash
-- | a callback for setPassword
setSaltAndPasswordHash :: Text -- ^ Salt
-> Text -- ^ Password hash
-> user -> user
setSaltAndPasswordHash = setUserHashAndSalt
-- | Generate random salt. Length of 8 is chosen arbitrarily -- | Generate random salt. Length of 8 is chosen arbitrarily
randomSalt :: MonadIO m => m Text randomSalt :: MonadIO m => m Text
@ -118,7 +126,7 @@ saltedHash salt =
-- passwords. It generates random salt and calculates proper hashes. -- passwords. It generates random salt and calculates proper hashes.
setPassword :: (MonadIO m, HashDBUser user) => Text -> user -> m user setPassword :: (MonadIO m, HashDBUser user) => Text -> user -> m user
setPassword pwd u = do salt <- randomSalt setPassword pwd u = do salt <- randomSalt
return $ setUserHashAndSalt salt (saltedHash salt pwd) u return $ setSaltAndPasswordHash salt (saltedHash salt pwd) u
---------------------------------------------------------------- ----------------------------------------------------------------
@ -256,6 +264,6 @@ User
instance HashDBUser (UserGeneric backend) where instance HashDBUser (UserGeneric backend) where
userPasswordHash = Just . userPassword userPasswordHash = Just . userPassword
userPasswordSalt = Just . userSalt userPasswordSalt = Just . userSalt
setUserHashAndSalt s h u = u { userSalt = s setSaltAndPasswordHash s h u = u { userSalt = s
, userPassword = h , userPassword = h
} }

View File

@ -10,6 +10,8 @@ import Data.Text (Text)
data AuthMessage = data AuthMessage =
NoOpenID NoOpenID
| LoginOpenID | LoginOpenID
| LoginGoogle
| LoginYahoo
| Email | Email
| Password | Password
| Register | Register
@ -37,6 +39,8 @@ data AuthMessage =
defaultMessage :: AuthMessage -> Text defaultMessage :: AuthMessage -> Text
defaultMessage NoOpenID = "No OpenID identifier found" defaultMessage NoOpenID = "No OpenID identifier found"
defaultMessage LoginOpenID = "Login via OpenID" defaultMessage LoginOpenID = "Login via OpenID"
defaultMessage LoginGoogle = "Login via Google"
defaultMessage LoginYahoo = "Login via Yahoo"
defaultMessage Email = "Email" defaultMessage Email = "Email"
defaultMessage Password = "Password" defaultMessage Password = "Password"
defaultMessage Register = "Register" defaultMessage Register = "Register"

View File

@ -3,6 +3,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Yesod.Auth.OpenId module Yesod.Auth.OpenId
( authOpenId ( authOpenId
, authOpenIdExtended
, forwardUrl , forwardUrl
) where ) where
@ -26,7 +27,10 @@ forwardUrl :: AuthRoute
forwardUrl = PluginR "openid" ["forward"] forwardUrl = PluginR "openid" ["forward"]
authOpenId :: YesodAuth m => AuthPlugin m authOpenId :: YesodAuth m => AuthPlugin m
authOpenId = authOpenId = authOpenIdExtended []
authOpenIdExtended :: YesodAuth m => [(Text, Text)] -> AuthPlugin m
authOpenIdExtended extensionFields =
AuthPlugin "openid" dispatch login AuthPlugin "openid" dispatch login
where where
complete = PluginR "openid" ["complete"] complete = PluginR "openid" ["complete"]
@ -39,6 +43,12 @@ authOpenId =
padding-left: 18px; padding-left: 18px;
|] |]
[QQ(whamlet)| [QQ(whamlet)|
<form method="get" action="@{tm forwardUrl}">
<input type="hidden" name="openid_identifier" value="https://www.google.com/accounts/o8/id">
<button .openid-google>_{Msg.LoginGoogle}
<form method="get" action="@{tm forwardUrl}">
<input type="hidden" name="openid_identifier" value="http://me.yahoo.com">
<button .openid-yahoo>_{Msg.LoginYahoo}
<form method="get" action="@{tm forwardUrl}"> <form method="get" action="@{tm forwardUrl}">
<label for="#{ident}">OpenID: # <label for="#{ident}">OpenID: #
<input id="#{ident}" type="text" name="#{name}" value="http://"> <input id="#{ident}" type="text" name="#{name}" value="http://">
@ -51,7 +61,7 @@ authOpenId =
render <- getUrlRender render <- getUrlRender
toMaster <- getRouteToMaster toMaster <- getRouteToMaster
let complete' = render $ toMaster complete let complete' = render $ toMaster complete
res <- runAttemptT $ OpenId.getForwardUrl oid complete' Nothing [] res <- runAttemptT $ OpenId.getForwardUrl oid complete' Nothing extensionFields
attempt attempt
(\err -> do (\err -> do
setMessage $ toHtml $ show err setMessage $ toHtml $ show err
@ -81,5 +91,5 @@ completeHelper gets' = do
setMessage $ toHtml $ show err setMessage $ toHtml $ show err
redirect RedirectTemporary $ toMaster LoginR redirect RedirectTemporary $ toMaster LoginR
let onSuccess (OpenId.Identifier ident, _) = let onSuccess (OpenId.Identifier ident, _) =
setCreds True $ Creds "openid" ident [] setCreds True $ Creds "openid" ident gets'
attempt onFailure onSuccess res attempt onFailure onSuccess res

View File

@ -10,7 +10,6 @@ import Web.Authenticate.Facebook
import Yesod.Form import Yesod.Form
data FB = FB Facebook data FB = FB Facebook
type Handler = GHandler FB FB
fb :: FB fb :: FB
fb = FB Facebook fb = FB Facebook

54
yesod-auth/openid.hs Normal file
View File

@ -0,0 +1,54 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Yesod.Core
import Yesod.Auth
import Yesod.Auth.OpenId
import Data.Text (Text)
import Text.Hamlet (hamlet)
import Control.Monad.IO.Class (liftIO)
import Yesod.Form
import Network.Wai.Handler.Warp (run)
data BID = BID
mkYesod "BID" [parseRoutes|
/ RootR GET
/after AfterLoginR GET
/auth AuthR Auth getAuth
|]
getRootR :: Handler RepHtml
getRootR = getAfterLoginR
getAfterLoginR :: Handler RepHtml
getAfterLoginR = do
mauth <- maybeAuthId
defaultLayout $ addHamlet [hamlet|
<p>Auth: #{show mauth}
$maybe _ <- mauth
<p>
<a href=@{AuthR LogoutR}>Logout
$nothing
<p>
<a href=@{AuthR LoginR}>Login
|]
instance Yesod BID where
approot _ = "http://localhost:3000"
instance YesodAuth BID where
type AuthId BID = Text
loginDest _ = AfterLoginR
logoutDest _ = AuthR LoginR
getAuthId = return . Just . credsIdent
authPlugins = [authOpenId]
instance RenderMessage BID FormMessage where
renderMessage _ _ = defaultFormMessage
main :: IO ()
main = toWaiApp BID >>= run 3000

View File

@ -1,5 +1,5 @@
name: yesod-auth name: yesod-auth
version: 0.7.3 version: 0.7.8
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman, Patrick Brisbin author: Michael Snoyman, Patrick Brisbin
@ -17,33 +17,34 @@ flag ghc7
library library
if flag(ghc7) if flag(ghc7)
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.3 && < 5
cpp-options: -DGHC7 cpp-options: -DGHC7
else else
build-depends: base >= 4 && < 4.3 build-depends: base >= 4 && < 4.3
build-depends: authenticate >= 0.10 && < 0.11 build-depends: authenticate >= 0.10.4 && < 0.11
, bytestring >= 0.9.1.4 && < 0.10 , bytestring >= 0.9.1.4 && < 0.10
, yesod-core >= 0.9 && < 0.10 , yesod-core >= 0.9.3.4 && < 0.10
, wai >= 0.4 && < 0.5 , wai >= 0.4 && < 0.5
, template-haskell , template-haskell
, pureMD5 >= 1.1 && < 2.2 , pureMD5 >= 2.0 && < 2.2
, random >= 1.0 && < 1.1 , random >= 1.0.0.2 && < 1.1
, control-monad-attempt >= 0.3.0 && < 0.4 , control-monad-attempt >= 0.3.0 && < 0.4
, text >= 0.7 && < 0.12 , text >= 0.7 && < 0.12
, mime-mail >= 0.3 && < 0.4 , mime-mail >= 0.3 && < 0.5
, blaze-html >= 0.4 && < 0.5 , blaze-html >= 0.4.1.3 && < 0.5
, yesod-persistent >= 0.2 && < 0.3 , yesod-persistent >= 0.2 && < 0.3
, hamlet >= 0.10 && < 0.11 , hamlet >= 0.10 && < 0.11
, shakespeare-css >= 0.10 && < 0.11 , shakespeare-css >= 0.10 && < 0.11
, yesod-json >= 0.2 && < 0.3 , yesod-json >= 0.2 && < 0.3
, containers >= 0.2 && < 0.5 , containers
, unordered-containers
, yesod-form >= 0.3 && < 0.4 , yesod-form >= 0.3 && < 0.4
, transformers >= 0.2 && < 0.3 , transformers >= 0.2.2 && < 0.3
, persistent >= 0.6 && < 0.7 , persistent >= 0.6 && < 0.7
, persistent-template >= 0.6 && < 0.7 , persistent-template >= 0.6 && < 0.7
, SHA >= 1.4.1.3 && < 1.6 , SHA >= 1.4.1.3 && < 1.6
, http-enumerator >= 0.6 && < 0.8 , http-enumerator >= 0.6 && < 0.8
, aeson-native >= 0.3.2.11 && < 0.4 , aeson >= 0.3
, pwstore-fast >= 2.2 && < 3 , pwstore-fast >= 2.2 && < 3
exposed-modules: Yesod.Auth exposed-modules: Yesod.Auth
@ -57,9 +58,10 @@ library
Yesod.Auth.HashDB Yesod.Auth.HashDB
Yesod.Auth.Message Yesod.Auth.Message
Yesod.Auth.Kerberos Yesod.Auth.Kerberos
Yesod.Auth.GoogleEmail
ghc-options: -Wall ghc-options: -Wall
include-dirs: include include-dirs: include
source-repository head source-repository head
type: git type: git
location: git://github.com/snoyberg/yesod-auth.git location: git://github.com/yesodweb/yesod.git

113
yesod-core/Yesod/Config.hs Normal file
View File

@ -0,0 +1,113 @@
{-# LANGUAGE OverloadedStrings #-}
module Yesod.Config
{-# DEPRECATED "This code has been moved to yesod-default. This module will be removed in the next major version bump." #-}
( AppConfig(..)
, loadConfig
, withYamlEnvironment
) where
import Control.Monad (join)
import Data.Maybe (fromMaybe)
import Data.Object
import Data.Object.Yaml
import Data.Text (Text)
import qualified Data.Text as T
-- | Dynamic per-environment configuration which can be loaded at
-- run-time negating the need to recompile between environments.
data AppConfig e = AppConfig
{ appEnv :: e
, appPort :: Int
, appRoot :: Text
} deriving (Show)
-- | Load an @'AppConfig'@ from @config\/settings.yml@.
--
-- Some examples:
--
-- > -- typical local development
-- > Development:
-- > host: localhost
-- > port: 3000
-- >
-- > -- ssl: will default false
-- > -- approot: will default to "http://localhost:3000"
--
-- > -- typical outward-facing production box
-- > Production:
-- > host: www.example.com
-- >
-- > -- ssl: will default false
-- > -- port: will default 80
-- > -- approot: will default "http://www.example.com"
--
-- > -- maybe you're reverse proxying connections to the running app
-- > -- on some other port
-- > Production:
-- > port: 8080
-- > approot: "http://example.com"
-- >
-- > -- approot is specified so that the non-80 port is not appended
-- > -- automatically.
--
loadConfig :: Show e => e -> IO (AppConfig e)
loadConfig env = withYamlEnvironment "config/settings.yml" env $ \e' -> do
e <- maybe (fail "Expected map") return $ fromMapping e'
let mssl = lookupScalar "ssl" e
let mhost = lookupScalar "host" e
let mport = lookupScalar "port" e
let mapproot = lookupScalar "approot" e
-- set some default arguments
let ssl = maybe False toBool mssl
port <- safeRead "port" $ fromMaybe (if ssl then "443" else "80") mport
approot <- case (mhost, mapproot) of
(_ , Just ar) -> return ar
(Just host, _ ) -> return $ T.concat
[ if ssl then "https://" else "http://"
, host
, addPort ssl port
]
_ -> fail "You must supply either a host or approot"
return $ AppConfig
{ appEnv = env
, appPort = port
, appRoot = approot
}
where
toBool :: Text -> Bool
toBool = (`elem` ["true", "TRUE", "yes", "YES", "Y", "1"])
addPort :: Bool -> Int -> Text
addPort True 443 = ""
addPort False 80 = ""
addPort _ p = T.pack $ ':' : show p
-- | Loads the configuration block in the passed file named by the
-- passed environment, yeilds to the passed function as a mapping.
--
-- Errors in the case of a bad load or if your function returns
-- @Nothing@.
withYamlEnvironment :: Show e
=> FilePath -- ^ the yaml file
-> e -- ^ the environment you want to load
-> (TextObject -> IO a) -- ^ what to do with the mapping
-> IO a
withYamlEnvironment fp env f = do
obj <- join $ decodeFile fp
envs <- fromMapping obj
conf <- maybe (fail $ "Could not find environment: " ++ show env) return
$ lookup (T.pack $ show env) envs
f conf
-- | Returns 'fail' if read fails
safeRead :: Monad m => String -> Text -> m Int
safeRead name t = case reads s of
(i, _):_ -> return i
[] -> fail $ concat ["Invalid value for ", name, ": ", s]
where
s = T.unpack t

View File

@ -1,4 +1,5 @@
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-warnings-deprecations #-}
module Yesod.Core module Yesod.Core
( -- * Type classes ( -- * Type classes
Yesod (..) Yesod (..)
@ -33,6 +34,7 @@ module Yesod.Core
, module Yesod.Request , module Yesod.Request
, module Yesod.Widget , module Yesod.Widget
, module Yesod.Message , module Yesod.Message
, module Yesod.Config
) where ) where
import Yesod.Internal.Core import Yesod.Internal.Core
@ -42,6 +44,7 @@ import Yesod.Handler
import Yesod.Request import Yesod.Request
import Yesod.Widget import Yesod.Widget
import Yesod.Message import Yesod.Message
import Yesod.Config
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import Data.Text (Text) import Data.Text (Text)

View File

@ -4,7 +4,9 @@
module Yesod.Dispatch module Yesod.Dispatch
( -- * Quasi-quoted routing ( -- * Quasi-quoted routing
parseRoutes parseRoutes
, parseRoutesNoCheck
, parseRoutesFile , parseRoutesFile
, parseRoutesFileNoCheck
, mkYesod , mkYesod
, mkYesodSub , mkYesodSub
-- ** More fine-grained -- ** More fine-grained
@ -30,11 +32,10 @@ import Yesod.Internal.Dispatch
import Yesod.Widget (GWidget) import Yesod.Widget (GWidget)
import Web.PathPieces (SinglePiece (..), MultiPiece (..)) import Web.PathPieces (SinglePiece (..), MultiPiece (..))
import Yesod.Internal.RouteParsing (THResource, Pieces (..), createRoutes, createRender, Resource (..), parseRoutes, parseRoutesFile) import Yesod.Internal.RouteParsing (THResource, Pieces (..), createRoutes, createRender, Resource (..), parseRoutes, parseRoutesNoCheck, parseRoutesFile, parseRoutesFileNoCheck)
import Language.Haskell.TH.Syntax import Language.Haskell.TH.Syntax
import qualified Network.Wai as W import qualified Network.Wai as W
import Network.Wai.Middleware.Jsonp
import Network.Wai.Middleware.Gzip import Network.Wai.Middleware.Gzip
import Network.Wai.Middleware.Autohead import Network.Wai.Middleware.Autohead
@ -172,11 +173,11 @@ thResourceFromResource (Resource n _ _) =
error $ "Invalid attributes for resource: " ++ n error $ "Invalid attributes for resource: " ++ n
-- | Convert the given argument into a WAI application, executable with any WAI -- | Convert the given argument into a WAI application, executable with any WAI
-- handler. This is the same as 'toWaiAppPlain', except it includes three -- handler. This is the same as 'toWaiAppPlain', except it includes two
-- middlewares: GZIP compression, JSON-P and autohead. This is the -- middlewares: GZIP compression and autohead. This is the
-- recommended approach for most users. -- recommended approach for most users.
toWaiApp :: (Yesod y, YesodDispatch y y) => y -> IO W.Application toWaiApp :: (Yesod y, YesodDispatch y y) => y -> IO W.Application
toWaiApp y = gzip (gzipCompressFiles y) . jsonp . autohead <$> toWaiAppPlain y toWaiApp y = gzip (gzipCompressFiles y) . autohead <$> toWaiAppPlain y
-- | Convert the given argument into a WAI application, executable with any WAI -- | Convert the given argument into a WAI application, executable with any WAI
-- handler. This differs from 'toWaiApp' in that it uses no middlewares. -- handler. This differs from 'toWaiApp' in that it uses no middlewares.

View File

@ -8,6 +8,7 @@
{-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE DeriveDataTypeable #-}
--------------------------------------------------------- ---------------------------------------------------------
-- --
-- Module : Yesod.Handler -- Module : Yesod.Handler
@ -97,6 +98,12 @@ module Yesod.Handler
, liftIOHandler , liftIOHandler
-- * i18n -- * i18n
, getMessageRender , getMessageRender
-- * Per-request caching
, CacheKey
, mkCacheKey
, cacheLookup
, cacheInsert
, cacheDelete
-- * Internal Yesod -- * Internal Yesod
, runHandler , runHandler
, YesodApp (..) , YesodApp (..)
@ -119,17 +126,13 @@ import Yesod.Internal
import Data.Time (UTCTime) import Data.Time (UTCTime)
import Control.Exception hiding (Handler, catch, finally) import Control.Exception hiding (Handler, catch, finally)
import qualified Control.Exception as E
import Control.Applicative import Control.Applicative
import Control.Monad (liftM, join, MonadPlus) import Control.Monad (liftM)
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Trans.Class import Control.Monad.Trans.Class
import Control.Monad.Trans.Writer
import Control.Monad.Trans.Reader import Control.Monad.Trans.Reader
import Control.Monad.Trans.State
import Control.Monad.Trans.Error (throwError, ErrorT (..), Error (..))
import System.IO import System.IO
import qualified Network.Wai as W import qualified Network.Wai as W
@ -143,8 +146,6 @@ import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode) import Data.Text.Encoding.Error (lenientDecode)
import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy as TL
import Control.Monad.IO.Control (MonadControlIO)
import Control.Monad.Trans.Control (MonadTransControl, liftControl)
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.ByteString as S import qualified Data.ByteString as S
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
@ -154,7 +155,7 @@ import Network.Wai.Parse (parseHttpAccept)
import Yesod.Content import Yesod.Content
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Web.Cookie (SetCookie (..), renderSetCookie) import Web.Cookie (SetCookie (..), renderSetCookie)
import Control.Arrow (second, (***)) 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
@ -164,6 +165,12 @@ import Data.Text (Text)
import Yesod.Message (RenderMessage (..)) import Yesod.Message (RenderMessage (..))
import Text.Blaze (toHtml, preEscapedText) import Text.Blaze (toHtml, preEscapedText)
import Yesod.Internal.TestApi (catchIter)
import qualified Yesod.Internal.Cache as Cache
import Yesod.Internal.Cache (mkCacheKey, CacheKey)
import Data.Typeable (Typeable)
import qualified Data.IORef as I
-- | The type-safe URLs associated with a site argument. -- | The type-safe URLs associated with a site argument.
type family Route a type family Route a
@ -178,6 +185,7 @@ data HandlerData sub master = HandlerData
, handlerRoute :: Maybe (Route sub) , handlerRoute :: Maybe (Route sub)
, 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
} }
handlerSubData :: (Route sub -> Route master) handlerSubData :: (Route sub -> Route master)
@ -198,6 +206,24 @@ handlerSubDataMaybe tm ts route hd = hd
, handlerRoute = route , handlerRoute = route
} }
get :: MonadIO monad => GGHandler sub master monad GHState
get = do
hd <- ask
liftIO $ I.readIORef $ handlerState hd
put :: MonadIO monad => GHState -> GGHandler sub master monad ()
put g = do
hd <- ask
liftIO $ I.writeIORef (handlerState hd) g
modify :: MonadIO monad => (GHState -> GHState) -> GGHandler sub master monad ()
modify f = do
hd <- ask
liftIO $ I.atomicModifyIORef (handlerState hd) $ \g -> (f g, ())
tell :: MonadIO monad => Endo [Header] -> GGHandler sub master monad ()
tell hs = modify $ \g -> g { ghsHeaders = ghsHeaders g `mappend` hs }
-- | Used internally for promoting subsite handler functions to master site -- | Used internally for promoting subsite handler functions to master site
-- handler functions. Should not be needed by users. -- handler functions. Should not be needed by users.
toMasterHandler :: (Route sub -> Route master) toMasterHandler :: (Route sub -> Route master)
@ -205,8 +231,7 @@ toMasterHandler :: (Route sub -> Route master)
-> Route sub -> Route sub
-> GGHandler sub master mo a -> GGHandler sub master mo a
-> GGHandler sub' master mo a -> GGHandler sub' master mo a
toMasterHandler tm ts route (GHandler h) = toMasterHandler tm ts route = withReaderT (handlerSubData tm ts route)
GHandler $ withReaderT (handlerSubData tm ts route) h
toMasterHandlerDyn :: Monad mo toMasterHandlerDyn :: Monad mo
=> (Route sub -> Route master) => (Route sub -> Route master)
@ -214,9 +239,9 @@ toMasterHandlerDyn :: Monad mo
-> Route sub -> Route sub
-> GGHandler sub master mo a -> GGHandler sub master mo a
-> GGHandler sub' master mo a -> GGHandler sub' master mo a
toMasterHandlerDyn tm getSub route (GHandler h) = do toMasterHandlerDyn tm getSub route h = do
sub <- getSub sub <- getSub
GHandler $ withReaderT (handlerSubData tm (const sub) route) h withReaderT (handlerSubData tm (const sub) route) h
class SubsiteGetter g m s | g -> s where class SubsiteGetter g m s | g -> s where
runSubsiteGetter :: g -> m s runSubsiteGetter :: g -> m s
@ -235,22 +260,14 @@ toMasterHandlerMaybe :: (Route sub -> Route master)
-> Maybe (Route sub) -> Maybe (Route sub)
-> GGHandler sub master mo a -> GGHandler sub master mo a
-> GGHandler sub' master mo a -> GGHandler sub' master mo a
toMasterHandlerMaybe tm ts route (GHandler h) = toMasterHandlerMaybe tm ts route = withReaderT (handlerSubDataMaybe tm ts route)
GHandler $ withReaderT (handlerSubDataMaybe tm ts route) h
-- | A generic handler monad, which can have a different subsite and master -- | A generic handler monad, which can have a different subsite and master
-- site. This monad is a combination of 'ReaderT' for basic arguments, a -- site. This monad is a combination of 'ReaderT' for basic arguments, a
-- 'WriterT' for headers and session, and an 'MEitherT' monad for handling -- 'WriterT' for headers and session, and an 'MEitherT' monad for handling
-- special responses. It is declared as a newtype to make compiler errors more -- special responses. It is declared as a newtype to make compiler errors more
-- readable. -- readable.
newtype GGHandler sub master m a = type GGHandler sub master = ReaderT (HandlerData sub master)
GHandler
{ unGHandler :: GHInner sub master m a
}
deriving (Functor, Applicative, Monad, MonadIO, MonadControlIO, MonadPlus)
instance MonadTrans (GGHandler s m) where
lift = GHandler . lift . lift . lift . lift
type GHandler sub master = GGHandler sub master (Iteratee ByteString IO) type GHandler sub master = GGHandler sub master (Iteratee ByteString IO)
@ -258,16 +275,10 @@ data GHState = GHState
{ ghsSession :: SessionMap { ghsSession :: SessionMap
, ghsRBC :: Maybe RequestBodyContents , ghsRBC :: Maybe RequestBodyContents
, ghsIdent :: Int , ghsIdent :: Int
, ghsCache :: Cache.Cache
, ghsHeaders :: Endo [Header]
} }
type GHInner s m monad = -- FIXME collapse the stack
ReaderT (HandlerData s m) (
ErrorT HandlerContents (
WriterT (Endo [Header]) (
StateT GHState (
monad
))))
type SessionMap = Map.Map Text Text type SessionMap = Map.Map Text Text
-- | An extension of the basic WAI 'W.Application' datatype to provide extra -- | An extension of the basic WAI 'W.Application' datatype to provide extra
@ -293,25 +304,27 @@ data HandlerContents =
| HCRedirect RedirectType Text | HCRedirect RedirectType Text
| HCCreated Text | HCCreated Text
| HCWai W.Response | HCWai W.Response
deriving Typeable
instance Error HandlerContents where instance Show HandlerContents where
strMsg = HCError . InternalError . T.pack show _ = "Cannot show a HandlerContents"
instance Exception HandlerContents
getRequest :: Monad mo => GGHandler s m mo Request getRequest :: Monad mo => GGHandler s m mo Request
getRequest = handlerRequest `liftM` GHandler ask getRequest = handlerRequest `liftM` ask
instance Monad monad => Failure ErrorResponse (GGHandler sub master monad) where instance MonadIO monad => Failure ErrorResponse (GGHandler sub master monad) where
failure = GHandler . lift . throwError . HCError failure = liftIO . throwIO . HCError
runRequestBody :: GHandler s m RequestBodyContents runRequestBody :: GHandler s m RequestBodyContents
runRequestBody = do runRequestBody = do
x <- GHandler $ lift $ lift $ lift 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 rr
GHandler $ lift $ lift $ lift $ put x { ghsRBC = Just rbc } put x { ghsRBC = Just rbc }
return rbc return rbc
rbHelper :: W.Request -> Iteratee ByteString IO RequestBodyContents rbHelper :: W.Request -> Iteratee ByteString IO RequestBodyContents
@ -326,33 +339,33 @@ rbHelper req =
-- | Get the sub application argument. -- | Get the sub application argument.
getYesodSub :: Monad m => GGHandler sub master m sub getYesodSub :: Monad m => GGHandler sub master m sub
getYesodSub = handlerSub `liftM` GHandler ask getYesodSub = handlerSub `liftM` ask
-- | Get the master site appliation argument. -- | Get the master site appliation argument.
getYesod :: Monad m => GGHandler sub master m master getYesod :: Monad m => GGHandler sub master m master
getYesod = handlerMaster `liftM` GHandler ask getYesod = handlerMaster `liftM` ask
-- | Get the URL rendering function. -- | Get the URL rendering function.
getUrlRender :: Monad m => GGHandler sub master m (Route master -> Text) getUrlRender :: Monad m => GGHandler sub master m (Route master -> Text)
getUrlRender = do getUrlRender = do
x <- handlerRender `liftM` GHandler ask x <- handlerRender `liftM` ask
return $ flip x [] return $ flip x []
-- | The URL rendering function with query-string parameters. -- | The URL rendering function with query-string parameters.
getUrlRenderParams getUrlRenderParams
:: Monad m :: Monad m
=> GGHandler sub master m (Route master -> [(Text, Text)] -> Text) => GGHandler sub master m (Route master -> [(Text, Text)] -> Text)
getUrlRenderParams = handlerRender `liftM` GHandler ask getUrlRenderParams = handlerRender `liftM` ask
-- | Get the route requested by the user. If this is a 404 response- where the -- | Get the route requested by the user. If this is a 404 response- where the
-- user requested an invalid route- this function will return 'Nothing'. -- user requested an invalid route- this function will return 'Nothing'.
getCurrentRoute :: Monad m => GGHandler sub master m (Maybe (Route sub)) getCurrentRoute :: Monad m => GGHandler sub master m (Maybe (Route sub))
getCurrentRoute = handlerRoute `liftM` GHandler ask getCurrentRoute = handlerRoute `liftM` ask
-- | Get the function to promote a route for a subsite to a route for the -- | Get the function to promote a route for a subsite to a route for the
-- master site. -- master site.
getRouteToMaster :: Monad m => GGHandler sub master m (Route sub -> Route master) getRouteToMaster :: Monad m => GGHandler sub master m (Route sub -> Route master)
getRouteToMaster = handlerToMaster `liftM` GHandler ask getRouteToMaster = handlerToMaster `liftM` ask
-- | Function used internally by Yesod in the process of converting a -- | Function used internally by Yesod in the process of converting a
-- 'GHandler' into an 'W.Application'. Should not be needed by users. -- 'GHandler' into an 'W.Application'. Should not be needed by users.
@ -370,6 +383,13 @@ runHandler handler mrender sroute tomr ma sa =
case fromException e of case fromException e of
Just x -> x Just x -> x
Nothing -> InternalError $ T.pack $ show e Nothing -> InternalError $ T.pack $ show e
istate <- liftIO $ I.newIORef GHState
{ ghsSession = initSession
, ghsRBC = Nothing
, ghsIdent = 1
, ghsCache = mempty
, ghsHeaders = mempty
}
let hd = HandlerData let hd = HandlerData
{ handlerRequest = rr { handlerRequest = rr
, handlerSub = sa , handlerSub = sa
@ -377,16 +397,14 @@ runHandler handler mrender sroute tomr ma sa =
, handlerRoute = sroute , handlerRoute = sroute
, handlerRender = mrender , handlerRender = mrender
, handlerToMaster = tomr , handlerToMaster = tomr
, handlerState = istate
} }
let initSession' = GHState initSession Nothing 1 contents' <- catchIter (fmap Right $ runReaderT handler hd)
((contents', headers), finalSession) <- catchIter ( (\e -> return $ Left $ maybe (HCError $ toErrorHandler e) id
fmap (second ghsSession) $ fromException e)
$ flip runStateT initSession' state <- liftIO $ I.readIORef istate
$ runWriterT let finalSession = ghsSession state
$ runErrorT let headers = ghsHeaders state
$ flip runReaderT hd
$ unGHandler handler
) (\e -> return ((Left $ HCError $ toErrorHandler e, mempty), initSession))
let contents = either id (HCContent H.status200 . chooseRep) contents' let contents = either id (HCContent H.status200 . chooseRep) contents'
let handleError e = do let handleError e = do
yar <- unYesodApp (eh e) safeEh rr cts finalSession yar <- unYesodApp (eh e) safeEh rr cts finalSession
@ -420,12 +438,6 @@ runHandler handler mrender sroute tomr ma sa =
finalSession finalSession
HCWai r -> return $ YARWai r HCWai r -> return $ YARWai r
catchIter :: Exception e
=> Iteratee ByteString IO a
-> (e -> Iteratee ByteString IO a)
-> Iteratee ByteString IO a
catchIter (Iteratee mstep) f = Iteratee $ mstep `E.catch` (runIteratee . f)
safeEh :: ErrorResponse -> YesodApp safeEh :: ErrorResponse -> YesodApp
safeEh er = YesodApp $ \_ _ _ session -> do safeEh er = YesodApp $ \_ _ _ session -> do
liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er
@ -437,11 +449,11 @@ safeEh er = YesodApp $ \_ _ _ session -> do
session session
-- | Redirect to the given route. -- | Redirect to the given route.
redirect :: Monad mo => RedirectType -> Route master -> GGHandler sub master mo a redirect :: MonadIO mo => RedirectType -> Route master -> GGHandler sub master mo a
redirect rt url = redirectParams rt url [] redirect rt url = redirectParams rt url []
-- | Redirects to the given route with the associated query-string parameters. -- | Redirects to the given route with the associated query-string parameters.
redirectParams :: Monad mo redirectParams :: MonadIO mo
=> RedirectType -> Route master -> [(Text, Text)] => RedirectType -> Route master -> [(Text, Text)]
-> GGHandler sub master mo a -> GGHandler sub master mo a
redirectParams rt url params = do redirectParams rt url params = do
@ -449,8 +461,8 @@ redirectParams rt url params = do
redirectString rt $ r url params redirectString rt $ r url params
-- | Redirect to the given URL. -- | Redirect to the given URL.
redirectString, redirectText :: Monad mo => RedirectType -> Text -> GGHandler sub master mo a redirectString, redirectText :: MonadIO mo => RedirectType -> Text -> GGHandler sub master mo a
redirectText rt = GHandler . lift . throwError . HCRedirect rt redirectText rt = liftIO . throwIO . HCRedirect rt
redirectString = redirectText redirectString = redirectText
{-# DEPRECATED redirectString "Use redirectText instead" #-} {-# DEPRECATED redirectString "Use redirectText instead" #-}
@ -461,16 +473,16 @@ ultDestKey = "_ULT"
-- --
-- An ultimate destination is stored in the user session and can be loaded -- An ultimate destination is stored in the user session and can be loaded
-- later by 'redirectUltDest'. -- later by 'redirectUltDest'.
setUltDest :: Monad mo => Route master -> GGHandler sub master mo () setUltDest :: MonadIO mo => Route master -> GGHandler sub master mo ()
setUltDest dest = do setUltDest dest = do
render <- getUrlRender render <- getUrlRender
setUltDestString $ render dest setUltDestString $ render dest
-- | Same as 'setUltDest', but use the given string. -- | Same as 'setUltDest', but use the given string.
setUltDestText :: Monad mo => Text -> GGHandler sub master mo () setUltDestText :: MonadIO mo => Text -> GGHandler sub master mo ()
setUltDestText = setSession ultDestKey setUltDestText = setSession ultDestKey
setUltDestString :: Monad mo => Text -> GGHandler sub master mo () setUltDestString :: MonadIO mo => Text -> GGHandler sub master mo ()
setUltDestString = setSession ultDestKey setUltDestString = setSession ultDestKey
{-# DEPRECATED setUltDestString "Use setUltDestText instead" #-} {-# DEPRECATED setUltDestString "Use setUltDestText instead" #-}
@ -478,21 +490,21 @@ setUltDestString = setSession ultDestKey
-- --
-- If this is a 404 handler, there is no current page, and then this call does -- If this is a 404 handler, there is no current page, and then this call does
-- nothing. -- nothing.
setUltDest' :: Monad mo => GGHandler sub master mo () setUltDest' :: MonadIO mo => GGHandler sub master mo ()
setUltDest' = do setUltDest' = do
route <- getCurrentRoute route <- getCurrentRoute
case route of case route of
Nothing -> return () Nothing -> return ()
Just r -> do Just r -> do
tm <- getRouteToMaster tm <- getRouteToMaster
gets' <- reqGetParams `liftM` handlerRequest `liftM` GHandler ask gets' <- reqGetParams `liftM` handlerRequest `liftM` ask
render <- getUrlRenderParams render <- getUrlRenderParams
setUltDestString $ render (tm r) gets' setUltDestString $ render (tm r) gets'
-- | Sets the ultimate destination to the referer request header, if present. -- | Sets the ultimate destination to the referer request header, if present.
-- --
-- This function will not overwrite an existing ultdest. -- This function will not overwrite an existing ultdest.
setUltDestReferer :: Monad mo => GGHandler sub master mo () setUltDestReferer :: MonadIO mo => GGHandler sub master mo ()
setUltDestReferer = do setUltDestReferer = do
mdest <- lookupSession ultDestKey mdest <- lookupSession ultDestKey
maybe maybe
@ -506,7 +518,7 @@ setUltDestReferer = do
-- value from the session. -- value from the session.
-- --
-- The ultimate destination is set with 'setUltDest'. -- The ultimate destination is set with 'setUltDest'.
redirectUltDest :: Monad mo redirectUltDest :: MonadIO mo
=> RedirectType => RedirectType
-> Route master -- ^ default destination if nothing in session -> Route master -- ^ default destination if nothing in session
-> GGHandler sub master mo a -> GGHandler sub master mo a
@ -516,7 +528,7 @@ redirectUltDest rt def = do
maybe (redirect rt def) (redirectText rt) mdest maybe (redirect rt def) (redirectText rt) mdest
-- | Remove a previously set ultimate destination. See 'setUltDest'. -- | Remove a previously set ultimate destination. See 'setUltDest'.
clearUltDest :: Monad mo => GGHandler sub master mo () clearUltDest :: MonadIO mo => GGHandler sub master mo ()
clearUltDest = deleteSession ultDestKey clearUltDest = deleteSession ultDestKey
msgKey :: Text msgKey :: Text
@ -525,13 +537,13 @@ msgKey = "_MSG"
-- | Sets a message in the user's session. -- | Sets a message in the user's session.
-- --
-- See 'getMessage'. -- See 'getMessage'.
setMessage :: Monad mo => Html -> GGHandler sub master mo () setMessage :: MonadIO mo => Html -> GGHandler sub master mo ()
setMessage = setSession msgKey . T.concat . TL.toChunks . Text.Blaze.Renderer.Text.renderHtml setMessage = setSession msgKey . T.concat . TL.toChunks . Text.Blaze.Renderer.Text.renderHtml
-- | Sets a message in the user's session. -- | Sets a message in the user's session.
-- --
-- See 'getMessage'. -- See 'getMessage'.
setMessageI :: (RenderMessage y msg, Monad mo) => msg -> GGHandler sub y mo () setMessageI :: (RenderMessage y msg, MonadIO mo) => msg -> GGHandler sub y mo ()
setMessageI msg = do setMessageI msg = do
mr <- getMessageRender mr <- getMessageRender
setMessage $ toHtml $ mr msg setMessage $ toHtml $ mr msg
@ -540,7 +552,7 @@ setMessageI msg = do
-- variable. -- variable.
-- --
-- See 'setMessage'. -- See 'setMessage'.
getMessage :: Monad mo => GGHandler sub master mo (Maybe Html) getMessage :: MonadIO mo => GGHandler sub master mo (Maybe Html)
getMessage = do getMessage = do
mmsg <- liftM (fmap preEscapedText) $ lookupSession msgKey mmsg <- liftM (fmap preEscapedText) $ lookupSession msgKey
deleteSession msgKey deleteSession msgKey
@ -550,52 +562,52 @@ getMessage = do
-- --
-- For some backends, this is more efficient than reading in the file to -- For some backends, this is more efficient than reading in the file to
-- memory, since they can optimize file sending via a system call to sendfile. -- memory, since they can optimize file sending via a system call to sendfile.
sendFile :: Monad mo => ContentType -> FilePath -> GGHandler sub master mo a sendFile :: MonadIO mo => ContentType -> FilePath -> GGHandler sub master mo a
sendFile ct fp = GHandler . lift . throwError $ HCSendFile ct fp Nothing sendFile ct fp = liftIO . throwIO $ HCSendFile ct fp Nothing
-- | Same as 'sendFile', but only sends part of a file. -- | Same as 'sendFile', but only sends part of a file.
sendFilePart :: Monad mo sendFilePart :: MonadIO mo
=> ContentType => ContentType
-> FilePath -> FilePath
-> Integer -- ^ offset -> Integer -- ^ offset
-> Integer -- ^ count -> Integer -- ^ count
-> GGHandler sub master mo a -> GGHandler sub master mo a
sendFilePart ct fp off count = sendFilePart ct fp off count =
GHandler . lift . throwError $ HCSendFile ct fp $ Just $ W.FilePart off count liftIO . throwIO $ HCSendFile ct fp $ Just $ W.FilePart off count
-- | Bypass remaining handler code and output the given content with a 200 -- | Bypass remaining handler code and output the given content with a 200
-- status code. -- status code.
sendResponse :: (Monad mo, HasReps c) => c -> GGHandler sub master mo a sendResponse :: (MonadIO mo, HasReps c) => c -> GGHandler sub master mo a
sendResponse = GHandler . lift . throwError . HCContent H.status200 sendResponse = liftIO . throwIO . HCContent H.status200
. chooseRep . chooseRep
-- | Bypass remaining handler code and output the given content with the given -- | Bypass remaining handler code and output the given content with the given
-- status code. -- status code.
sendResponseStatus :: (Monad mo, HasReps c) => H.Status -> c -> GGHandler s m mo a sendResponseStatus :: (MonadIO mo, HasReps c) => H.Status -> c -> GGHandler s m mo a
sendResponseStatus s = GHandler . lift . throwError . HCContent s sendResponseStatus s = liftIO . throwIO . HCContent s
. chooseRep . chooseRep
-- | Send a 201 "Created" response with the given route as the Location -- | Send a 201 "Created" response with the given route as the Location
-- response header. -- response header.
sendResponseCreated :: Monad mo => Route m -> GGHandler s m mo a sendResponseCreated :: MonadIO mo => Route m -> GGHandler s m mo a
sendResponseCreated url = do sendResponseCreated url = do
r <- getUrlRender r <- getUrlRender
GHandler $ lift $ throwError $ HCCreated $ r url liftIO . throwIO $ HCCreated $ r url
-- | Send a 'W.Response'. Please note: this function is rarely -- | Send a 'W.Response'. Please note: this function is rarely
-- necessary, and will /disregard/ any changes to response headers and session -- necessary, and will /disregard/ any changes to response headers and session
-- that you have already specified. This function short-circuits. It should be -- that you have already specified. This function short-circuits. It should be
-- considered only for very specific needs. If you are not sure if you need it, -- considered only for very specific needs. If you are not sure if you need it,
-- you don't. -- you don't.
sendWaiResponse :: Monad mo => W.Response -> GGHandler s m mo b sendWaiResponse :: MonadIO mo => W.Response -> GGHandler s m mo b
sendWaiResponse = GHandler . lift . throwError . HCWai sendWaiResponse = liftIO . throwIO . HCWai
-- | Return a 404 not found page. Also denotes no handler available. -- | Return a 404 not found page. Also denotes no handler available.
notFound :: Failure ErrorResponse m => m a notFound :: Failure ErrorResponse m => m a
notFound = failure NotFound notFound = failure NotFound
-- | Return a 405 method not supported page. -- | Return a 405 method not supported page.
badMethod :: Monad mo => GGHandler s m mo a badMethod :: MonadIO mo => GGHandler s m mo a
badMethod = do badMethod = do
w <- waiRequest w <- waiRequest
failure $ BadMethod $ W.requestMethod w failure $ BadMethod $ W.requestMethod w
@ -605,7 +617,7 @@ permissionDenied :: Failure ErrorResponse m => Text -> m a
permissionDenied = failure . PermissionDenied permissionDenied = failure . PermissionDenied
-- | Return a 403 permission denied page. -- | Return a 403 permission denied page.
permissionDeniedI :: (RenderMessage y msg, Monad mo) => msg -> GGHandler s y mo a permissionDeniedI :: (RenderMessage y msg, MonadIO mo) => msg -> GGHandler s y mo a
permissionDeniedI msg = do permissionDeniedI msg = do
mr <- getMessageRender mr <- getMessageRender
permissionDenied $ mr msg permissionDenied $ mr msg
@ -615,14 +627,14 @@ invalidArgs :: Failure ErrorResponse m => [Text] -> m a
invalidArgs = failure . InvalidArgs invalidArgs = failure . InvalidArgs
-- | Return a 400 invalid arguments page. -- | Return a 400 invalid arguments page.
invalidArgsI :: (RenderMessage y msg, Monad mo) => [msg] -> GGHandler s y mo a invalidArgsI :: (RenderMessage y msg, MonadIO mo) => [msg] -> GGHandler s y mo a
invalidArgsI msg = do invalidArgsI msg = do
mr <- getMessageRender mr <- getMessageRender
invalidArgs $ map mr msg invalidArgs $ map mr msg
------- Headers ------- Headers
-- | Set the cookie on the client. -- | Set the cookie on the client.
setCookie :: Monad mo setCookie :: MonadIO mo
=> Int -- ^ minutes to timeout => Int -- ^ minutes to timeout
-> H.Ascii -- ^ key -> H.Ascii -- ^ key
-> H.Ascii -- ^ value -> H.Ascii -- ^ value
@ -630,22 +642,22 @@ setCookie :: Monad mo
setCookie a b = addHeader . AddCookie a b setCookie a b = addHeader . AddCookie a b
-- | Unset the cookie on the client. -- | Unset the cookie on the client.
deleteCookie :: Monad mo => H.Ascii -> GGHandler sub master mo () deleteCookie :: MonadIO mo => H.Ascii -> GGHandler sub master mo ()
deleteCookie = addHeader . DeleteCookie deleteCookie = addHeader . DeleteCookie
-- | Set the language in the user session. Will show up in 'languages' on the -- | Set the language in the user session. Will show up in 'languages' on the
-- next request. -- next request.
setLanguage :: Monad mo => Text -> GGHandler sub master mo () setLanguage :: MonadIO mo => Text -> GGHandler sub master mo ()
setLanguage = setSession langKey setLanguage = setSession langKey
-- | Set an arbitrary response header. -- | Set an arbitrary response header.
setHeader :: Monad mo setHeader :: MonadIO mo
=> CI H.Ascii -> H.Ascii -> GGHandler sub master mo () => CI H.Ascii -> H.Ascii -> GGHandler sub master mo ()
setHeader a = addHeader . Header a setHeader a = addHeader . Header a
-- | Set the Cache-Control header to indicate this response should be cached -- | Set the Cache-Control header to indicate this response should be cached
-- for the given number of seconds. -- for the given number of seconds.
cacheSeconds :: Monad mo => Int -> GGHandler s m mo () cacheSeconds :: MonadIO mo => Int -> GGHandler s m mo ()
cacheSeconds i = setHeader "Cache-Control" $ S8.pack $ concat cacheSeconds i = setHeader "Cache-Control" $ S8.pack $ concat
[ "max-age=" [ "max-age="
, show i , show i
@ -654,16 +666,16 @@ cacheSeconds i = setHeader "Cache-Control" $ S8.pack $ concat
-- | Set the Expires header to some date in 2037. In other words, this content -- | Set the Expires header to some date in 2037. In other words, this content
-- is never (realistically) expired. -- is never (realistically) expired.
neverExpires :: Monad mo => GGHandler s m mo () neverExpires :: MonadIO mo => GGHandler s m mo ()
neverExpires = setHeader "Expires" "Thu, 31 Dec 2037 23:55:55 GMT" neverExpires = setHeader "Expires" "Thu, 31 Dec 2037 23:55:55 GMT"
-- | Set an Expires header in the past, meaning this content should not be -- | Set an Expires header in the past, meaning this content should not be
-- cached. -- cached.
alreadyExpired :: Monad mo => GGHandler s m mo () alreadyExpired :: MonadIO mo => GGHandler s m mo ()
alreadyExpired = setHeader "Expires" "Thu, 01 Jan 1970 05:05:05 GMT" alreadyExpired = setHeader "Expires" "Thu, 01 Jan 1970 05:05:05 GMT"
-- | Set an Expires header to the given date. -- | Set an Expires header to the given date.
expiresAt :: Monad mo => UTCTime -> GGHandler s m mo () expiresAt :: MonadIO mo => UTCTime -> GGHandler s m mo ()
expiresAt = setHeader "Expires" . encodeUtf8 . formatRFC1123 expiresAt = setHeader "Expires" . encodeUtf8 . formatRFC1123
-- | Set a variable in the user's session. -- | Set a variable in the user's session.
@ -671,22 +683,22 @@ expiresAt = setHeader "Expires" . encodeUtf8 . formatRFC1123
-- The session is handled by the clientsession package: it sets an encrypted -- The session is handled by the clientsession package: it sets an encrypted
-- and hashed cookie on the client. This ensures that all data is secure and -- and hashed cookie on the client. This ensures that all data is secure and
-- not tampered with. -- not tampered with.
setSession :: Monad mo setSession :: MonadIO mo
=> Text -- ^ key => Text -- ^ key
-> Text -- ^ value -> Text -- ^ value
-> GGHandler sub master mo () -> GGHandler sub master mo ()
setSession k = GHandler . lift . lift . lift . modify . modSession . Map.insert k setSession k = modify . modSession . Map.insert k
-- | Unsets a session variable. See 'setSession'. -- | Unsets a session variable. See 'setSession'.
deleteSession :: Monad mo => Text -> GGHandler sub master mo () deleteSession :: MonadIO mo => Text -> GGHandler sub master mo ()
deleteSession = GHandler . lift . lift . lift . modify . modSession . Map.delete deleteSession = modify . modSession . Map.delete
modSession :: (SessionMap -> SessionMap) -> GHState -> GHState modSession :: (SessionMap -> SessionMap) -> GHState -> GHState
modSession f x = x { ghsSession = f $ ghsSession x } modSession f x = x { ghsSession = f $ ghsSession x }
-- | Internal use only, not to be confused with 'setHeader'. -- | Internal use only, not to be confused with 'setHeader'.
addHeader :: Monad mo => Header -> GGHandler sub master mo () addHeader :: MonadIO mo => Header -> GGHandler sub master mo ()
addHeader = GHandler . lift . lift . tell . Endo . (:) addHeader = tell . Endo . (:)
getStatus :: ErrorResponse -> H.Status getStatus :: ErrorResponse -> H.Status
getStatus NotFound = H.status404 getStatus NotFound = H.status404
@ -708,17 +720,17 @@ data RedirectType = RedirectPermanent
localNoCurrent :: Monad mo => GGHandler s m mo a -> GGHandler s m mo a localNoCurrent :: Monad mo => GGHandler s m mo a -> GGHandler s m mo a
localNoCurrent = localNoCurrent =
GHandler . local (\hd -> hd { handlerRoute = Nothing }) . unGHandler local (\hd -> hd { handlerRoute = Nothing })
-- | Lookup for session data. -- | Lookup for session data.
lookupSession :: Monad mo => Text -> GGHandler s m mo (Maybe Text) lookupSession :: MonadIO mo => Text -> GGHandler s m mo (Maybe Text)
lookupSession n = GHandler $ do lookupSession n = do
m <- liftM ghsSession $ lift $ lift $ lift get m <- liftM ghsSession get
return $ Map.lookup n m return $ Map.lookup n m
-- | Get all session variables. -- | Get all session variables.
getSession :: Monad mo => GGHandler s m mo SessionMap getSession :: MonadIO mo => GGHandler s m mo SessionMap
getSession = liftM ghsSession $ GHandler $ lift $ lift $ lift get getSession = liftM ghsSession get
handlerToYAR :: (HasReps a, HasReps b) handlerToYAR :: (HasReps a, HasReps b)
=> m -- ^ master site foundation => m -- ^ master site foundation
@ -808,8 +820,8 @@ headerToPair cp _ (DeleteCookie key) =
headerToPair _ _ (Header key value) = (key, value) headerToPair _ _ (Header key value) = (key, value)
-- | Get a unique identifier. -- | Get a unique identifier.
newIdent :: Monad mo => GGHandler sub master mo String -- FIXME use Text newIdent :: MonadIO mo => GGHandler sub master mo String -- FIXME use Text
newIdent = GHandler $ lift $ lift $ lift $ do newIdent = do
x <- get x <- get
let i' = ghsIdent x + 1 let i' = ghsIdent x + 1
put x { ghsIdent = i' } put x { ghsIdent = i' }
@ -818,42 +830,7 @@ newIdent = GHandler $ lift $ lift $ lift $ do
liftIOHandler :: MonadIO mo liftIOHandler :: MonadIO mo
=> GGHandler sub master IO a => GGHandler sub master IO a
-> GGHandler sub master mo a -> GGHandler sub master mo a
liftIOHandler m = GHandler $ liftIOHandler (ReaderT m) = ReaderT $ \r -> liftIO $ m r
ReaderT $ \r ->
ErrorT $
WriterT $
StateT $ \s ->
liftIO $ runGGHandler m r s
runGGHandler :: GGHandler sub master m a
-> HandlerData sub master
-> GHState
-> m ( ( Either HandlerContents a
, Endo [Header]
)
, GHState
)
runGGHandler m r s = runStateT
(runWriterT
(runErrorT
(runReaderT
(unGHandler m) r))) s
instance MonadTransControl (GGHandler s m) where
liftControl f =
GHandler $
liftControl $ \runRdr ->
liftControl $ \runErr ->
liftControl $ \runWrt ->
liftControl $ \runSt ->
f ( liftM ( GHandler
. join . lift
. join . lift
. join . lift
)
. runSt . runWrt . runErr . runRdr
. unGHandler
)
-- | Redirect to a POST resource. -- | Redirect to a POST resource.
-- --
@ -861,7 +838,7 @@ instance MonadTransControl (GGHandler s m) where
-- POST form, and some Javascript to automatically submit the form. This can be -- POST form, and some Javascript to automatically submit the form. This can be
-- useful when you need to post a plain link somewhere that needs to cause -- useful when you need to post a plain link somewhere that needs to cause
-- changes on the server. -- changes on the server.
redirectToPost :: Monad mo => Route master -> GGHandler sub master mo a redirectToPost :: MonadIO mo => Route master -> GGHandler sub master mo a
redirectToPost dest = hamletToRepHtml redirectToPost dest = hamletToRepHtml
#if GHC7 #if GHC7
[hamlet| [hamlet|
@ -902,3 +879,16 @@ getMessageRender = do
m <- getYesod m <- getYesod
l <- reqLangs `liftM` getRequest l <- reqLangs `liftM` getRequest
return $ renderMessage m l return $ renderMessage m l
cacheLookup :: MonadIO mo => CacheKey a -> GGHandler sub master mo (Maybe a)
cacheLookup k = do
gs <- get
return $ Cache.lookup k $ ghsCache gs
cacheInsert :: MonadIO mo => CacheKey a -> a -> GGHandler sub master mo ()
cacheInsert k v = modify $ \gs ->
gs { ghsCache = Cache.insert k v $ ghsCache gs }
cacheDelete :: MonadIO mo => CacheKey a -> GGHandler sub master mo ()
cacheDelete k = modify $ \gs ->
gs { ghsCache = Cache.delete k $ ghsCache gs }

View File

@ -29,7 +29,6 @@ module Yesod.Internal
) where ) where
import Text.Hamlet (HtmlUrl, hamlet, Html) import Text.Hamlet (HtmlUrl, hamlet, Html)
import Text.Cassius (CssUrl)
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)
@ -44,6 +43,7 @@ import qualified Network.HTTP.Types as A
import Data.CaseInsensitive (CI) import Data.CaseInsensitive (CI)
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)
#if GHC7 #if GHC7
#define HAMLET hamlet #define HAMLET hamlet
@ -107,12 +107,14 @@ nonceKey = "_NONCE"
sessionName :: IsString a => a sessionName :: IsString a => a
sessionName = "_SESSION" sessionName = "_SESSION"
type CssBuilderUrl a = (a -> [(Text, Text)] -> Text) -> Builder
data GWData a = GWData data GWData a = GWData
!(Body a) !(Body a)
!(Last Title) !(Last Title)
!(UniqueList (Script a)) !(UniqueList (Script a))
!(UniqueList (Stylesheet a)) !(UniqueList (Stylesheet a))
!(Map.Map (Maybe Text) (CssUrl a)) -- media type !(Map.Map (Maybe Text) (CssBuilderUrl a)) -- media type
!(Maybe (JavascriptUrl a)) !(Maybe (JavascriptUrl a))
!(Head a) !(Head a)
instance Monoid (GWData a) where instance Monoid (GWData a) where

View File

@ -0,0 +1,38 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
module Yesod.Internal.Cache
( Cache
, CacheKey
, mkCacheKey
, lookup
, insert
, delete
) where
import Prelude hiding (lookup)
import qualified Data.IntMap as Map
import Language.Haskell.TH.Syntax (Q, Exp, runIO, Exp (LitE), Lit (IntegerL))
import Language.Haskell.TH (appE)
import Data.Unique (hashUnique, newUnique)
import GHC.Exts (Any)
import Unsafe.Coerce (unsafeCoerce)
import Data.Monoid (Monoid)
import Control.Applicative ((<$>))
newtype Cache = Cache (Map.IntMap Any)
deriving Monoid
newtype CacheKey a = CacheKey Int
-- | Generate a new 'CacheKey'. Be sure to give a full type signature.
mkCacheKey :: Q Exp
mkCacheKey = [|CacheKey|] `appE` (LitE . IntegerL . fromIntegral . hashUnique <$> runIO newUnique)
lookup :: CacheKey a -> Cache -> Maybe a
lookup (CacheKey i) (Cache m) = unsafeCoerce <$> Map.lookup i m
insert :: CacheKey a -> a -> Cache -> Cache
insert (CacheKey k) v (Cache m) = Cache (Map.insert k (unsafeCoerce v) m)
delete :: CacheKey a -> Cache -> Cache
delete (CacheKey k) (Cache m) = Cache (Map.delete k m)

View File

@ -35,8 +35,6 @@ import Yesod.Handler
import Control.Arrow ((***)) import Control.Arrow ((***))
import Control.Monad (forM) import Control.Monad (forM)
import qualified Paths_yesod_core
import Data.Version (showVersion)
import Yesod.Widget import Yesod.Widget
import Yesod.Request import Yesod.Request
import qualified Network.Wai as W import qualified Network.Wai as W
@ -48,11 +46,10 @@ import qualified Web.ClientSession as CS
import qualified Data.ByteString.Char8 as S8 import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import Data.Monoid import Data.Monoid
import Control.Monad.Trans.RWS import Control.Monad.Trans.Writer (runWriterT)
import Text.Hamlet import Text.Hamlet
import Text.Cassius
import Text.Julius import Text.Julius
import Text.Blaze ((!), customAttribute, textTag, toValue) import Text.Blaze ((!), customAttribute, textTag, toValue, unsafeLazyByteString)
import qualified Text.Blaze.Html5 as TBH import qualified Text.Blaze.Html5 as TBH
import Data.Text.Lazy.Builder (toLazyText) import Data.Text.Lazy.Builder (toLazyText)
import Data.Text.Lazy.Encoding (encodeUtf8) import Data.Text.Lazy.Encoding (encodeUtf8)
@ -75,6 +72,20 @@ import qualified Data.Text.Lazy.IO
import qualified Data.Text.Lazy.Builder as TB import qualified Data.Text.Lazy.Builder as TB
import Language.Haskell.TH.Syntax (Loc (..), Lift (..)) import Language.Haskell.TH.Syntax (Loc (..), Lift (..))
import Text.Blaze (preEscapedLazyText) import Text.Blaze (preEscapedLazyText)
import Data.Aeson (Value (Array, String))
import Data.Aeson.Encode (encode)
import qualified Data.Vector as Vector
-- mega repo can't access this
#ifndef MEGA
import qualified Paths_yesod_core
import Data.Version (showVersion)
yesodVersion :: String
yesodVersion = showVersion Paths_yesod_core.version
#else
yesodVersion :: String
yesodVersion = "0.9.3.2"
#endif
#if GHC7 #if GHC7
#define HAMLET hamlet #define HAMLET hamlet
@ -159,9 +170,9 @@ class RenderRoute (Route a) => Yesod a where
-- | Determine if a request is authorized or not. -- | Determine if a request is authorized or not.
-- --
-- Return 'Nothing' is the request is authorized, 'Just' a message if -- Return 'Authorized' if the request is authorized,
-- unauthorized. If authentication is required, you should use a redirect; -- 'Unauthorized' a message if unauthorized.
-- the Auth helper provides this functionality automatically. -- If authentication is required, return 'AuthenticationRequired'.
isAuthorized :: Route a isAuthorized :: Route a
-> Bool -- ^ is this a write request? -> Bool -- ^ is this a write request?
-> GHandler s a AuthResult -> GHandler s a AuthResult
@ -264,6 +275,11 @@ class RenderRoute (Route a) => Yesod a where
gzipCompressFiles :: a -> Bool gzipCompressFiles :: a -> Bool
gzipCompressFiles _ = False gzipCompressFiles _ = False
-- | Location of yepnope.js, if any. If one is provided, then all
-- Javascript files will be loaded asynchronously.
yepnopeJs :: a -> Maybe (Either Text (Route a))
yepnopeJs _ = Nothing
messageLoggerHandler :: (Yesod m, MonadIO mo) messageLoggerHandler :: (Yesod m, MonadIO mo)
=> Loc -> LogLevel -> Text -> GGHandler s m mo () => Loc -> LogLevel -> Text -> GGHandler s m mo ()
messageLoggerHandler loc level msg = do messageLoggerHandler loc level msg = do
@ -327,12 +343,12 @@ defaultYesodRunner _ m toMaster _ murl _ req
[] -> Nothing [] -> Nothing
(x, _):_ -> Just x (x, _):_ -> Just x
defaultYesodRunner s master toMasterRoute mkey murl handler req = do defaultYesodRunner s master toMasterRoute mkey murl handler req = do
now <- liftIO getCurrentTime now <- {-# SCC "getCurrentTime" #-} liftIO getCurrentTime
let getExpires m = fromIntegral (m * 60) `addUTCTime` now let getExpires m = {-# SCC "getExpires" #-} fromIntegral (m * 60) `addUTCTime` now
let exp' = getExpires $ clientSessionDuration master let exp' = {-# SCC "exp'" #-} getExpires $ clientSessionDuration master
let rh = takeWhile (/= ':') $ show $ W.remoteHost req let rh = {-# SCC "rh" #-} takeWhile (/= ':') $ show $ W.remoteHost req
let host = if sessionIpAddress master then S8.pack rh else "" let host = if sessionIpAddress master then S8.pack rh else ""
let session' = let session' = {-# SCC "session'" #-}
case mkey of case mkey of
Nothing -> [] Nothing -> []
Just key -> fromMaybe [] $ do Just key -> fromMaybe [] $ do
@ -340,7 +356,7 @@ defaultYesodRunner s master toMasterRoute mkey murl handler req = do
val <- lookup sessionName $ parseCookies raw val <- lookup sessionName $ parseCookies raw
decodeSession key now host val decodeSession key now host val
rr <- liftIO $ parseWaiRequest req session' mkey rr <- liftIO $ parseWaiRequest req session' mkey
let h = do let h = {-# SCC "h" #-} do
case murl of case murl of
Nothing -> handler Nothing -> handler
Just url -> do Just url -> do
@ -361,7 +377,8 @@ defaultYesodRunner s master toMasterRoute mkey murl handler req = do
$ filter (\(x, _) -> x /= nonceKey) session' $ filter (\(x, _) -> x /= nonceKey) session'
yar <- handlerToYAR master s toMasterRoute (yesodRender master) errorHandler rr murl sessionMap h yar <- handlerToYAR master s toMasterRoute (yesodRender master) errorHandler rr murl sessionMap h
let mnonce = reqNonce rr let mnonce = reqNonce rr
iv <- liftIO CS.randomIV -- FIXME should we be caching this IV value and reusing it for efficiency?
iv <- {-# SCC "iv" #-} maybe (return $ error "Should not be used") (const $ liftIO CS.randomIV) mkey
return $ yarToResponse (hr iv mnonce getExpires host exp') yar return $ yarToResponse (hr iv mnonce getExpires host exp') yar
where where
hr iv mnonce getExpires host exp' hs ct sm = hr iv mnonce getExpires host exp' hs ct sm =
@ -472,18 +489,22 @@ maybeAuthorized r isWrite = do
x <- isAuthorized r isWrite x <- isAuthorized r isWrite
return $ if x == Authorized then Just r else Nothing return $ if x == Authorized then Just r else Nothing
jsToHtml :: Javascript -> Html
jsToHtml (Javascript b) = preEscapedLazyText $ toLazyText b
jelper :: JavascriptUrl url -> HtmlUrl url
jelper = fmap jsToHtml
-- | Convert a widget to a 'PageContent'. -- | Convert a widget to a 'PageContent'.
widgetToPageContent :: (Eq (Route master), Yesod master) widgetToPageContent :: (Eq (Route master), Yesod master)
=> GWidget sub master () => GWidget sub master ()
-> GHandler sub master (PageContent (Route master)) -> GHandler sub master (PageContent (Route master))
widgetToPageContent (GWidget w) = do widgetToPageContent (GWidget w) = do
((), _, GWData (Body body) (Last mTitle) scripts' stylesheets' style jscript (Head head')) <- runRWST w () 0 master <- getYesod
((), GWData (Body body) (Last mTitle) scripts' stylesheets' style jscript (Head head')) <- runWriterT w
let title = maybe mempty unTitle mTitle let title = maybe mempty unTitle mTitle
let scripts = runUniqueList scripts' let scripts = runUniqueList scripts'
let stylesheets = runUniqueList stylesheets' let stylesheets = runUniqueList stylesheets'
let jsToHtml (Javascript b) = preEscapedLazyText $ toLazyText b
jelper :: JavascriptUrl url -> HtmlUrl url
jelper = fmap jsToHtml
render <- getUrlRenderParams render <- getUrlRenderParams
let renderLoc x = let renderLoc x =
@ -492,7 +513,7 @@ widgetToPageContent (GWidget w) = do
Just (Left s) -> Just s Just (Left s) -> Just s
Just (Right (u, p)) -> Just $ render u p Just (Right (u, p)) -> Just $ render u p
css <- forM (Map.toList style) $ \(mmedia, content) -> do css <- forM (Map.toList style) $ \(mmedia, content) -> do
let rendered = renderCssUrl render content let rendered = toLazyText $ content render
x <- addStaticContent "css" "text/css; charset=utf-8" x <- addStaticContent "css" "text/css; charset=utf-8"
$ encodeUtf8 rendered $ encodeUtf8 rendered
return (mmedia, return (mmedia,
@ -536,19 +557,54 @@ $forall s <- css
<style media=#{media}>#{content} <style media=#{media}>#{content}
$nothing $nothing
<style>#{content} <style>#{content}
$forall s <- scripts $maybe _ <- yepnopeJs master
^{mkScriptTag s} $nothing
$maybe j <- jscript $forall s <- scripts
$maybe s <- jsLoc ^{mkScriptTag s}
<script src="#{s}"> $maybe j <- jscript
$nothing $maybe s <- jsLoc
<script>^{jelper j} <script src="#{s}">
$nothing
<script>^{jelper j}
\^{head'} \^{head'}
|] |]
return $ PageContent title head'' body let (mcomplete, ynscripts) = ynHelper render scripts jscript jsLoc
let bodyYN = [HAMLET|
^{body}
$maybe eyn <- yepnopeJs master
$maybe yn <- left eyn
<script src=#{yn}>
$maybe yn <- right eyn
<script src=@{yn}>
$maybe complete <- mcomplete
<script>yepnope({load:#{ynscripts},complete:function(){^{complete}}})
$nothing
<script>yepnope({load:#{ynscripts}})
|]
return $ PageContent title head'' bodyYN
yesodVersion :: String ynHelper :: (url -> [x] -> Text)
yesodVersion = showVersion Paths_yesod_core.version -> [Script (url)]
-> Maybe (JavascriptUrl (url))
-> Maybe Text
-> (Maybe (HtmlUrl (url)), Html)
ynHelper render scripts jscript jsLoc =
(mcomplete, unsafeLazyByteString $ encode $ Array $ Vector.fromList $ map String scripts'')
where
scripts' = map goScript scripts
scripts'' =
case jsLoc of
Just s -> scripts' ++ [s]
Nothing -> scripts'
goScript (Script (Local url) _) = render url []
goScript (Script (Remote s) _) = s
mcomplete =
case jsLoc of
Just{} -> Nothing
Nothing ->
case jscript of
Nothing -> Nothing
Just j -> Just $ jelper j
yesodRender :: Yesod y yesodRender :: Yesod y
=> y => y

View File

@ -60,9 +60,9 @@ parseWaiRequest' env session' key' gen = Request gets'' cookies' env langs' nonc
, lookup langKey cookies' -- Cookie _LANG , lookup langKey cookies' -- Cookie _LANG
, lookup langKey session' -- Session _LANG , lookup langKey session' -- Session _LANG
] ++ langs -- Accept-Language(s) ] ++ langs -- Accept-Language(s)
-- If the session is not secure a nonce should not be -- If sessions are disabled nonces should not be used (any
-- used (any nonce present in the session is ignored). -- nonceKey present in the session is ignored). If sessions
-- If a secure session has no nonceKey a new one is -- are enabled and a session has no nonceKey a new one is
-- generated. -- generated.
nonce = case (key', lookup nonceKey session') of nonce = case (key', lookup nonceKey session') of
(Nothing, _) -> Nothing (Nothing, _) -> Nothing
@ -75,7 +75,10 @@ parseWaiRequest' env session' key' gen = Request gets'' cookies' env langs' nonc
randomString :: RandomGen g => Int -> g -> String randomString :: RandomGen g => Int -> g -> String
randomString len = take len . map toChar . randomRs (0, 61) randomString len = take len . map toChar . randomRs (0, 61)
where where
toChar i = (['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9']) !! i toChar i
| i < 26 = toEnum $ i + fromEnum 'A'
| i < 52 = toEnum $ i + fromEnum 'a' - 26
| otherwise = toEnum $ i + fromEnum '0' - 52
-- | A tuple containing both the POST parameters and submitted files. -- | A tuple containing both the POST parameters and submitted files.
type RequestBodyContents = type RequestBodyContents =

View File

@ -331,18 +331,24 @@ pieceFromString ('#':x) = SinglePiece x
pieceFromString ('*':x) = MultiPiece x pieceFromString ('*':x) = MultiPiece x
pieceFromString x = StaticPiece x pieceFromString x = StaticPiece x
-- n^2, should be a way to speed it up
findOverlaps :: [Resource] -> [(Resource, Resource)] findOverlaps :: [Resource] -> [(Resource, Resource)]
findOverlaps = gos . map justPieces findOverlaps = go . map justPieces
where where
justPieces :: Resource -> ([Piece], Resource)
justPieces r@(Resource _ ps _) = (ps, r) justPieces r@(Resource _ ps _) = (ps, r)
gos [] = []
gos (x:xs) = mapMaybe (go x) xs ++ gos xs go [] = []
go (StaticPiece x:xs, xr) (StaticPiece y:ys, yr) go (x:xs) = mapMaybe (mOverlap x) xs ++ go xs
| x == y = go (xs, xr) (ys, yr)
mOverlap :: ([Piece], Resource) -> ([Piece], Resource) ->
Maybe (Resource, Resource)
mOverlap (StaticPiece x:xs, xr) (StaticPiece y:ys, yr)
| x == y = mOverlap (xs, xr) (ys, yr)
| otherwise = Nothing | otherwise = Nothing
go (MultiPiece _:_, xr) (_, yr) = Just (xr, yr) mOverlap (MultiPiece _:_, xr) (_, yr) = Just (xr, yr)
go (_, xr) (MultiPiece _:_, yr) = Just (xr, yr) mOverlap (_, xr) (MultiPiece _:_, yr) = Just (xr, yr)
go ([], xr) ([], yr) = Just (xr, yr) mOverlap ([], xr) ([], yr) = Just (xr, yr)
go ([], _) (_, _) = Nothing mOverlap ([], _) (_, _) = Nothing
go (_, _) ([], _) = Nothing mOverlap (_, _) ([], _) = Nothing
go (_:xs, xr) (_:ys, yr) = go (xs, xr) (ys, yr) mOverlap (_:xs, xr) (_:ys, yr) = mOverlap (xs, xr) (ys, yr)

View File

@ -6,6 +6,22 @@
-- --
module Yesod.Internal.TestApi module Yesod.Internal.TestApi
( randomString, parseWaiRequest' ( randomString, parseWaiRequest'
, catchIter
) where ) where
import Yesod.Internal.Request (randomString, parseWaiRequest') import Yesod.Internal.Request (randomString, parseWaiRequest')
import Control.Exception (Exception, catch)
import Data.Enumerator (Iteratee (..), Step (..))
import Data.ByteString (ByteString)
import Prelude hiding (catch)
catchIter :: Exception e
=> Iteratee ByteString IO a
-> (e -> Iteratee ByteString IO a)
-> Iteratee ByteString IO a
catchIter (Iteratee mstep) f = Iteratee $ do
step <- mstep `catch` (runIteratee . f)
return $ case step of
Continue k -> Continue $ \s -> catchIter (k s) f
Yield b s -> Yield b s
Error e -> Error e

View File

@ -1,259 +1,5 @@
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE ExistentialQuantification #-}
module Yesod.Message module Yesod.Message
( mkMessage ( module Text.Shakespeare.I18N
, RenderMessage (..)
, ToMessage (..)
, SomeMessage (..)
) where ) where
import Language.Haskell.TH.Syntax import Text.Shakespeare.I18N
import Data.Text (Text, pack, unpack)
import System.Directory
import Data.Maybe (catMaybes)
import Data.List (isSuffixOf, sortBy, foldl')
import qualified Data.ByteString as S
import Data.Text.Encoding (decodeUtf8)
import Data.Char (isSpace, toLower, toUpper)
import Data.Ord (comparing)
import Text.Shakespeare.Base (Deref (..), Ident (..), parseHash, derefToExp)
import Text.ParserCombinators.Parsec (parse, many, eof, many1, noneOf, (<|>))
import Control.Arrow ((***))
import Data.Monoid (mempty, mappend)
import qualified Data.Text as T
import Data.String (IsString (fromString))
class ToMessage a where
toMessage :: a -> Text
instance ToMessage Text where
toMessage = id
instance ToMessage String where
toMessage = Data.Text.pack
class RenderMessage master message where
renderMessage :: master
-> [Text] -- ^ languages
-> message
-> Text
instance RenderMessage master Text where
renderMessage _ _ = id
type Lang = Text
mkMessage :: String
-> FilePath
-> Lang
-> Q [Dec]
mkMessage dt folder lang = do
files <- qRunIO $ getDirectoryContents folder
contents <- qRunIO $ fmap catMaybes $ mapM (loadLang folder) files
sdef <-
case lookup lang contents of
Nothing -> error $ "Did not find main language file: " ++ unpack lang
Just def -> toSDefs def
mapM_ (checkDef sdef) $ map snd contents
let dt' = ConT $ mkName dt
let mname = mkName $ dt ++ "Message"
c1 <- fmap concat $ mapM (toClauses dt) contents
c2 <- mapM (sToClause dt) sdef
c3 <- defClause
return
[ DataD [] mname [] (map (toCon dt) sdef) []
, InstanceD
[]
(ConT ''RenderMessage `AppT` dt' `AppT` ConT mname)
[ FunD (mkName "renderMessage") $ c1 ++ c2 ++ [c3]
]
]
toClauses :: String -> (Lang, [Def]) -> Q [Clause]
toClauses dt (lang, defs) =
mapM go defs
where
go def = do
a <- newName "lang"
(pat, bod) <- mkBody dt (constr def) (map fst $ vars def) (content def)
guard <- fmap NormalG [|$(return $ VarE a) == pack $(lift $ unpack lang)|]
return $ Clause
[WildP, ConP (mkName ":") [VarP a, WildP], pat]
(GuardedB [(guard, bod)])
[]
mkBody :: String -- ^ datatype
-> String -- ^ constructor
-> [String] -- ^ variable names
-> [Content]
-> Q (Pat, Exp)
mkBody dt cs vs ct = do
vp <- mapM go vs
let pat = RecP (mkName $ "Msg" ++ cs) (map (varName dt *** VarP) vp)
let ct' = map (fixVars vp) ct
pack' <- [|Data.Text.pack|]
tomsg <- [|toMessage|]
let ct'' = map (toH pack' tomsg) ct'
mapp <- [|mappend|]
let app a b = InfixE (Just a) mapp (Just b)
e <-
case ct'' of
[] -> [|mempty|]
[x] -> return x
(x:xs) -> return $ foldl' app x xs
return (pat, e)
where
toH pack' _ (Raw s) = pack' `AppE` SigE (LitE (StringL s)) (ConT ''String)
toH _ tomsg (Var d) = tomsg `AppE` derefToExp [] d
go x = do
let y = mkName $ '_' : x
return (x, y)
fixVars vp (Var d) = Var $ fixDeref vp d
fixVars _ (Raw s) = Raw s
fixDeref vp (DerefIdent (Ident i)) = DerefIdent $ Ident $ fixIdent vp i
fixDeref vp (DerefBranch a b) = DerefBranch (fixDeref vp a) (fixDeref vp b)
fixDeref _ d = d
fixIdent vp i =
case lookup i vp of
Nothing -> i
Just y -> nameBase y
sToClause :: String -> SDef -> Q Clause
sToClause dt sdef = do
(pat, bod) <- mkBody dt (sconstr sdef) (map fst $ svars sdef) (scontent sdef)
return $ Clause
[WildP, ConP (mkName "[]") [], pat]
(NormalB bod)
[]
defClause :: Q Clause
defClause = do
a <- newName "sub"
c <- newName "langs"
d <- newName "msg"
rm <- [|renderMessage|]
return $ Clause
[VarP a, ConP (mkName ":") [WildP, VarP c], VarP d]
(NormalB $ rm `AppE` VarE a `AppE` VarE c `AppE` VarE d)
[]
toCon :: String -> SDef -> Con
toCon dt (SDef c vs _) =
RecC (mkName $ "Msg" ++ c) $ map go vs
where
go (n, t) = (varName dt n, NotStrict, ConT $ mkName t)
varName :: String -> String -> Name
varName a y =
mkName $ concat [lower a, "Message", upper y]
where
lower (x:xs) = toLower x : xs
lower [] = []
upper (x:xs) = toUpper x : xs
upper [] = []
checkDef :: [SDef] -> [Def] -> Q ()
checkDef x y =
go (sortBy (comparing sconstr) x) (sortBy (comparing constr) y)
where
go _ [] = return ()
go [] (b:_) = error $ "Extra message constructor: " ++ constr b
go (a:as) (b:bs)
| sconstr a < constr b = go as (b:bs)
| sconstr a > constr b = error $ "Extra message constructor: " ++ constr b
| otherwise = do
go' (svars a) (vars b)
go as bs
go' ((an, at):as) ((bn, mbt):bs)
| an /= bn = error "Mismatched variable names"
| otherwise =
case mbt of
Nothing -> go' as bs
Just bt
| at == bt -> go' as bs
| otherwise -> error "Mismatched variable types"
go' [] [] = return ()
go' _ _ = error "Mistmached variable count"
toSDefs :: [Def] -> Q [SDef]
toSDefs = mapM toSDef
toSDef :: Def -> Q SDef
toSDef d = do
vars' <- mapM go $ vars d
return $ SDef (constr d) vars' (content d)
where
go (a, Just b) = return (a, b)
go (a, Nothing) = error $ "Main language missing type for " ++ show (constr d, a)
data SDef = SDef
{ sconstr :: String
, svars :: [(String, String)]
, scontent :: [Content]
}
data Def = Def
{ constr :: String
, vars :: [(String, Maybe String)]
, content :: [Content]
}
loadLang :: FilePath -> FilePath -> IO (Maybe (Lang, [Def]))
loadLang folder file = do
let file' = folder ++ '/' : file
e <- doesFileExist file'
if e && ".msg" `isSuffixOf` file
then do
let lang = pack $ reverse $ drop 4 $ reverse file
bs <- S.readFile file'
let s = unpack $ decodeUtf8 bs
defs <- fmap catMaybes $ mapM parseDef $ lines s
return $ Just (lang, defs)
else return Nothing
parseDef :: String -> IO (Maybe Def)
parseDef "" = return Nothing
parseDef ('#':_) = return Nothing
parseDef s =
case end of
':':end' -> do
content' <- fmap compress $ parseContent $ dropWhile isSpace end'
case words begin of
[] -> error $ "Missing constructor: " ++ s
(w:ws) -> return $ Just Def
{ constr = w
, vars = map parseVar ws
, content = content'
}
_ -> error $ "Missing colon: " ++ s
where
(begin, end) = break (== ':') s
data Content = Var Deref | Raw String
compress :: [Content] -> [Content]
compress [] = []
compress (Raw a:Raw b:rest) = compress $ Raw (a ++ b) : rest
compress (x:y) = x : compress y
parseContent :: String -> IO [Content]
parseContent s =
either (error . show) return $ parse go s s
where
go = do
x <- many go'
eof
return x
go' = (Raw `fmap` many1 (noneOf "#")) <|> (fmap (either Raw Var) parseHash)
parseVar :: String -> (String, Maybe String)
parseVar s =
case break (== '@') s of
(x, '@':y) -> (x, Just y)
_ -> (s, Nothing)
data SomeMessage master = forall msg. RenderMessage master msg => SomeMessage msg
instance IsString (SomeMessage master) where
fromString = SomeMessage . T.pack

View File

@ -5,6 +5,7 @@
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE CPP #-}
-- | Widgets combine HTML with JS and CSS dependencies with a unique identifier -- | Widgets combine HTML with JS and CSS dependencies with a unique identifier
-- generator, allowing you to create truly modular HTML components. -- generator, allowing you to create truly modular HTML components.
module Yesod.Widget module Yesod.Widget
@ -56,7 +57,7 @@ module Yesod.Widget
) where ) where
import Data.Monoid import Data.Monoid
import Control.Monad.Trans.RWS import Control.Monad.Trans.Writer
import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5 as H
import Text.Hamlet import Text.Hamlet
import Text.Cassius import Text.Cassius
@ -78,22 +79,48 @@ import qualified Data.Map as Map
import Language.Haskell.TH.Quote (QuasiQuoter) import Language.Haskell.TH.Quote (QuasiQuoter)
import Language.Haskell.TH.Syntax (Q, Exp (InfixE, VarE, LamE), Pat (VarP), newName) import Language.Haskell.TH.Syntax (Q, Exp (InfixE, VarE, LamE), Pat (VarP), newName)
#if MIN_VERSION_monad_control(0, 3, 0)
import Control.Monad.Trans.Control (MonadTransControl (..), MonadBaseControl (..), defaultLiftBaseWith, defaultRestoreM, ComposeSt)
#else
import Control.Monad.IO.Control (MonadControlIO) import Control.Monad.IO.Control (MonadControlIO)
#endif
import qualified Text.Hamlet as NP import qualified Text.Hamlet as NP
import Data.Text.Lazy.Builder (fromLazyText) import Data.Text.Lazy.Builder (fromLazyText)
import Text.Blaze (toHtml, preEscapedLazyText) import Text.Blaze (toHtml, preEscapedLazyText)
import Control.Monad.Base (MonadBase (liftBase))
-- | A generic widget, allowing specification of both the subsite and master -- | A generic widget, allowing specification of both the subsite and master
-- site datatypes. This is basically a large 'WriterT' stack keeping track of -- site datatypes. This is basically a large 'WriterT' stack keeping track of
-- dependencies along with a 'StateT' to track unique identifiers. -- dependencies along with a 'StateT' to track unique identifiers.
newtype GGWidget m monad a = GWidget { unGWidget :: GWInner m monad a } newtype GGWidget m monad a = GWidget { unGWidget :: GWInner m monad a }
deriving (Functor, Applicative, Monad, MonadIO, MonadControlIO) deriving (Functor, Applicative, Monad, MonadIO
#if !MIN_VERSION_monad_control(0, 3, 0)
, MonadControlIO
#endif
)
instance MonadBase b m => MonadBase b (GGWidget master m) where
liftBase = lift . liftBase
#if MIN_VERSION_monad_control(0, 3, 0)
instance MonadTransControl (GGWidget master) where
newtype StT (GGWidget master) a =
StWidget {unStWidget :: StT (GWInner master) a}
liftWith f = GWidget $ liftWith $ \run ->
f $ liftM StWidget . run . unGWidget
restoreT = GWidget . restoreT . liftM unStWidget
{-# INLINE liftWith #-}
{-# INLINE restoreT #-}
instance MonadBaseControl b m => MonadBaseControl b (GGWidget master m) where
newtype StM (GGWidget master m) a = StMT {unStMT :: ComposeSt (GGWidget master) m a}
liftBaseWith = defaultLiftBaseWith StMT
restoreM = defaultRestoreM unStMT
#endif
instance MonadTrans (GGWidget m) where instance MonadTrans (GGWidget m) where
lift = GWidget . lift lift = GWidget . lift
type GWidget s m = GGWidget m (GHandler s m) type GWidget s m = GGWidget m (GHandler s m)
type GWInner master = RWST () (GWData (Route master)) Int type GWInner master = WriterT (GWData (Route master))
instance (Monad monad, a ~ ()) => Monoid (GGWidget master monad a) where instance (Monad monad, a ~ ()) => Monoid (GGWidget master monad a) where
mempty = return () mempty = return ()
@ -103,9 +130,7 @@ addSubWidget :: (YesodSubRoute sub master) => sub -> GWidget sub master a -> GWi
addSubWidget sub (GWidget w) = do addSubWidget sub (GWidget w) = do
master <- lift getYesod master <- lift getYesod
let sr = fromSubRoute sub master let sr = fromSubRoute sub master
s <- GWidget get (a, w') <- lift $ toMasterHandlerMaybe sr (const sub) Nothing $ runWriterT w
(a, s', w') <- lift $ toMasterHandlerMaybe sr (const sub) Nothing $ runRWST w () s
GWidget $ put s'
GWidget $ tell w' GWidget $ tell w'
return a return a
@ -192,7 +217,7 @@ addWidget = id
-- | Add some raw CSS to the style tag. Applies to all media types. -- | Add some raw CSS to the style tag. Applies to all media types.
addCassius :: Monad m => CssUrl (Route master) -> GGWidget master m () addCassius :: Monad m => CssUrl (Route master) -> GGWidget master m ()
addCassius x = GWidget $ tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing x) mempty mempty addCassius x = GWidget $ tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing $ \r -> fromLazyText $ renderCss $ x r) mempty mempty
-- | Identical to 'addCassius'. -- | Identical to 'addCassius'.
addLucius :: Monad m => CssUrl (Route master) -> GGWidget master m () addLucius :: Monad m => CssUrl (Route master) -> GGWidget master m ()
@ -200,7 +225,7 @@ addLucius = addCassius
-- | Add some raw CSS to the style tag, for a specific media type. -- | Add some raw CSS to the style tag, for a specific media type.
addCassiusMedia :: Monad m => Text -> CssUrl (Route master) -> GGWidget master m () addCassiusMedia :: Monad m => Text -> CssUrl (Route master) -> GGWidget master m ()
addCassiusMedia m x = GWidget $ tell $ GWData mempty mempty mempty mempty (Map.singleton (Just m) x) mempty mempty addCassiusMedia m x = GWidget $ tell $ GWData mempty mempty mempty mempty (Map.singleton (Just m) $ \r -> fromLazyText $ renderCss $ x r) mempty mempty
-- | Identical to 'addCassiusMedia'. -- | Identical to 'addCassiusMedia'.
addLuciusMedia :: Monad m => Text -> CssUrl (Route master) -> GGWidget master m () addLuciusMedia :: Monad m => Text -> CssUrl (Route master) -> GGWidget master m ()
@ -273,9 +298,9 @@ addCoffeeBody c = do
-- manipulations. It can be easier to use this sometimes than 'wrapWidget'. -- manipulations. It can be easier to use this sometimes than 'wrapWidget'.
extractBody :: Monad mo => GGWidget m mo () -> GGWidget m mo (HtmlUrl (Route m)) extractBody :: Monad mo => GGWidget m mo () -> GGWidget m mo (HtmlUrl (Route m))
extractBody (GWidget w) = extractBody (GWidget w) =
GWidget $ mapRWST (liftM go) w GWidget $ mapWriterT (liftM go) w
where where
go ((), s, GWData (Body h) b c d e f g) = (h, s, GWData (Body mempty) b c d e f g) go ((), GWData (Body h) b c d e f g) = (h, GWData (Body mempty) b c d e f g)
-- | Content for a web page. By providing this datatype, we can easily create -- | Content for a web page. By providing this datatype, we can easily create
-- generic site templates, which would have the type signature: -- generic site templates, which would have the type signature:

8
yesod-core/bench.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash -ex
ghc --make bench/pong.hs
ghc --make bench/pong.hs -prof -osuf o_p -caf-all -auto-all -rtsopts
./bench/pong +RTS -p &
sleep 2
ab -n 1000 -c 5 http://localhost:3000/ 2>&1 | grep 'Time taken'
curl http://localhost:3000/kill

32
yesod-core/bench/pong.hs Normal file
View File

@ -0,0 +1,32 @@
{-# LANGUAGE OverloadedStrings, QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
import Yesod.Dispatch
import Yesod.Content
import Yesod.Internal.Core
import Data.ByteString (ByteString)
import Network.Wai.Handler.Warp (run)
import Control.Concurrent.MVar
import Control.Concurrent
import Network.Wai
import Control.Monad.IO.Class
data Pong = Pong
mkYesod "Pong" [$parseRoutes|
/ PongR GET
|]
instance Yesod Pong where
approot _ = ""
encryptKey _ = return Nothing
getPongR = return $ RepPlain $ toContent ("PONG" :: ByteString)
main = do
app <- toWaiAppPlain Pong
flag <- newEmptyMVar
forkIO $ run 3000 $ \req ->
if pathInfo req == ["kill"]
then do
liftIO $ putMVar flag ()
error "done"
else app req
takeMVar flag

View File

@ -5,10 +5,13 @@
import Yesod.Core import Yesod.Core
import Network.Wai.Handler.Warp (run) import Network.Wai.Handler.Warp (run)
import Data.Text (unpack) import Data.Text (unpack)
import Text.Julius (julius)
data Subsite = Subsite String data Subsite = Subsite String
mkYesodSub "Subsite" [] [$parseRoutes| type Strings = [String]
mkYesodSub "Subsite" [] [parseRoutes|
/ SubRootR GET / SubRootR GET
/multi/*Strings SubMultiR /multi/*Strings SubMultiR
|] |]
@ -32,9 +35,15 @@ mkYesod "HelloWorld" [$parseRoutes|
/ RootR GET / RootR GET
/subsite/#String SubsiteR Subsite getSubsite /subsite/#String SubsiteR Subsite getSubsite
|] |]
instance Yesod HelloWorld where approot _ = "" instance Yesod HelloWorld where
-- getRootR :: GHandler HelloWorld HelloWorld RepPlain -- FIXME remove type sig approot _ = ""
yepnopeJs _ = Just $ Left "http://cdnjs.cloudflare.com/ajax/libs/modernizr/2.0.6/modernizr.min.js"
getRootR = do getRootR = do
$(logOther "HAHAHA") "Here I am" $(logOther "HAHAHA") "Here I am"
return $ RepPlain "Hello World" defaultLayout $ do
addScriptRemote "https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"
toWidget [julius|$(function(){$("#mypara").css("color", "red")});|]
[whamlet|<p #mypara>Hello World|]
main = toWaiApp (HelloWorld Subsite) >>= run 3000 main = toWaiApp (HelloWorld Subsite) >>= run 3000

4
yesod-core/test.hs Normal file
View File

@ -0,0 +1,4 @@
import Test.Hspec
import qualified YesodCoreTest
main = hspecX $ descriptions $ YesodCoreTest.specs

View File

@ -0,0 +1,26 @@
module YesodCoreTest (specs) where
import YesodCoreTest.CleanPath
import YesodCoreTest.Exceptions
import YesodCoreTest.Widget
import YesodCoreTest.Media
import YesodCoreTest.Links
import YesodCoreTest.NoOverloadedStrings
import YesodCoreTest.InternalRequest
import YesodCoreTest.ErrorHandling
import YesodCoreTest.Cache
import Test.Hspec
specs :: [Specs]
specs =
[ cleanPathTest
, exceptionsTest
, widgetTest
, mediaTest
, linksTest
, noOverloadedTest
, internalRequestTest
, errorHandlingTest
, cacheTest
]

View File

@ -0,0 +1,50 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
module YesodCoreTest.Cache (cacheTest, Widget) where
import Test.Hspec
import Test.Hspec.HUnit()
import Network.Wai
import Network.Wai.Test
import Yesod.Core
data C = C
key :: CacheKey Int
key = $(mkCacheKey)
key2 :: CacheKey Int
key2 = $(mkCacheKey)
mkYesod "C" [parseRoutes|/ RootR GET|]
instance Yesod C where approot _ = ""
getRootR :: Handler ()
getRootR = do
Nothing <- cacheLookup key
cacheInsert key 5
Just 5 <- cacheLookup key
cacheInsert key 7
Just 7 <- cacheLookup key
Nothing <- cacheLookup key2
cacheDelete key
Nothing <- cacheLookup key
return ()
cacheTest :: [Spec]
cacheTest =
describe "Test.Cache"
[ it "works" works
]
runner :: Session () -> IO ()
runner f = toWaiApp C >>= runSession f
works :: IO ()
works = runner $ do
res <- request defaultRequest { pathInfo = [] }
assertStatus 200 res

View File

@ -1,7 +1,7 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
module Test.CleanPath (cleanPathTest, Widget) where module YesodCoreTest.CleanPath (cleanPathTest, Widget) where
import Test.Hspec import Test.Hspec
import Test.Hspec.HUnit() import Test.Hspec.HUnit()

View File

@ -0,0 +1,111 @@
{-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
module YesodCoreTest.ErrorHandling
( errorHandlingTest
, Widget
) where
import Yesod.Core
import Test.Hspec
import Test.Hspec.HUnit()
import Network.Wai
import Network.Wai.Test
import Text.Hamlet (hamlet)
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as S8
import Yesod.Internal.TestApi
import qualified Data.Enumerator as E
import qualified Data.Enumerator.List as EL
import Control.Exception (SomeException)
data App = App
mkYesod "App" [parseRoutes|
/ HomeR GET
/not_found NotFoundR POST
/first_thing FirstThingR POST
/after_runRequestBody AfterRunRequestBodyR POST
|]
instance Yesod App where approot _ = ""
getHomeR :: Handler RepHtml
getHomeR = defaultLayout $ toWidget [hamlet|
!!!
<html>
<body>
<form method=post action=@{NotFoundR}>
<input type=submit value="Not found">
<form method=post action=@{FirstThingR}>
<input type=submit value="Error is thrown first thing in handler">
<form method=post action=@{AfterRunRequestBodyR}>
<input type=submit value="BUGGY: Error thrown after runRequestBody">
|]
postNotFoundR, postFirstThingR, postAfterRunRequestBodyR :: Handler RepHtml
postNotFoundR = do
(_, _files) <- runRequestBody
_ <- notFound
getHomeR
postFirstThingR = do
_ <- error "There was an error 3.14159"
getHomeR
postAfterRunRequestBodyR = do
x <- runRequestBody
_ <- error $ show x
getHomeR
errorHandlingTest :: [Spec]
errorHandlingTest = describe "Test.ErrorHandling"
[ it "says not found" caseNotFound
, it "says 'There was an error' before runRequestBody" caseBefore
, it "says 'There was an error' after runRequestBody" caseAfter
, it "catchIter handles internal exceptions" caseCatchIter
]
runner :: Session () -> IO ()
runner f = toWaiApp App >>= runSession f
caseNotFound :: IO ()
caseNotFound = runner $ do
res <- request defaultRequest
{ pathInfo = ["not_found"]
, requestMethod = "POST"
}
assertStatus 404 res
assertBodyContains "Not Found" res
caseBefore :: IO ()
caseBefore = runner $ do
res <- request defaultRequest
{ pathInfo = ["first_thing"]
, requestMethod = "POST"
}
assertStatus 500 res
assertBodyContains "There was an error 3.14159" res
caseAfter :: IO ()
caseAfter = runner $ do
let content = "foo=bar&baz=bin12345"
res <- srequest SRequest
{ simpleRequest = defaultRequest
{ pathInfo = ["after_runRequestBody"]
, requestMethod = "POST"
, requestHeaders =
[ ("content-type", "application/x-www-form-urlencoded")
, ("content-length", S8.pack $ show $ L.length content)
]
}
, simpleRequestBody = content
}
assertStatus 500 res
assertBodyContains "bin12345" res
caseCatchIter :: IO ()
caseCatchIter = E.run_ $ E.enumList 8 (replicate 1000 "foo") E.$$ flip catchIter ignorer $ do
_ <- EL.consume
error "foo"
where
ignorer :: SomeException -> E.Iteratee a IO ()
ignorer _ = return ()

View File

@ -1,17 +1,19 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
module Test.Exceptions (exceptionsTest, Widget) where module YesodCoreTest.Exceptions (exceptionsTest, Widget) where
import Test.Hspec import Test.Hspec
import Test.Hspec.HUnit () import Test.Hspec.HUnit ()
import Yesod.Core hiding (Request) import Yesod.Core hiding (Request)
import Network.Wai
import Network.Wai.Test import Network.Wai.Test
data Y = Y data Y = Y
mkYesod "Y" [parseRoutes| mkYesod "Y" [parseRoutes|
/ RootR GET / RootR GET
/redirect RedirR GET
|] |]
instance Yesod Y where instance Yesod Y where
@ -22,9 +24,15 @@ instance Yesod Y where
getRootR :: Handler () getRootR :: Handler ()
getRootR = error "FOOBAR" >> return () getRootR = error "FOOBAR" >> return ()
getRedirR :: Handler ()
getRedirR = do
setHeader "foo" "bar"
redirect RedirectPermanent RootR
exceptionsTest :: [Spec] exceptionsTest :: [Spec]
exceptionsTest = describe "Test.Exceptions" exceptionsTest = describe "Test.Exceptions"
[ it "500" case500 [ it "500" case500
, it "redirect keeps headers" caseRedirect
] ]
runner :: Session () -> IO () runner :: Session () -> IO ()
@ -35,3 +43,9 @@ case500 = runner $ do
res <- request defaultRequest res <- request defaultRequest
assertStatus 500 res assertStatus 500 res
assertBody "FOOBAR" res assertBody "FOOBAR" res
caseRedirect :: IO ()
caseRedirect = runner $ do
res <- request defaultRequest { pathInfo = ["redirect"] }
assertStatus 301 res
assertHeader "foo" "bar" res

View File

@ -1,5 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module Test.InternalRequest (internalRequestTest) where module YesodCoreTest.InternalRequest (internalRequestTest) where
import Data.List (nub) import Data.List (nub)
import System.Random (StdGen, mkStdGen) import System.Random (StdGen, mkStdGen)
@ -30,16 +30,16 @@ g = undefined
nonceSpecs :: [Spec] nonceSpecs :: [Spec]
nonceSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqNonce)" nonceSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqNonce)"
[ it "is Nothing for unsecure sessions" noUnsecureNonce [ it "is Nothing if sessions are disabled" noDisabledNonce
, it "ignores pre-existing nonce for unsecure sessions" ignoreUnsecureNonce , it "ignores pre-existing nonce if sessions are disabled" ignoreDisabledNonce
, it "uses preexisting nonce for secure sessions" useOldNonce , it "uses preexisting nonce in session" useOldNonce
, it "generates a new nonce for secure sessions without nonce" generateNonce , it "generates a new nonce for sessions without nonce" generateNonce
] ]
noUnsecureNonce = reqNonce r == Nothing where noDisabledNonce = reqNonce r == Nothing where
r = parseWaiRequest' defaultRequest [] Nothing g r = parseWaiRequest' defaultRequest [] Nothing g
ignoreUnsecureNonce = reqNonce r == Nothing where ignoreDisabledNonce = reqNonce r == Nothing where
r = parseWaiRequest' defaultRequest [("_NONCE", "old")] Nothing g r = parseWaiRequest' defaultRequest [("_NONCE", "old")] Nothing g
useOldNonce = reqNonce r == Just "old" where useOldNonce = reqNonce r == Just "old" where

View File

@ -1,7 +1,7 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
module Test.Links (linksTest, Widget) where module YesodCoreTest.Links (linksTest, Widget) where
import Test.Hspec import Test.Hspec
import Test.Hspec.HUnit () import Test.Hspec.HUnit ()

View File

@ -1,7 +1,7 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
module Test.Media (mediaTest, Widget) where module YesodCoreTest.Media (mediaTest, Widget) where
import Test.Hspec import Test.Hspec
import Test.Hspec.HUnit () import Test.Hspec.HUnit ()

View File

@ -1,6 +1,6 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
module Test.NoOverloadedStrings (noOverloadedTest, Widget) where module YesodCoreTest.NoOverloadedStrings (noOverloadedTest, Widget) where
import Test.Hspec import Test.Hspec
import Test.Hspec.HUnit () import Test.Hspec.HUnit ()

View File

@ -1,7 +1,7 @@
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-} {-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
module Test.Widget (widgetTest) where module YesodCoreTest.Widget (widgetTest) where
import Test.Hspec import Test.Hspec
import Test.Hspec.HUnit () import Test.Hspec.HUnit ()

View File

@ -1,20 +0,0 @@
import Test.Hspec
import Test.CleanPath
import Test.Exceptions
import Test.Widget
import Test.Media
import Test.Links
import Test.NoOverloadedStrings
import Test.InternalRequest
main :: IO ()
main = hspecX $ descriptions $
[ cleanPathTest
, exceptionsTest
, widgetTest
, mediaTest
, linksTest
, noOverloadedTest
, internalRequestTest
]

1
yesod-core/test/test.hs Symbolic link
View File

@ -0,0 +1 @@
../test.hs

View File

@ -1,5 +1,5 @@
name: yesod-core name: yesod-core
version: 0.9.3 version: 0.9.4
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -14,6 +14,18 @@ stability: Stable
cabal-version: >= 1.8 cabal-version: >= 1.8
build-type: Simple build-type: Simple
homepage: http://www.yesodweb.com/ homepage: http://www.yesodweb.com/
extra-source-files:
test/en.msg
test/YesodCoreTest/NoOverloadedStrings.hs
test/YesodCoreTest/Media.hs
test/YesodCoreTest/Exceptions.hs
test/YesodCoreTest/Widget.hs
test/YesodCoreTest/CleanPath.hs
test/YesodCoreTest/Links.hs
test/YesodCoreTest/InternalRequest.hs
test/YesodCoreTest/ErrorHandling.hs
test/YesodCoreTest/Cache.hs
test.hs
flag test flag test
description: Build the executable to run unit tests description: Build the executable to run unit tests
@ -23,39 +35,51 @@ flag ghc7
library library
if flag(ghc7) if flag(ghc7)
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.3 && < 5
cpp-options: -DGHC7 cpp-options: -DGHC7
else else
build-depends: base >= 4 && < 4.3 build-depends: base >= 4 && < 4.3
build-depends: time >= 1.1.4 && < 1.4
, wai >= 0.4 && < 0.5 -- Work around a bug in cabal. Without this, wai-test doesn't get built and
, wai-extra >= 0.4.1 && < 0.5 -- we have a missing dependency during --enable-tests builds.
, bytestring >= 0.9.1.4 && < 0.10 if flag(test)
, text >= 0.5 && < 0.12 build-depends: wai-test
build-depends: time >= 1.1.4
, wai >= 0.4 && < 0.5
, wai-extra >= 0.4.1 && < 0.5
, bytestring >= 0.9.1.4 && < 0.10
, text >= 0.7 && < 0.12
, template-haskell , template-haskell
, path-pieces >= 0.0 && < 0.1 , path-pieces >= 0.0 && < 0.1
, hamlet >= 0.10 && < 0.11 , hamlet >= 0.10 && < 0.11
, shakespeare >= 0.10 && < 0.11 , shakespeare >= 0.10 && < 0.11
, shakespeare-js >= 0.10 && < 0.11 , shakespeare-js >= 0.10.4 && < 0.11
, shakespeare-css >= 0.10 && < 0.11 , shakespeare-css >= 0.10.5 && < 0.11
, blaze-builder >= 0.2.1 && < 0.4 , shakespeare-i18n >= 0.0 && < 0.1
, transformers >= 0.2 && < 0.3 , blaze-builder >= 0.2.1.4 && < 0.4
, clientsession >= 0.7.2 && < 0.8 , transformers >= 0.2.2 && < 0.3
, random >= 1.0.0.2 && < 1.1 , clientsession >= 0.7.3.1 && < 0.8
, cereal >= 0.2 && < 0.4 , random >= 1.0.0.2 && < 1.1
, old-locale >= 1.0.0.2 && < 1.1 , cereal >= 0.3 && < 0.4
, failure >= 0.1 && < 0.2 , old-locale >= 1.0.0.2 && < 1.1
, containers >= 0.2 && < 0.5 , failure >= 0.1 && < 0.2
, monad-control >= 0.2 && < 0.3 , containers >= 0.2 && < 0.5
, enumerator >= 0.4.7 && < 0.5 , monad-control >= 0.2 && < 0.4
, cookie >= 0.3 && < 0.4 , transformers-base >= 0.4
, blaze-html >= 0.4 && < 0.5 , enumerator >= 0.4.8 && < 0.5
, http-types >= 0.6.5 && < 0.7 , cookie >= 0.3 && < 0.4
, case-insensitive >= 0.2 && < 0.4 , blaze-html >= 0.4.1.3 && < 0.5
, parsec >= 2 && < 3.2 , http-types >= 0.6.5 && < 0.7
, directory >= 1 && < 1.2 , case-insensitive >= 0.2
, parsec >= 2 && < 3.2
, directory >= 1 && < 1.2
, data-object >= 0.3 && < 0.4
, data-object-yaml >= 0.3 && < 0.4
-- for logger. Probably logger should be a separate package -- for logger. Probably logger should be a separate package
, strict-concurrency >= 0.2.4 && < 0.2.5 , strict-concurrency >= 0.2.4 && < 0.2.5
, vector >= 0.9 && < 0.10
, aeson >= 0.3
exposed-modules: Yesod.Content exposed-modules: Yesod.Content
Yesod.Core Yesod.Core
@ -65,8 +89,10 @@ library
Yesod.Request Yesod.Request
Yesod.Widget Yesod.Widget
Yesod.Message Yesod.Message
Yesod.Config
Yesod.Internal.TestApi Yesod.Internal.TestApi
other-modules: Yesod.Internal other-modules: Yesod.Internal
Yesod.Internal.Cache
Yesod.Internal.Core Yesod.Internal.Core
Yesod.Internal.Session Yesod.Internal.Session
Yesod.Internal.Request Yesod.Internal.Request
@ -74,26 +100,24 @@ library
Yesod.Internal.RouteParsing Yesod.Internal.RouteParsing
Paths_yesod_core Paths_yesod_core
ghc-options: -Wall ghc-options: -Wall
if flag(test)
Buildable: False
test-suite runtests test-suite tests
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
main-is: main.hs main-is: test.hs
hs-source-dirs: test hs-source-dirs: test
if flag(ghc7) if flag(ghc7)
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
build-depends: base >= 4.3 && < 5 build-depends: base >= 4.3 && < 5
cpp-options: -DGHC7 cpp-options: -DGHC7
main-is: main.hs main-is: test.hs
else else
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
build-depends: base >= 4 && < 4.3 build-depends: base >= 4 && < 4.3
main-is: main.hs main-is: test.hs
cpp-options: -DTEST cpp-options: -DTEST
build-depends: hspec >= 0.8 && < 0.9 build-depends: hspec >= 0.8 && < 0.10
,wai-test ,wai-test >= 0.1.2 && < 0.2
,wai ,wai
,yesod-core ,yesod-core
,bytestring ,bytestring
@ -105,6 +129,7 @@ test-suite runtests
, random , random
,HUnit ,HUnit
,QuickCheck >= 2 && < 3 ,QuickCheck >= 2 && < 3
, enumerator
ghc-options: -Wall ghc-options: -Wall
source-repository head source-repository head

25
yesod-default/LICENSE Normal file
View File

@ -0,0 +1,25 @@
The following license covers this documentation, and the source code, except
where otherwise indicated.
Copyright 2010, Michael Snoyman. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,226 @@
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
module Yesod.Default.Config
( DefaultEnv (..)
, fromArgs
, fromArgsExtra
, loadDevelopmentConfig
-- reexport
, AppConfig (..)
, ConfigSettings (..)
, configSettings
, loadConfig
, withYamlEnvironment
) where
import Data.Char (toUpper, toLower)
import System.Console.CmdArgs hiding (args)
import Data.Text (Text)
import qualified Data.Text as T
import Control.Monad (join)
import Data.Object
import Data.Object.Yaml
import Data.Maybe (fromMaybe)
-- | A yesod-provided @'AppEnv'@, allows for Development, Testing, and
-- Production environments
data DefaultEnv = Development
| Testing
| Staging
| Production deriving (Read, Show, Enum, Bounded)
-- | Setup commandline arguments for environment and port
data ArgConfig = ArgConfig
{ environment :: String
, port :: Int
} deriving (Show, Data, Typeable)
-- | A default @'ArgConfig'@ if using the provided @'DefaultEnv'@ type.
defaultArgConfig :: ArgConfig
defaultArgConfig =
ArgConfig
{ environment = def
&= argPos 0
&= typ "ENVIRONMENT"
, port = def
&= help "the port to listen on"
&= typ "PORT"
}
-- | Load an @'AppConfig'@ using the @'DefaultEnv'@ environments from
-- commandline arguments.
fromArgs :: IO (AppConfig DefaultEnv ())
fromArgs = fromArgsExtra (const $ const $ return ())
-- | Same as 'fromArgs', but allows you to specify how to parse the 'appExtra'
-- record.
fromArgsExtra :: (DefaultEnv -> TextObject -> IO extra)
-> IO (AppConfig DefaultEnv extra)
fromArgsExtra = fromArgsWith defaultArgConfig
fromArgsWith :: (Read env, Show env)
=> ArgConfig
-> (env -> TextObject -> IO extra)
-> IO (AppConfig env extra)
fromArgsWith argConfig getExtra = do
args <- cmdArgs argConfig
env <-
case reads $ capitalize $ environment args of
(e, _):_ -> return e
[] -> error $ "Invalid environment: " ++ environment args
let cs = (configSettings env)
{ csLoadExtra = getExtra
}
config <- loadConfig cs
return $ if port args /= 0
then config { appPort = port args }
else config
where
capitalize [] = []
capitalize (x:xs) = toUpper x : map toLower xs
-- | Load your development config (when using @'DefaultEnv'@)
loadDevelopmentConfig :: IO (AppConfig DefaultEnv ())
loadDevelopmentConfig = loadConfig $ configSettings Development
-- | Dynamic per-environment configuration which can be loaded at
-- run-time negating the need to recompile between environments.
data AppConfig environment extra = AppConfig
{ appEnv :: environment
, appPort :: Int
, appRoot :: Text
, appExtra :: extra
} deriving (Show)
data ConfigSettings environment extra = ConfigSettings
{
-- | An arbitrary value, used below, to indicate the current running
-- environment. Usually, you will use 'DefaultEnv' for this type.
csEnv :: environment
-- | Load any extra data, to be used by the application.
, csLoadExtra :: environment -> TextObject -> IO extra
-- | Return the path to the YAML config file.
, csFile :: environment -> IO FilePath
-- | Get the sub-object (if relevant) from the given YAML source which
-- contains the specific settings for the current environment.
, csGetObject :: environment -> TextObject -> IO TextObject
}
-- | Default config settings.
configSettings :: Show env => env -> ConfigSettings env ()
configSettings env0 = ConfigSettings
{ csEnv = env0
, csLoadExtra = \_ _ -> return ()
, csFile = \_ -> return "config/settings.yml"
, csGetObject = \env obj -> do
envs <- fromMapping obj
let senv = show env
tenv = T.pack senv
maybe
(error $ "Could not find environment: " ++ senv)
return
(lookup tenv envs)
}
-- | Load an @'AppConfig'@.
--
-- Some examples:
--
-- > -- typical local development
-- > Development:
-- > host: localhost
-- > port: 3000
-- >
-- > -- ssl: will default false
-- > -- approot: will default to "http://localhost:3000"
--
-- > -- typical outward-facing production box
-- > Production:
-- > host: www.example.com
-- >
-- > -- ssl: will default false
-- > -- port: will default 80
-- > -- approot: will default "http://www.example.com"
--
-- > -- maybe you're reverse proxying connections to the running app
-- > -- on some other port
-- > Production:
-- > port: 8080
-- > approot: "http://example.com"
-- >
-- > -- approot is specified so that the non-80 port is not appended
-- > -- automatically.
--
loadConfig :: ConfigSettings environment extra
-> IO (AppConfig environment extra)
loadConfig (ConfigSettings env loadExtra getFile getObject) = do
fp <- getFile env
topObj <- join $ decodeFile fp
obj <- getObject env topObj
m <- maybe (fail "Expected map") return $ fromMapping obj
let mssl = lookupScalar "ssl" m
let mhost = lookupScalar "host" m
let mport = lookupScalar "port" m
let mapproot = lookupScalar "approot" m
extra <- loadExtra env obj
-- set some default arguments
let ssl = maybe False toBool mssl
port' <- safeRead "port" $ fromMaybe (if ssl then "443" else "80") mport
approot <- case (mhost, mapproot) of
(_ , Just ar) -> return ar
(Just host, _ ) -> return $ T.concat
[ if ssl then "https://" else "http://"
, host
, addPort ssl port'
]
_ -> fail "You must supply either a host or approot"
return $ AppConfig
{ appEnv = env
, appPort = port'
, appRoot = approot
, appExtra = extra
}
where
toBool :: Text -> Bool
toBool = (`elem` ["true", "TRUE", "yes", "YES", "Y", "1"])
addPort :: Bool -> Int -> Text
addPort True 443 = ""
addPort False 80 = ""
addPort _ p = T.pack $ ':' : show p
-- | Returns 'fail' if read fails
safeRead :: Monad m => String -> Text -> m Int
safeRead name' t = case reads s of
(i, _):_ -> return i
[] -> fail $ concat ["Invalid value for ", name', ": ", s]
where
s = T.unpack t
-- | Loads the configuration block in the passed file named by the
-- passed environment, yeilds to the passed function as a mapping.
--
-- Errors in the case of a bad load or if your function returns
-- @Nothing@.
withYamlEnvironment :: Show e
=> FilePath -- ^ the yaml file
-> e -- ^ the environment you want to load
-> (TextObject -> IO a) -- ^ what to do with the mapping
-> IO a
withYamlEnvironment fp env f = do
obj <- join $ decodeFile fp
envs <- fromMapping obj
conf <- maybe (fail $ "Could not find environment: " ++ show env) return
$ lookup (T.pack $ show env) envs
f conf

View File

@ -0,0 +1,14 @@
{-# LANGUAGE OverloadedStrings #-}
module Yesod.Default.Handlers
( getFaviconR
, getRobotsR
) where
import Yesod.Handler (GHandler, sendFile)
import Yesod.Content (RepPlain(..))
getFaviconR :: GHandler s m ()
getFaviconR = sendFile "image/x-icon" "config/favicon.ico"
getRobotsR :: GHandler s m RepPlain
getRobotsR = sendFile "text/plain" "config/robots.txt"

View File

@ -0,0 +1,115 @@
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Yesod.Default.Main
( defaultMain
, defaultRunner
, defaultDevelApp
, defaultDevelAppWith
) where
import Yesod.Core hiding (AppConfig (..))
import Yesod.Default.Config
import Yesod.Logger (Logger, makeLogger, logString, logLazyText, flushLogger)
import Network.Wai (Application)
import Network.Wai.Handler.Warp (run)
import Network.Wai.Middleware.Debug (debugHandle)
import System.Directory (doesDirectoryExist, removeDirectoryRecursive)
import Network.Wai.Middleware.Gzip (gzip', GzipFiles (GzipCacheFolder), gzipFiles, def)
import Network.Wai.Middleware.Autohead (autohead)
import Network.Wai.Middleware.Jsonp (jsonp)
import Control.Monad (when)
#ifndef WINDOWS
import qualified System.Posix.Signals as Signal
import Control.Concurrent (forkIO, killThread)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
#endif
-- | Run your app, taking environment and port settings from the
-- commandline.
--
-- Use @'fromArgs'@ when using the provided @'DefaultEnv'@ type, or
-- @'fromArgsWith'@ when using a custom type
--
-- > main :: IO ()
-- > main = defaultMain fromArgs withMySite
--
-- or
--
-- > main :: IO ()
-- > main = defaultMain (fromArgsWith customArgConfig) withMySite
--
defaultMain :: (Show env, Read env)
=> IO (AppConfig env extra)
-> (AppConfig env extra -> Logger -> (Application -> IO ()) -> IO ())
-> IO ()
defaultMain load withSite = do
config <- load
logger <- makeLogger
withSite config logger $ run (appPort config)
-- | Run your application continously, listening for SIGINT and exiting
-- when recieved
--
-- > withYourSite :: AppConfig DefaultEnv -> Logger -> (Application -> IO a) -> IO ()
-- > withYourSite conf logger f = do
-- > Settings.withConnectionPool conf $ \p -> do
-- > runConnectionPool (runMigration yourMigration) p
-- > defaultRunner f $ YourSite conf logger p
--
-- TODO: ifdef WINDOWS
--
defaultRunner :: (YesodDispatch y y, Yesod y)
=> (Application -> IO a)
-> y -- ^ your foundation type
-> IO ()
defaultRunner f h = do
-- clear the .static-cache so we don't have stale content
exists <- doesDirectoryExist staticCache
when exists $ removeDirectoryRecursive staticCache
#ifdef WINDOWS
toWaiAppPlain h >>= f . middlewares >> return ()
#else
tid <- forkIO $ toWaiAppPlain h >>= f . middlewares >> return ()
flag <- newEmptyMVar
_ <- Signal.installHandler Signal.sigINT (Signal.CatchOnce $ do
putStrLn "Caught an interrupt"
killThread tid
putMVar flag ()) Nothing
takeMVar flag
#endif
where
middlewares = gzip' gset . jsonp . autohead
gset = def { gzipFiles = GzipCacheFolder staticCache }
staticCache = ".static-cache"
-- | Run your development app using the provided @'DefaultEnv'@ type
--
-- > withDevelAppPort :: Dynamic
-- > withDevelAppPort = toDyn $ defaultDevelApp withMySite
--
defaultDevelApp :: (AppConfig DefaultEnv () -> Logger -> (Application -> IO ()) -> IO ())
-> ((Int, Application) -> IO ())
-> IO ()
defaultDevelApp = defaultDevelAppWith loadDevelopmentConfig
-- | Run your development app using a custom environment type and loader
-- function
--
-- > withDevelAppPort :: Dynamic
-- > withDevelAppPort = toDyn $ (defaultDevelAppWith customLoadAppConfig) withMySite
--
defaultDevelAppWith :: (Show env, Read env)
=> IO (AppConfig env extra) -- ^ A means to load your development @'AppConfig'@
-> (AppConfig env extra -> Logger -> (Application -> IO ()) -> IO ()) -- ^ Your @withMySite@ function
-> ((Int, Application) -> IO ()) -> IO ()
defaultDevelAppWith load withSite f = do
conf <- load
logger <- makeLogger
let p = appPort conf
logString logger $ "Devel application launched, listening on port " ++ show p
withSite conf logger $ \app -> f (p, debugHandle (logHandle logger) app)
flushLogger logger
where
logHandle logger msg = logLazyText logger msg >> flushLogger logger

View File

@ -0,0 +1,79 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE CPP #-}
-- | Various utilities used in the scaffolded site.
module Yesod.Default.Util
( addStaticContentExternal
, globFile
, widgetFileNoReload
, widgetFileReload
) where
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString.Lazy as L
import Data.Text (Text, pack, unpack)
import Yesod.Core -- purposely using complete import so that Haddock will see addStaticContent
import Control.Monad (unless)
import System.Directory (doesFileExist, createDirectoryIfMissing)
import Language.Haskell.TH.Syntax
import Text.Lucius (luciusFile, luciusFileReload)
import Text.Julius (juliusFile, juliusFileReload)
import Text.Cassius (cassiusFile, cassiusFileReload)
import Data.Monoid (mempty)
-- | An implementation of 'addStaticContent' which stores the contents in an
-- external file. Files are created in the given static folder with names based
-- on a hash of their content. This allows expiration dates to be set far in
-- the future without worry of users receiving stale content.
addStaticContentExternal
:: (L.ByteString -> Either a L.ByteString) -- ^ javascript minifier
-> (L.ByteString -> String) -- ^ hash function to determine file name
-> FilePath -- ^ location of static directory. files will be placed within a "tmp" subfolder
-> ([Text] -> Route master) -- ^ route constructor, taking a list of pieces
-> Text -- ^ filename extension
-> Text -- ^ mime type
-> L.ByteString -- ^ file contents
-> GHandler sub master (Maybe (Either Text (Route master, [(Text, Text)])))
addStaticContentExternal minify hash staticDir toRoute ext' _ content = do
liftIO $ createDirectoryIfMissing True statictmp
exists <- liftIO $ doesFileExist fn'
unless exists $ liftIO $ L.writeFile fn' content'
return $ Just $ Right (toRoute ["tmp", pack fn], [])
where
fn, statictmp, fn' :: FilePath
-- by basing the hash off of the un-minified content, we avoid a costly
-- minification if the file already exists
fn = hash content ++ '.' : unpack ext'
statictmp = staticDir ++ "/tmp/"
fn' = statictmp ++ fn
content' :: L.ByteString
content'
| ext' == "js" = either (const content) id $ minify content
| otherwise = content
-- | expects a file extension for each type, e.g: hamlet lucius julius
globFile :: String -> String -> FilePath
globFile kind x = "templates/" ++ x ++ "." ++ kind
widgetFileNoReload :: FilePath -> Q Exp
widgetFileNoReload x = do
let h = whenExists x "hamlet" whamletFile
let c = whenExists x "cassius" cassiusFile
let j = whenExists x "julius" juliusFile
let l = whenExists x "lucius" luciusFile
[|$h >> addCassius $c >> addJulius $j >> addLucius $l|]
widgetFileReload :: FilePath -> Q Exp
widgetFileReload x = do
let h = whenExists x "hamlet" whamletFile
let c = whenExists x "cassius" cassiusFileReload
let j = whenExists x "julius" juliusFileReload
let l = whenExists x "lucius" luciusFileReload
[|$h >> addCassius $c >> addJulius $j >> addLucius $l|]
whenExists :: String -> String -> (FilePath -> Q Exp) -> Q Exp
whenExists x glob f = do
let fn = globFile glob x
e <- qRunIO $ doesFileExist fn
if e then f fn else [|mempty|]

View File

@ -0,0 +1,48 @@
name: yesod-default
version: 0.5.0
license: BSD3
license-file: LICENSE
author: Patrick Brisbin
maintainer: Patrick Brisbin <pbrisbin@gmail.com>
synopsis: Default config and main functions for your yesod application
category: Web, Yesod
stability: Stable
cabal-version: >= 1.6
build-type: Simple
homepage: http://www.yesodweb.com/
description: Convenient wrappers for your the configuration and
execution of your yesod application
library
if os(windows)
cpp-options: -DWINDOWS
build-depends: base >= 4 && < 5
, yesod-core >= 0.9.4 && < 0.10
, cmdargs >= 0.8
, warp >= 0.4 && < 0.5
, wai >= 0.4 && < 0.5
, wai-extra >= 0.4.4 && < 0.5
, bytestring >= 0.9.1.4
, transformers >= 0.2.2 && < 0.3
, text >= 0.9
, directory >= 1.0
, shakespeare-css >= 0.10.5 && < 0.11
, shakespeare-js >= 0.10.4 && < 0.11
, template-haskell
, data-object >= 0.3 && < 0.4
, data-object-yaml >= 0.3 && < 0.4
if !os(windows)
build-depends: unix
exposed-modules: Yesod.Default.Config
, Yesod.Default.Main
, Yesod.Default.Util
, Yesod.Default.Handlers
ghc-options: -Wall
source-repository head
type: git
location: git://github.com/yesodweb/yesod.git

View File

@ -5,15 +5,14 @@
> {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} > {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
> import Yesod > import Yesod
> import Yesod.Static > import Yesod.Static
> import Data.Monoid (mempty) > import Data.Text (Text, unpack)
> import Text.Blaze (string)
Like the blog example, we'll define some data first. Like the blog example, we'll define some data first.
> data Page = Page > data Page = Page
> { pageName :: String > { pageName :: Text
> , pageSlug :: String > , pageSlug :: Text
> , pageContent :: String > , pageContent :: Text
> } > }
> loadPages :: IO [Page] > loadPages :: IO [Page]
@ -36,7 +35,7 @@ Now the routes; we'll have a homepage, a pattern for the pages, and use a static
> mkYesod "Ajax" [parseRoutes| > mkYesod "Ajax" [parseRoutes|
> / HomeR GET > / HomeR GET
> /page/#String PageR GET > /page/#Text PageR GET
> /static StaticR Static ajaxStatic > /static StaticR Static ajaxStatic
> |] > |]
@ -49,7 +48,7 @@ Now the routes; we'll have a homepage, a pattern for the pages, and use a static
> defaultLayout widget = do > defaultLayout widget = do
> Ajax pages _ <- getYesod > Ajax pages _ <- getYesod
> content <- widgetToPageContent widget > content <- widgetToPageContent widget
> hamletToRepHtml [$hamlet| > hamletToRepHtml [hamlet|
> \<!DOCTYPE html> > \<!DOCTYPE html>
> >
> <html> > <html>
@ -80,23 +79,23 @@ Now the routes; we'll have a homepage, a pattern for the pages, and use a static
And now the cool part: a handler that returns either HTML or JSON data, depending on the request headers. And now the cool part: a handler that returns either HTML or JSON data, depending on the request headers.
> getPageR :: String -> Handler RepHtmlJson > getPageR :: Text -> Handler RepHtmlJson
> getPageR slug = do > getPageR slug = do
> Ajax pages _ <- getYesod > Ajax pages _ <- getYesod
> case filter (\e -> pageSlug e == slug) pages of > case filter (\e -> pageSlug e == slug) pages of
> [] -> notFound > [] -> notFound
> page:_ -> defaultLayoutJson (do > page:_ -> defaultLayoutJson (do
> setTitle $ string $ pageName page > setTitle $ toHtml $ pageName page
> addHamlet $ html page > addHamlet $ html page
> ) (json page) > ) (json page)
> where > where
> html page = [$hamlet| > html page = [hamlet|
> <h1>#{pageName page} > <h1>#{pageName page}
> <article>#{pageContent page} > <article>#{pageContent page}
> |] > |]
> json page = jsonMap > json page = jsonMap
> [ ("name", jsonScalar $ pageName page) > [ ("name", jsonScalar $ unpack $ pageName page)
> , ("content", jsonScalar $ pageContent page) > , ("content", jsonScalar $ unpack $ pageContent page)
> ] > ]
<p>We first try and find the appropriate Page, returning a 404 if it's not there. We then use the applyLayoutJson function, which is really the heart of this example. It allows you an easy way to create responses that will be either HTML or JSON, and which use the default layout in the HTML responses. It takes four arguments: 1) the title of the HTML page, 2) some value, 3) a function from that value to a Hamlet value, and 4) a function from that value to a Json value.</p> <p>We first try and find the appropriate Page, returning a 404 if it's not there. We then use the applyLayoutJson function, which is really the heart of this example. It allows you an easy way to create responses that will be either HTML or JSON, and which use the default layout in the HTML responses. It takes four arguments: 1) the title of the HTML page, 2) some value, 3) a function from that value to a Hamlet value, and 4) a function from that value to a Json value.</p>
@ -110,3 +109,8 @@ And now the cool part: a handler that returns either HTML or JSON data, dependin
> pages <- loadPages > pages <- loadPages
> s <- static "static/yesod/ajax" > s <- static "static/yesod/ajax"
> warpDebug 3000 $ Ajax pages s > warpDebug 3000 $ Ajax pages s
And just to avoid some warnings...
> _ignored :: Widget
> _ignored = undefined ajaxPages

View File

@ -113,3 +113,8 @@ All that's left now is the main function. Yesod is built on top of WAI, so you c
> main = do > main = do
> entries <- loadEntries > entries <- loadEntries
> warpDebug 3000 $ Blog entries > warpDebug 3000 $ Blog entries
And this is just to avoid some warnings...
> _ignored :: Widget
> _ignored = undefined blogEntries

View File

@ -6,11 +6,9 @@
module Main where module Main where
import Yesod import Yesod
import Yesod.Helpers.Static import Yesod.Static
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Concurrent.STM.TChan
import Control.Concurrent.STM.TVar
import Control.Arrow ((***)) import Control.Arrow ((***))
import Data.Text (Text, unpack) import Data.Text (Text, unpack)
@ -18,8 +16,6 @@ import Data.Text (Text, unpack)
-- speaker and content -- speaker and content
data Message = Message Text Text data Message = Message Text Text
type Handler yesod = GHandler yesod yesod
-- all those TChans are dupes, so writing to any one writes to them all, but reading is separate -- all those TChans are dupes, so writing to any one writes to them all, but reading is separate
data Chat = Chat data Chat = Chat
{ chatClients :: TVar [(Int, TChan Message)] { chatClients :: TVar [(Int, TChan Message)]
@ -29,7 +25,7 @@ data Chat = Chat
staticFiles "static" staticFiles "static"
mkYesod "Chat" [$parseRoutes| mkYesod "Chat" [parseRoutes|
/ HomeR GET / HomeR GET
/check CheckR GET /check CheckR GET
/post PostR GET /post PostR GET
@ -40,21 +36,20 @@ instance Yesod Chat where
approot _ = "" approot _ = ""
defaultLayout widget = do defaultLayout widget = do
content <- widgetToPageContent widget content <- widgetToPageContent widget
hamletToRepHtml [$hamlet|\ hamletToRepHtml [hamlet|
\<!DOCTYPE html> !!!
<html> <html>
<head> <head>
<title>#{pageTitle content} <title>#{pageTitle content}
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js">
<script src="@{StaticR chat_js}"> <script src="@{StaticR chat_js}">
\^{pageHead content} ^{pageHead content}
<body> <body>
\^{pageBody content} ^{pageBody content}
\
|] |]
getHomeR :: Handler Chat RepHtml getHomeR :: Handler RepHtml
getHomeR = do getHomeR = do
Chat clients next _ <- getYesod Chat clients next _ <- getYesod
client <- liftIO . atomically $ do client <- liftIO . atomically $ do
@ -68,8 +63,8 @@ getHomeR = do
return c return c
defaultLayout $ do defaultLayout $ do
setTitle "Chat Page" setTitle "Chat Page"
addWidget [$hamlet|\ toWidget [hamlet|
\<!DOCTYPE html> !!!
<h1>Chat Example <h1>Chat Example
<form> <form>
@ -81,7 +76,7 @@ getHomeR = do
<script>var clientNumber = #{show client} <script>var clientNumber = #{show client}
|] |]
getCheckR :: Handler Chat RepJson getCheckR :: Handler RepJson
getCheckR = do getCheckR = do
liftIO $ putStrLn "Check" liftIO $ putStrLn "Check"
Chat clients _ _ <- getYesod Chat clients _ _ <- getYesod
@ -99,9 +94,10 @@ getCheckR = do
let Message s c = first let Message s c = first
jsonToRepJson $ zipJson ["sender", "content"] [s,c] jsonToRepJson $ zipJson ["sender", "content"] [s,c]
zipJson :: [Text] -> [Text] -> Json
zipJson x y = jsonMap $ map (unpack *** jsonScalar . unpack) $ zip x y zipJson x y = jsonMap $ map (unpack *** jsonScalar . unpack) $ zip x y
getPostR :: Handler Chat RepJson getPostR :: Handler RepJson
getPostR = do getPostR = do
liftIO $ putStrLn "Post" liftIO $ putStrLn "Post"
Chat clients _ _ <- getYesod Chat clients _ _ <- getYesod
@ -122,4 +118,5 @@ main :: IO ()
main = do main = do
clients <- newTVarIO [] clients <- newTVarIO []
next <- newTVarIO 0 next <- newTVarIO 0
warpDebug 3000 $ Chat clients next $ static "static" s <- static "static"
warpDebug 3000 $ Chat clients next s

View File

@ -1,10 +1,8 @@
> {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-} > {-# LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings #-}
> import Yesod > import Yesod
> import Data.Monoid (mempty)
> import qualified Data.ByteString.Char8 as S8 > import qualified Data.ByteString.Char8 as S8
> import qualified Data.Text as T > import qualified Data.Text as T
> import Text.Blaze (string)
> data Echo = Echo > data Echo = Echo
@ -14,18 +12,26 @@
> instance Yesod Echo where approot _ = "" > instance Yesod Echo where approot _ = ""
> getHomepage :: Handler RepHtml
> getHomepage = defaultLayout $ do > getHomepage = defaultLayout $ do
> setTitle $ string "Upload a file" > setTitle "Upload a file"
> addHamlet [$hamlet| > addHamlet [hamlet|
> %form!method=post!action=.!enctype=multipart/form-data > <form method=post action=. enctype=multipart/form-data>
> File name: > File name:
> %input!type=file!name=file > <input type=file name=file
> %input!type=submit > <input type=submit
> |] > |]
> postHomepage :: Handler [(ContentType, Content)]
> postHomepage = do > postHomepage = do
> (_, files) <- runRequestBody > (_, files) <- runRequestBody
> fi <- maybe notFound return $ lookup "file" files > fi <- maybe notFound return $ lookup "file" files
> return [(S8.pack $ T.unpack $ fileContentType fi, toContent $ fileContent fi)] > return [(S8.pack $ T.unpack $ fileContentType fi, toContent $ fileContent fi)]
> main :: IO ()
> main = warpDebug 3000 Echo > main = warpDebug 3000 Echo
To avoid warnings
> _ignored :: Widget
> _ignored = undefined

View File

@ -2,7 +2,7 @@
> {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings, MultiParamTypeClasses, TemplateHaskell #-} > {-# LANGUAGE TypeFamilies, QuasiQuotes, OverloadedStrings, MultiParamTypeClasses, TemplateHaskell #-}
> import Yesod > import Yesod hiding (Form)
> import Control.Applicative > import Control.Applicative
> import Data.Text (Text) > import Data.Text (Text)
@ -10,6 +10,8 @@
> mkYesod "FormExample" [parseRoutes| > mkYesod "FormExample" [parseRoutes|
> / RootR GET > / RootR GET
> |] > |]
> type Form a = Html -> MForm FormExample FormExample (FormResult a, Widget)
> type Formlet a = Maybe a -> Form a
> instance Yesod FormExample where approot _ = "" > instance Yesod FormExample where approot _ = ""
> instance RenderMessage FormExample FormMessage where > instance RenderMessage FormExample FormMessage where
> renderMessage _ _ = defaultFormMessage > renderMessage _ _ = defaultFormMessage
@ -18,6 +20,7 @@ Next, we'll declare a Person datatype with a name and age. After that, we'll cre
> data Person = Person { name :: Text, age :: Int } > data Person = Person { name :: Text, age :: Int }
> deriving Show > deriving Show
> personFormlet :: Formlet Person
> personFormlet p = renderTable $ Person > personFormlet p = renderTable $ Person
> <$> areq textField "Name" (fmap name p) > <$> areq textField "Name" (fmap name p)
> <*> areq intField "Age" (fmap age p) > <*> areq intField "Age" (fmap age p)
@ -38,14 +41,15 @@ We use an applicative approach and stay mostly declarative. The "fmap name p" bi
<p>extractBody returns the HTML of a widget and "passes" all of the other declarations (the CSS, Javascript, etc) up to the parent widget. The rest of this is just standard Hamlet code and our main function.</p> <p>extractBody returns the HTML of a widget and "passes" all of the other declarations (the CSS, Javascript, etc) up to the parent widget. The rest of this is just standard Hamlet code and our main function.</p>
> addHamlet [$hamlet| > addHamlet [hamlet|
> <p>Last result: #{show res} > <p>Last result: #{show res}
> <form enctype="#{enctype}"> > <form enctype="#{enctype}">
> <table> > <table>
> \^{form} > ^{form}
> <tr> > <tr>
> <td colspan="2"> > <td colspan="2">
> <input type="submit"> > <input type="submit">
> |] > |]
> >
> main :: IO ()
> main = warpDebug 3000 FormExample > main = warpDebug 3000 FormExample

View File

@ -1,52 +0,0 @@
This example shows how generalized hamlet templates allow the creation of
different types of values. The key component here is the HamletValue typeclass.
Yesod has instances for:
* Html
* HtmlUrl (= (url -> [(String, String)] -> String) -> Html)
* GWidget s m ()
This example uses all three. You are of course free in your own code to make
your own instances.
> {-# LANGUAGE QuasiQuotes, TypeFamilies, MultiParamTypeClasses, OverloadedStrings, TemplateHaskell #-}
> import Yesod
> import Text.Hamlet (shamlet)
> data NewHamlet = NewHamlet
> mkYesod "NewHamlet" [$parseRoutes|/ RootR GET|]
> instance Yesod NewHamlet where approot _ = ""
>
> myHtml :: Html
> myHtml = [shamlet|<p>Just don't use any URLs in here!|]
>
> myInnerWidget :: Widget
> myInnerWidget = do
> addHamlet [$hamlet|
> <div #inner>Inner widget
> #{myHtml}
> |]
> addCassius [$cassius|
>#inner
> color: red|]
>
> myPlainTemplate :: HtmlUrl NewHamletRoute
> myPlainTemplate = [hamlet|
> <p
> <a href=@{RootR}>Link to home
> |]
>
> myWidget :: Widget
> myWidget = [whamlet|
> <h1>Embed another widget
> \^{myInnerWidget}
> <h1>Embed a Hamlet
> \^{addHamlet myPlainTemplate}
> |]
>
> getRootR :: GHandler NewHamlet NewHamlet RepHtml
> getRootR = defaultLayout myWidget
>
> main :: IO ()
> main = warpDebug 3000 NewHamlet

View File

@ -3,14 +3,14 @@
> {-# LANGUAGE TypeFamilies #-} > {-# LANGUAGE TypeFamilies #-}
> {-# LANGUAGE MultiParamTypeClasses #-} > {-# LANGUAGE MultiParamTypeClasses #-}
> {-# LANGUAGE OverloadedStrings #-} > {-# LANGUAGE OverloadedStrings #-}
> {-# LANGUAGE CPP #-}
> import Yesod > import Yesod
> import Data.Monoid (mempty)
> import Data.Text (Text) > import Data.Text (Text)
> data I18N = I18N > data I18N = I18N
> mkYesod "I18N" [$parseRoutes| > mkYesod "I18N" [parseRoutes|
> / HomepageR GET > / HomepageR GET
> /set/#Text SetLangR GET > /set/#Text SetLangR GET
> |] > |]
@ -24,12 +24,12 @@
> let hello = chooseHello ls > let hello = chooseHello ls
> let choices = > let choices =
> [ ("en", "English") :: (Text, Text) > [ ("en", "English") :: (Text, Text)
> , ("es", "Spanish") > , ("es", "Español")
> , ("he", "Hebrew") > , ("he", "עִבְרִית")
> ] > ]
> defaultLayout $ do > defaultLayout $ do
> setTitle "I18N Homepage" > setTitle "I18N Homepage"
> addHamlet [$hamlet| > addHamlet [hamlet|
> <h1>#{hello} > <h1>#{hello}
> <p>In other languages: > <p>In other languages:
> <ul> > <ul>
@ -40,8 +40,8 @@
> chooseHello :: [Text] -> Text > chooseHello :: [Text] -> Text
> chooseHello [] = "Hello" > chooseHello [] = "Hello"
> chooseHello ("he":_) = "Shalom" > chooseHello ("he":_) = "שלום"
> chooseHello ("es":_) = "Hola" > chooseHello ("es":_) = "¡Hola!"
> chooseHello (_:rest) = chooseHello rest > chooseHello (_:rest) = chooseHello rest
> getSetLangR :: Text -> Handler () > getSetLangR :: Text -> Handler ()
@ -51,3 +51,6 @@
> main :: IO () > main :: IO ()
> main = warpDebug 3000 I18N > main = warpDebug 3000 I18N
> _ignored :: Widget
> _ignored = undefined

View File

@ -10,14 +10,14 @@
> data PY = PY > data PY = PY
> mkYesod "PY" [$parseRoutes| > mkYesod "PY" [parseRoutes|
> / Homepage GET POST > / Homepage GET POST
> |] > |]
> instance Yesod PY where approot _ = "" > instance Yesod PY where approot _ = ""
> template :: Maybe (HtmlUrl url) -> HtmlUrl url > template :: Maybe (HtmlUrl url) -> HtmlUrl url
> template myaml = [$hamlet| > template myaml = [hamlet|
> !!! > !!!
> >
> <html> > <html>
@ -46,13 +46,13 @@
> hamletToRepHtml $ template $ Just $ objToHamlet so > hamletToRepHtml $ template $ Just $ objToHamlet so
> objToHamlet :: StringObject -> HtmlUrl url > objToHamlet :: StringObject -> HtmlUrl url
> objToHamlet (Scalar s) = [$hamlet|#{s}|] > objToHamlet (Scalar s) = [hamlet|#{s}|]
> objToHamlet (Sequence list) = [$hamlet| > objToHamlet (Sequence list) = [hamlet|
> <ul > <ul
> $forall o <- list > $forall o <- list
> <li>^{objToHamlet o} > <li>^{objToHamlet o}
> |] > |]
> objToHamlet (Mapping pairs) = [$hamlet| > objToHamlet (Mapping pairs) = [hamlet|
> <dl > <dl
> $forall pair <- pairs > $forall pair <- pairs
> <dt>#{fst pair} > <dt>#{fst pair}
@ -61,3 +61,6 @@
> main :: IO () > main :: IO ()
> main = warpDebug 3000 PY > main = warpDebug 3000 PY
> _ignored :: Widget
> _ignored = undefined

View File

@ -17,7 +17,7 @@
> getRoot :: Handler RepHtml > getRoot :: Handler RepHtml
> getRoot = do > getRoot = do
> sess <- getSession > sess <- getSession
> hamletToRepHtml [$hamlet| > hamletToRepHtml [hamlet|
> <form method=post > <form method=post
> <input type=text name=key > <input type=text name=key
> <input type=text name=val > <input type=text name=val
@ -32,4 +32,8 @@
> liftIO $ print (key, val) > liftIO $ print (key, val)
> redirect RedirectTemporary Root > redirect RedirectTemporary Root
> >
> main :: IO ()
> main = warpDebug 3000 Session > main = warpDebug 3000 Session
> _ignored :: Widget
> _ignored = undefined

View File

@ -2,8 +2,10 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-} {-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
import Text.Hamlet import Text.Hamlet
import qualified Data.ByteString.Lazy as L
import Data.Text (Text, cons) import Data.Text (Text, cons)
import qualified Data.Text.Lazy.IO as L
import Text.Blaze.Renderer.Text (renderHtml)
data Person = Person data Person = Person
{ name :: String { name :: String
@ -16,23 +18,23 @@ data PersonUrls = Homepage | PersonPage Text
renderUrls :: PersonUrls -> [(Text, Text)] -> Text renderUrls :: PersonUrls -> [(Text, Text)] -> Text
renderUrls Homepage _ = "/" renderUrls Homepage _ = "/"
renderUrls (PersonPage name) _ = '/' `cons` name renderUrls (PersonPage name') _ = '/' `cons` name'
footer :: Hamlet url footer :: HtmlUrl url
footer = [$hamlet|\ footer = [hamlet|
<div id="footer">Thank you, come again <div id="footer">Thank you, come again
|] |]
template :: Person -> Hamlet PersonUrls template :: Person -> HtmlUrl PersonUrls
template person = [$hamlet| template person = [hamlet|
!!! !!!
<html> <html>
<head> <head>
<title>Hamlet Demo <title>Hamlet Demo
<body> <body>
<h1>Information on #{string (name person)} <h1>Information on #{name person}
<p>#{string (name person)} is #{string (age person)} years old. <p>#{name person} is #{age person} years old.
<h2> <h2>
$if isMarried person $if isMarried person
\Married \Married
@ -40,7 +42,7 @@ template person = [$hamlet|
\Not married \Not married
<ul> <ul>
$forall child <- children person $forall child <- children person
<li>#{string child} <li>#{child}
<p> <p>
<a href="@{page person}">See the page. <a href="@{page person}">See the page.
\^{footer} \^{footer}
@ -55,7 +57,7 @@ main = do
, isMarried = True , isMarried = True
, children = ["Adam", "Ben", "Chris"] , children = ["Adam", "Ben", "Chris"]
} }
L.putStrLn $ renderHamlet renderUrls $ template person L.putStrLn $ renderHtml $ (template person) renderUrls
\end{code} \end{code}
Outputs (new lines added for readability): Outputs (new lines added for readability):

View File

@ -37,3 +37,6 @@ Just (Person {personName = "Michael", personAge = 25})
Just (Person {personName = "Michael", personAge = 26}) Just (Person {personName = "Michael", personAge = 26})
[(PersonId 1,Person {personName = "Michael", personAge = 26})] [(PersonId 1,Person {personName = "Michael", personAge = 26})]
[]</pre></code> []</pre></code>
> _ignored :: PersonId
> _ignored = undefined personName personAge

View File

@ -1,5 +1,5 @@
Name: yesod-examples Name: yesod-examples
Version: 0.8.0.3 Version: 0.9.0
Synopsis: Example programs using the Yesod Web Framework. Synopsis: Example programs using the Yesod Web Framework.
Description: These are the same examples and tutorials found on the documentation site. Description: These are the same examples and tutorials found on the documentation site.
Homepage: http://www.yesodweb.com/ Homepage: http://www.yesodweb.com/
@ -15,6 +15,8 @@ extra-source-files: static/yesod/ajax/script.js,
static/yesod/ajax/style.css, static/yesod/ajax/style.css,
static/chat.js static/chat.js
flag ghc7
Executable yesod-blog Executable yesod-blog
Main-is: src/blog.lhs Main-is: src/blog.lhs
Build-depends: base >= 4 && < 5, Build-depends: base >= 4 && < 5,
@ -23,23 +25,25 @@ Executable yesod-blog
Executable yesod-ajax Executable yesod-ajax
Main-is: src/ajax.lhs Main-is: src/ajax.lhs
Build-depends: yesod-static, Build-depends: yesod-static,
blaze-html, blaze-html >= 0.4.1.3 && < 0.5,
yesod >= 0.9 yesod >= 0.9
Executable yesod-file-echo Executable yesod-file-echo
Main-is: src/file-echo.lhs Main-is: src/file-echo.lhs
Build-depends: text, Build-depends: text >= 0.9 && < 0.12,
yesod >= 0.9 yesod >= 0.9
Executable yesod-pretty-yaml Executable yesod-pretty-yaml
Main-is: src/pretty-yaml.lhs Main-is: src/pretty-yaml.lhs
Build-depends: data-object-yaml >= 0.3.0 && < 0.4, Build-depends: data-object-yaml >= 0.3.0 && < 0.4,
data-object >= 0.3.1 && < 0.4, data-object >= 0.3.1 && < 0.4,
bytestring >= 0.9 && < 0.10, bytestring >= 0.9.1.4 && < 0.10,
yesod >= 0.9 yesod >= 0.9
Executable yesod-i18n Executable yesod-i18n
Main-is: src/i18n.lhs Main-is: src/i18n.lhs
if flag(ghc7)
cpp-options: -DGHC7
Executable yesod-session Executable yesod-session
Main-is: src/session.lhs Main-is: src/session.lhs
@ -48,21 +52,19 @@ Executable yesod-session
-- Main-is: src/widgets.lhs -- Main-is: src/widgets.lhs
-- Build-depends: yesod-form -- Build-depends: yesod-form
Executable yesod-generalized-hamlet
Main-is: src/generalized-hamlet.lhs
Executable yesod-form Executable yesod-form
Main-is: src/form.lhs Main-is: src/form.lhs
Executable yesod-persistent-synopsis Executable yesod-persistent-synopsis
Main-is: synopsis/persistent.lhs Main-is: synopsis/persistent.lhs
Build-depends: transformers >= 0.2.1 && < 0.3, Build-depends: transformers >= 0.2.2 && < 0.3,
persistent-sqlite >= 0.6, persistent-sqlite >= 0.6 && < 0.7,
persistent-template persistent-template >= 0.6 && < 0.7
extra-libraries: sqlite3
Executable yesod-hamlet-synopsis Executable yesod-hamlet-synopsis
Main-is: synopsis/hamlet.lhs Main-is: synopsis/hamlet.lhs
Build-depends: hamlet Build-depends: hamlet, yesod-core
Executable yesod-chat Executable yesod-chat
Main-is: src/chat.hs Main-is: src/chat.hs

View File

@ -35,6 +35,8 @@ module Yesod.Form.Fields
, selectField' , selectField'
, radioField' , radioField'
, Option (..) , Option (..)
, OptionList (..)
, mkOptionList
, optionsPersist , optionsPersist
, optionsPairs , optionsPairs
, optionsEnum , optionsEnum
@ -66,7 +68,6 @@ import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L import qualified Data.ByteString.Lazy as L
import Data.Text (Text, unpack, pack) import Data.Text (Text, unpack, pack)
import qualified Data.Text.Read import qualified Data.Text.Read
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Class (lift)
import Control.Applicative ((<$>)) import Control.Applicative ((<$>))
@ -76,6 +77,7 @@ import Yesod.Request (FileInfo)
import Yesod.Core (toSinglePiece, GGHandler, SinglePiece) import Yesod.Core (toSinglePiece, GGHandler, SinglePiece)
import Yesod.Persist (selectList, runDB, Filter, SelectOpt, YesodPersistBackend, Key, YesodPersist, PersistEntity, PersistBackend) import Yesod.Persist (selectList, runDB, Filter, SelectOpt, YesodPersistBackend, Key, YesodPersist, PersistEntity, PersistBackend)
import Control.Arrow ((&&&))
#if __GLASGOW_HASKELL__ >= 700 #if __GLASGOW_HASKELL__ >= 700
#define WHAMLET whamlet #define WHAMLET whamlet
@ -303,7 +305,7 @@ urlField = Field
selectField :: (Eq a, RenderMessage master FormMessage) => [(Text, a)] -> Field sub master a selectField :: (Eq a, RenderMessage master FormMessage) => [(Text, a)] -> Field sub master a
selectField = selectField' . optionsPairs selectField = selectField' . optionsPairs
selectField' :: (Eq a, RenderMessage master FormMessage) => GGHandler sub master IO [Option a] -> Field sub master a selectField' :: (Eq a, RenderMessage master FormMessage) => GGHandler sub master IO (OptionList a) -> Field sub master a
selectField' = selectFieldHelper selectField' = selectFieldHelper
(\theId name inside -> [WHAMLET|<select ##{theId} name=#{name}>^{inside}|]) -- outside (\theId name inside -> [WHAMLET|<select ##{theId} name=#{name}>^{inside}|]) -- outside
(\_theId _name isSel -> [WHAMLET|<option value=none :isSel:selected>_{MsgSelectNone}|]) -- onOpt (\_theId _name isSel -> [WHAMLET|<option value=none :isSel:selected>_{MsgSelectNone}|]) -- onOpt
@ -317,7 +319,7 @@ multiSelectField = multiSelectFieldHelper
radioField :: (Eq a, RenderMessage master FormMessage) => [(Text, a)] -> Field sub master a radioField :: (Eq a, RenderMessage master FormMessage) => [(Text, a)] -> Field sub master a
radioField = radioField' . optionsPairs radioField = radioField' . optionsPairs
radioField' :: (Eq a, RenderMessage master FormMessage) => GGHandler sub master IO [Option a] -> Field sub master a radioField' :: (Eq a, RenderMessage master FormMessage) => GGHandler sub master IO (OptionList a) -> Field sub master a
radioField' = selectFieldHelper radioField' = selectFieldHelper
(\theId _name inside -> [WHAMLET|<div ##{theId}>^{inside}|]) (\theId _name inside -> [WHAMLET|<div ##{theId}>^{inside}|])
(\theId name isSel -> [WHAMLET| (\theId name isSel -> [WHAMLET|
@ -380,27 +382,38 @@ multiSelectFieldHelper outside inside opts = Field
selectParser xs | not $ null (["", "none"] `intersect` xs) = Right Nothing selectParser xs | not $ null (["", "none"] `intersect` xs) = Right Nothing
| otherwise = (Right . Just . map snd . catMaybes . map (\y -> lookup y pairs) . nub . map fst . rights . map Data.Text.Read.decimal) xs | otherwise = (Right . Just . map snd . catMaybes . map (\y -> lookup y pairs) . nub . map fst . rights . map Data.Text.Read.decimal) xs
data OptionList a = OptionList
{ olOptions :: [Option a]
, olReadExternal :: Text -> Maybe a
}
mkOptionList :: [Option a] -> OptionList a
mkOptionList os = OptionList
{ olOptions = os
, olReadExternal = flip Map.lookup $ Map.fromList $ map (optionExternalValue &&& optionInternalValue) os
}
data Option a = Option data Option a = Option
{ optionDisplay :: Text { optionDisplay :: Text
, optionInternalValue :: a , optionInternalValue :: a
, optionExternalValue :: Text , optionExternalValue :: Text
} }
optionsPairs :: [(Text, a)] -> GGHandler sub master IO [Option a] optionsPairs :: [(Text, a)] -> GGHandler sub master IO (OptionList a)
optionsPairs = return . zipWith (\external (display, internal) -> Option optionsPairs = return . mkOptionList . zipWith (\external (display, internal) -> Option
{ optionDisplay = display { optionDisplay = display
, optionInternalValue = internal , optionInternalValue = internal
, optionExternalValue = pack $ show external , optionExternalValue = pack $ show external
}) [1 :: Int ..] }) [1 :: Int ..]
optionsEnum :: (Show a, Enum a, Bounded a) => GGHandler sub master IO [Option a] optionsEnum :: (Show a, Enum a, Bounded a) => GGHandler sub master IO (OptionList a)
optionsEnum = optionsPairs $ map (\x -> (pack $ show x, x)) [minBound..maxBound] optionsEnum = optionsPairs $ map (\x -> (pack $ show x, x)) [minBound..maxBound]
optionsPersist :: ( YesodPersist master, PersistEntity a, PersistBackend (YesodPersistBackend master) (GGHandler sub master IO) optionsPersist :: ( YesodPersist master, PersistEntity a, PersistBackend (YesodPersistBackend master) (GGHandler sub master IO)
, SinglePiece (Key (YesodPersistBackend master) a) , SinglePiece (Key (YesodPersistBackend master) a)
) )
=> [Filter a] -> [SelectOpt a] -> (a -> Text) -> GGHandler sub master IO [Option (Key (YesodPersistBackend master) a, a)] => [Filter a] -> [SelectOpt a] -> (a -> Text) -> GGHandler sub master IO (OptionList (Key (YesodPersistBackend master) a, a))
optionsPersist filts ords toDisplay = do optionsPersist filts ords toDisplay = fmap mkOptionList $ do
pairs <- runDB $ selectList filts ords pairs <- runDB $ selectList filts ords
return $ map (\(key, value) -> Option return $ map (\(key, value) -> Option
{ optionDisplay = toDisplay value { optionDisplay = toDisplay value
@ -413,13 +426,13 @@ selectFieldHelper
=> (Text -> Text -> GWidget sub master () -> GWidget sub master ()) => (Text -> Text -> GWidget sub master () -> GWidget sub master ())
-> (Text -> Text -> Bool -> GWidget sub master ()) -> (Text -> Text -> Bool -> GWidget sub master ())
-> (Text -> Text -> Text -> Bool -> Text -> GWidget sub master ()) -> (Text -> Text -> Text -> Bool -> Text -> GWidget sub master ())
-> GGHandler sub master IO [Option a] -> Field sub master a -> GGHandler sub master IO (OptionList a) -> Field sub master a
selectFieldHelper outside onOpt inside opts' = Field selectFieldHelper outside onOpt inside opts' = Field
{ fieldParse = \x -> do { fieldParse = \x -> do
opts <- opts' opts <- opts'
return $ selectParser opts x return $ selectParser opts x
, fieldView = \theId name val isReq -> do , fieldView = \theId name val isReq -> do
opts <- lift $ liftIOHandler opts' opts <- fmap olOptions $ lift $ liftIOHandler opts'
outside theId name $ do outside theId name $ do
unless isReq $ onOpt theId name $ not $ render opts val `elem` map optionExternalValue opts unless isReq $ onOpt theId name $ not $ render opts val `elem` map optionExternalValue opts
flip mapM_ opts $ \opt -> inside flip mapM_ opts $ \opt -> inside
@ -436,9 +449,9 @@ selectFieldHelper outside onOpt inside opts' = Field
selectParser opts (s:_) = case s of selectParser opts (s:_) = case s of
"" -> Right Nothing "" -> Right Nothing
"none" -> Right Nothing "none" -> Right Nothing
x -> case listToMaybe $ filter ((== x) . optionExternalValue) opts of x -> case olReadExternal opts x of
Nothing -> Left $ SomeMessage $ MsgInvalidEntry x Nothing -> Left $ SomeMessage $ MsgInvalidEntry x
Just y -> Right $ Just $ optionInternalValue y Just y -> Right $ Just y
fileAFormReq :: (RenderMessage master msg, RenderMessage master FormMessage) => FieldSettings msg -> AForm sub master FileInfo fileAFormReq :: (RenderMessage master msg, RenderMessage master FormMessage) => FieldSettings msg -> AForm sub master FileInfo
fileAFormReq fs = AForm $ \(master, langs) menvs ints -> do fileAFormReq fs = AForm $ \(master, langs) menvs ints -> do
@ -472,7 +485,6 @@ fileAFormReq fs = AForm $ \(master, langs) menvs ints -> do
fileAFormOpt :: (RenderMessage master msg, RenderMessage master FormMessage) => FieldSettings msg -> AForm sub master (Maybe FileInfo) fileAFormOpt :: (RenderMessage master msg, RenderMessage master FormMessage) => FieldSettings msg -> AForm sub master (Maybe FileInfo)
fileAFormOpt fs = AForm $ \(master, langs) menvs ints -> do fileAFormOpt fs = AForm $ \(master, langs) menvs ints -> do
liftIO $ print menvs
let (name, ints') = let (name, ints') =
case fsName fs of case fsName fs of
Just x -> (x, ints) Just x -> (x, ints)

View File

@ -4,7 +4,7 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
module Yesod.Form.Functions module Yesod.Form.Functions
( -- * Running in Form monad ( -- * Running in MForm monad
newFormIdent newFormIdent
, askParams , askParams
, askFiles , askFiles
@ -31,6 +31,7 @@ module Yesod.Form.Functions
, check , check
, checkBool , checkBool
, checkM , checkM
, customErrorMessage
) where ) where
import Yesod.Form.Types import Yesod.Form.Types
@ -61,7 +62,7 @@ import qualified Data.ByteString.Lazy as L
#endif #endif
-- | Get a unique identifier. -- | Get a unique identifier.
newFormIdent :: Form sub master Text newFormIdent :: MForm sub master Text
newFormIdent = do newFormIdent = do
i <- get i <- get
let i' = incrInts i let i' = incrInts i
@ -71,12 +72,12 @@ newFormIdent = do
incrInts (IntSingle i) = IntSingle $ i + 1 incrInts (IntSingle i) = IntSingle $ i + 1
incrInts (IntCons i is) = (i + 1) `IntCons` is incrInts (IntCons i is) = (i + 1) `IntCons` is
formToAForm :: Form sub master (FormResult a, FieldView sub master) -> AForm sub master a formToAForm :: MForm sub master (FormResult a, FieldView sub master) -> AForm sub master a
formToAForm form = AForm $ \(master, langs) env ints -> do formToAForm form = AForm $ \(master, langs) env ints -> do
((a, xml), ints', enc) <- runRWST form (env, master, langs) ints ((a, xml), ints', enc) <- runRWST form (env, master, langs) ints
return (a, (:) xml, ints', enc) return (a, (:) xml, ints', enc)
aFormToForm :: AForm sub master a -> Form sub master (FormResult a, [FieldView sub master] -> [FieldView sub master]) aFormToForm :: AForm sub master a -> MForm sub master (FormResult a, [FieldView sub master] -> [FieldView sub master])
aFormToForm (AForm aform) = do aFormToForm (AForm aform) = do
ints <- get ints <- get
(env, master, langs) <- ask (env, master, langs) <- ask
@ -85,24 +86,24 @@ aFormToForm (AForm aform) = do
tell enc tell enc
return (a, xml) return (a, xml)
askParams :: Form sub master (Maybe Env) askParams :: MForm sub master (Maybe Env)
askParams = do askParams = do
(x, _, _) <- ask (x, _, _) <- ask
return $ liftM fst x return $ liftM fst x
askFiles :: Form sub master (Maybe FileEnv) askFiles :: MForm sub master (Maybe FileEnv)
askFiles = do askFiles = do
(x, _, _) <- ask (x, _, _) <- ask
return $ liftM snd x return $ liftM snd x
mreq :: (RenderMessage master msg, RenderMessage master FormMessage) mreq :: (RenderMessage master msg, RenderMessage master FormMessage)
=> Field sub master a -> FieldSettings msg -> Maybe a => Field sub master a -> FieldSettings msg -> Maybe a
-> Form sub master (FormResult a, FieldView sub master) -> MForm sub master (FormResult a, FieldView sub master)
mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True mreq field fs mdef = mhelper field fs mdef (\m l -> FormFailure [renderMessage m l MsgValueRequired]) FormSuccess True
mopt :: RenderMessage master msg mopt :: RenderMessage master msg
=> Field sub master a -> FieldSettings msg -> Maybe (Maybe a) => Field sub master a -> FieldSettings msg -> Maybe (Maybe a)
-> Form sub master (FormResult (Maybe a), FieldView sub master) -> MForm sub master (FormResult (Maybe a), FieldView sub master)
mopt field fs mdef = mhelper field fs (join mdef) (const $ const $ FormSuccess Nothing) (FormSuccess . Just) False mopt field fs mdef = mhelper field fs (join mdef) (const $ const $ FormSuccess Nothing) (FormSuccess . Just) False
mhelper :: RenderMessage master msg mhelper :: RenderMessage master msg
@ -112,7 +113,7 @@ mhelper :: RenderMessage master msg
-> (master -> [Text] -> FormResult b) -- ^ on missing -> (master -> [Text] -> FormResult b) -- ^ on missing
-> (a -> FormResult b) -- ^ on success -> (a -> FormResult b) -- ^ on success
-> Bool -- ^ is it required? -> Bool -- ^ is it required?
-> Form sub master (FormResult b, FieldView sub master) -> MForm sub master (FormResult b, FieldView sub master)
mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do mhelper Field {..} FieldSettings {..} mdef onMissing onFound isReq = do
mp <- askParams mp <- askParams
@ -156,7 +157,7 @@ aopt :: RenderMessage master msg
-> AForm sub master (Maybe a) -> AForm sub master (Maybe a)
aopt a b = formToAForm . mopt a b aopt a b = formToAForm . mopt a b
runFormGeneric :: MonadIO m => Form sub master a -> master -> [Text] -> Maybe (Env, FileEnv) -> GGHandler sub master m (a, Enctype) runFormGeneric :: MonadIO m => MForm sub master a -> master -> [Text] -> Maybe (Env, FileEnv) -> GGHandler sub master m (a, Enctype)
runFormGeneric form master langs env = liftIOHandler $ evalRWST form (env, master, langs) (IntSingle 1) runFormGeneric form master langs env = liftIOHandler $ evalRWST form (env, master, langs) (IntSingle 1)
-- | This function is used to both initially render a form and to later extract -- | This function is used to both initially render a form and to later extract
@ -169,14 +170,14 @@ runFormGeneric form master langs env = liftIOHandler $ evalRWST form (env, maste
-- the form submit to a POST page. In such a case, both the GET and POST -- the form submit to a POST page. In such a case, both the GET and POST
-- handlers should use 'runFormPost'. -- handlers should use 'runFormPost'.
runFormPost :: RenderMessage master FormMessage runFormPost :: RenderMessage master FormMessage
=> (Html -> Form sub master (FormResult a, xml)) => (Html -> MForm sub master (FormResult a, xml))
-> GHandler sub master ((FormResult a, xml), Enctype) -> GHandler sub master ((FormResult a, xml), Enctype)
runFormPost form = do runFormPost form = do
env <- postEnv env <- postEnv
postHelper form env postHelper form env
postHelper :: RenderMessage master FormMessage postHelper :: RenderMessage master FormMessage
=> (Html -> Form sub master (FormResult a, xml)) => (Html -> MForm sub master (FormResult a, xml))
-> Maybe (Env, FileEnv) -> Maybe (Env, FileEnv)
-> GHandler sub master ((FormResult a, xml), Enctype) -> GHandler sub master ((FormResult a, xml), Enctype)
postHelper form env = do postHelper form env = do
@ -203,7 +204,7 @@ postHelper form env = do
-- general usage, you can stick with @runFormPost@. -- general usage, you can stick with @runFormPost@.
generateFormPost generateFormPost
:: RenderMessage master FormMessage :: RenderMessage master FormMessage
=> (Html -> Form sub master (FormResult a, xml)) => (Html -> MForm sub master (FormResult a, xml))
-> GHandler sub master ((FormResult a, xml), Enctype) -> GHandler sub master ((FormResult a, xml), Enctype)
generateFormPost form = postHelper form Nothing generateFormPost form = postHelper form Nothing
@ -219,14 +220,14 @@ postEnv = do
where where
notEmpty = not . L.null . fileContent notEmpty = not . L.null . fileContent
runFormPostNoNonce :: (Html -> Form sub master (FormResult a, xml)) -> GHandler sub master ((FormResult a, xml), Enctype) runFormPostNoNonce :: (Html -> MForm sub master (FormResult a, xml)) -> GHandler sub master ((FormResult a, xml), Enctype)
runFormPostNoNonce form = do runFormPostNoNonce form = do
langs <- languages langs <- languages
m <- getYesod m <- getYesod
env <- postEnv env <- postEnv
runFormGeneric (form mempty) m langs env runFormGeneric (form mempty) m langs env
runFormGet :: (Html -> Form sub master a) -> GHandler sub master (a, Enctype) runFormGet :: (Html -> MForm sub master a) -> GHandler sub master (a, Enctype)
runFormGet form = do runFormGet form = do
gets <- liftM reqGetParams getRequest gets <- liftM reqGetParams getRequest
let env = let env =
@ -235,13 +236,13 @@ runFormGet form = do
Just _ -> Just (Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) gets, Map.empty) Just _ -> Just (Map.unionsWith (++) $ map (\(x, y) -> Map.singleton x [y]) gets, Map.empty)
getHelper form env getHelper form env
generateFormGet :: (Html -> Form sub master a) -> GHandler sub master (a, Enctype) generateFormGet :: (Html -> MForm sub master a) -> GHandler sub master (a, Enctype)
generateFormGet form = getHelper form Nothing generateFormGet form = getHelper form Nothing
getKey :: Text getKey :: Text
getKey = "_hasdata" getKey = "_hasdata"
getHelper :: (Html -> Form 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 = [HTML|<input type=hidden name=#{getKey}>|] let fragment = [HTML|<input type=hidden name=#{getKey}>|]
langs <- languages langs <- languages
@ -251,7 +252,7 @@ getHelper form env = do
type FormRender sub master a = type FormRender sub master a =
AForm sub master a AForm sub master a
-> Html -> Html
-> Form sub master (FormResult a, GWidget sub master ()) -> MForm sub master (FormResult a, GWidget sub master ())
renderTable, renderDivs :: FormRender sub master a renderTable, renderDivs :: FormRender sub master a
renderTable aform fragment = do renderTable aform fragment = do
@ -309,3 +310,8 @@ checkM f field = field
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
} }
-- | Allows you to overwrite the error message on parse error.
customErrorMessage :: SomeMessage master -> Field sub master a -> Field sub master a
customErrorMessage msg field = field { fieldParse = \ts -> fmap (either
(const $ Left msg) Right) $ fieldParse field ts }

View File

@ -35,7 +35,7 @@ import Data.Maybe (listToMaybe)
#define WHAMLET $whamlet #define WHAMLET $whamlet
#endif #endif
down :: Int -> Form sub master () down :: Int -> MForm sub master ()
down 0 = return () down 0 = return ()
down i | i < 0 = error "called down with a negative number" down i | i < 0 = error "called down with a negative number"
down i = do down i = do
@ -43,7 +43,7 @@ down i = do
put $ IntCons 0 is put $ IntCons 0 is
down $ i - 1 down $ i - 1
up :: Int -> Form sub master () up :: Int -> MForm sub master ()
up 0 = return () up 0 = return ()
up i | i < 0 = error "called down with a negative number" up i | i < 0 = error "called down with a negative number"
up i = do up i = do
@ -98,7 +98,7 @@ inputList label fixXml single mdef = formToAForm $ do
withDelete :: (xml ~ GWidget sub master (), RenderMessage master FormMessage) withDelete :: (xml ~ GWidget sub master (), RenderMessage master FormMessage)
=> AForm sub master a => AForm sub master a
-> Form sub master (Either xml (FormResult a, [FieldView sub master])) -> MForm sub master (Either xml (FormResult a, [FieldView sub master]))
withDelete af = do withDelete af = do
down 1 down 1
deleteName <- newFormIdent deleteName <- newFormIdent

View File

@ -11,6 +11,7 @@ module Yesod.Form.Types
, Ints (..) , Ints (..)
-- * Form -- * Form
, Form , Form
, MForm
, AForm (..) , AForm (..)
-- * Build forms -- * Build forms
, Field (..) , Field (..)
@ -75,6 +76,8 @@ type FileEnv = Map.Map Text FileInfo
type Lang = Text type Lang = Text
type Form sub master a = RWST (Maybe (Env, FileEnv), master, [Lang]) Enctype Ints (GGHandler sub master IO) a type Form sub master a = RWST (Maybe (Env, FileEnv), master, [Lang]) Enctype Ints (GGHandler sub master IO) a
{-# DEPRECATED Form "Use MForm instead" #-}
type MForm sub master a = RWST (Maybe (Env, FileEnv), master, [Lang]) Enctype Ints (GGHandler sub master IO) a
newtype AForm sub master a = AForm newtype AForm sub master a = AForm
{ unAForm :: (master, [Text]) -> Maybe (Env, FileEnv) -> Ints -> GGHandler sub master IO (FormResult a, [FieldView sub master] -> [FieldView sub master], Ints, Enctype) { unAForm :: (master, [Text]) -> Maybe (Env, FileEnv) -> Ints -> GGHandler sub master IO (FormResult a, [FieldView sub master] -> [FieldView sub master], Ints, Enctype)

View File

@ -1,5 +1,5 @@
name: yesod-form name: yesod-form
version: 0.3.3 version: 0.3.4
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -13,27 +13,27 @@ homepage: http://www.yesodweb.com/
description: Form handling support for Yesod Web Framework description: Form handling support for Yesod Web Framework
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 0.9 && < 0.10 , yesod-core >= 0.9 && < 0.10
, yesod-persistent >= 0.2 && < 0.3 , yesod-persistent >= 0.2 && < 0.3
, time >= 1.1.4 && < 1.3 , time >= 1.1.4
, hamlet >= 0.10 && < 0.11 , hamlet >= 0.10 && < 0.11
, shakespeare-css >= 0.10 && < 0.11 , shakespeare-css >= 0.10 && < 0.11
, shakespeare-js >= 0.10 && < 0.11 , shakespeare-js >= 0.10 && < 0.11
, persistent >= 0.6 && < 0.7 , persistent >= 0.6 && < 0.7
, yesod-persistent >= 0.2 && < 0.3 , yesod-persistent >= 0.2 && < 0.3
, template-haskell , template-haskell
, transformers >= 0.2.2 && < 0.3 , transformers >= 0.2.2 && < 0.3
, data-default >= 0.3 && < 0.4 , data-default >= 0.3 && < 0.4
, xss-sanitize >= 0.3.0.1 && < 0.4 , xss-sanitize >= 0.3.0.1 && < 0.4
, blaze-builder >= 0.2.1 && < 0.4 , blaze-builder >= 0.2.1.4 && < 0.4
, network >= 2.2 && < 2.4 , network >= 2.2 && < 2.4
, email-validate >= 0.2.6 && < 0.3 , email-validate >= 0.2.6 && < 0.3
, blaze-html >= 0.4 && < 0.5 , blaze-html >= 0.4.1.3 && < 0.5
, bytestring >= 0.9 && < 0.10 , bytestring >= 0.9.1.4 && < 0.10
, text >= 0.7 && < 1.0 , text >= 0.9 && < 0.12
, wai >= 0.4 && < 0.5 , wai >= 0.4 && < 0.5
, containers >= 0.2 && < 0.5 , containers >= 0.2 && < 0.5
exposed-modules: Yesod.Form exposed-modules: Yesod.Form
Yesod.Form.Class Yesod.Form.Class
Yesod.Form.Types Yesod.Form.Types

View File

@ -1,4 +1,5 @@
{-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
module Yesod.Json module Yesod.Json
( -- * Convert from a JSON value ( -- * Convert from a JSON value
@ -23,7 +24,11 @@ import qualified Data.Aeson.Encode as JE
import Data.Aeson.Encode (fromValue) import Data.Aeson.Encode (fromValue)
import Data.Text (pack) import Data.Text (pack)
import Control.Arrow (first) import Control.Arrow (first)
#if MIN_VERSION_aeson(0, 4, 0)
import Data.HashMap.Strict (fromList)
#else
import Data.Map (fromList) import Data.Map (fromList)
#endif
import qualified Data.Vector as V import qualified Data.Vector as V
import Text.Julius (ToJavascript (..)) import Text.Julius (ToJavascript (..))
import Data.Text.Lazy.Builder (fromLazyText) import Data.Text.Lazy.Builder (fromLazyText)

View File

@ -1,5 +1,5 @@
name: yesod-json name: yesod-json
version: 0.2.1 version: 0.2.2.1
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -13,13 +13,14 @@ homepage: http://www.yesodweb.com/
description: Generate content for Yesod using the aeson package. description: Generate content for Yesod using the aeson package.
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 0.9 && < 0.10 , yesod-core >= 0.9 && < 0.10
, aeson-native >= 0.3.2.11 && < 0.4 , aeson >= 0.3
, text >= 0.8 && < 0.12 , text >= 0.8 && < 0.12
, shakespeare-js >= 0.10 && < 0.11 , shakespeare-js >= 0.10 && < 0.11
, vector , vector >= 0.9
, containers , containers >= 0.2 && < 0.5
, unordered-containers
exposed-modules: Yesod.Json exposed-modules: Yesod.Json
ghc-options: -Wall ghc-options: -Wall

234
yesod-mega.cabal Normal file
View File

@ -0,0 +1,234 @@
name: yesod-mega
version: 0.9.3
license: BSD3
license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com>
maintainer: Michael Snoyman <michael@snoyman.com>
synopsis: Creation of type-safe, RESTful web applications.
description:
Builds all yesod* repo code at once
category: Web, Yesod
stability: Stable
cabal-version: >= 1.8
build-type: Simple
homepage: http://www.yesodweb.com/
flag ghc7
flag threaded
default: True
description: Build with support for multithreaded execution
flag test
description: Build for use with running tests
default: False
library
hs-source-dirs: yesod, yesod-auth, yesod-core, yesod-default, yesod-examples, yesod-form, yesod-json, yesod-newsfeed, yesod-persistent, yesod-sitemap, yesod-static
exposed-modules:
-- yesod
Yesod
-- yesod-static
Yesod.Static
-- yesod-persistent
Yesod.Persist
-- yesod-json
Yesod.Json
-- yesod-sitemap
Yesod.Sitemap
-- yesod-core
Yesod.Content
Yesod.Core
Yesod.Dispatch
Yesod.Handler
Yesod.Logger
Yesod.Request
Yesod.Widget
Yesod.Message
Yesod.Config
Yesod.Internal.TestApi
-- yesod-form
Yesod.Form
Yesod.Form.Class
Yesod.Form.Types
Yesod.Form.Functions
Yesod.Form.Input
Yesod.Form.Fields
Yesod.Form.Jquery
Yesod.Form.Nic
Yesod.Form.MassInput
Yesod.Form.I18n.English
Yesod.Form.I18n.Swedish
-- yesod-auth
Yesod.Auth
Yesod.Auth.BrowserId
Yesod.Auth.Dummy
Yesod.Auth.Email
Yesod.Auth.Facebook
Yesod.Auth.OpenId
Yesod.Auth.OAuth
Yesod.Auth.Rpxnow
Yesod.Auth.HashDB
Yesod.Auth.Message
Yesod.Auth.Kerberos
-- yesod-default
Yesod.Default.Config
Yesod.Default.Main
Yesod.Default.Util
Yesod.Default.Handlers
-- yesod-newsfeed
Yesod.AtomFeed
Yesod.RssFeed
Yesod.Feed
other-modules:
-- yesod-newsfeed
Yesod.FeedTypes
-- yesod-core
Yesod.Internal
Yesod.Internal.Core
Yesod.Internal.Session
Yesod.Internal.Request
Yesod.Internal.Dispatch
Yesod.Internal.RouteParsing
-- yesod
Scaffolding.CodeGen
Scaffolding.Scaffolder
Devel
Build
cpp-options: -DMEGA
if flag(ghc7)
build-depends: base >= 4.3 && < 5
cpp-options: -DGHC7
else
build-depends: base >= 4 && < 4.3
include-dirs: yesod-auth/include
if !os(windows)
build-depends: unix
if flag(test)
cpp-options: -DTEST
build-depends:
-- yesod
Cabal >= 1.8 && < 1.13
, shakespeare-text >= 0.10 && < 0.11
, filepath >= 1.1 && < 1.3
, process
, attoparsec >= 0.10
-- yesod-sitemap
-- empty
-- yesod-newsfeed
, wai-extra >= 0.4.4 && < 0.5
-- yesod-default
, cmdargs >= 0.8 && < 0.9
-- yesod-auth
, authenticate >= 0.10.3 && < 0.11
, control-monad-attempt >= 0.3.0 && < 0.4
, mime-mail >= 0.3 && < 0.4
, SHA >= 1.4.1.3 && < 1.6
, http-enumerator >= 0.6 && < 0.8
, pwstore-fast >= 2.2 && < 3
, old-time >= 1.0
, base64-bytestring >= 0.1.0.1 && < 0.2
, pureMD5 >= 2.1.0.3 && < 2.2
, cereal >= 0.3 && < 0.4
, wai-app-static >= 0.3.2.1 && < 0.4
, file-embed >= 0.0.4.1 && < 0.5
, unix-compat >= 0.2 && < 0.3
, enumerator >= 0.4.14 && < 0.5
, transformers >= 0.2.2 && < 0.3
, data-default >= 0.3 && < 0.4
, xss-sanitize >= 0.3.0.1 && < 0.4
, blaze-builder >= 0.2.1 && < 0.4
, network >= 2.2 && < 2.4
, email-validate >= 0.2.6 && < 0.3
, persistent >= 0.6 && < 0.7
, persistent-template >= 0.6 && < 0.7
, failure >= 0.1 && < 0.2
, warp >= 0.4 && < 0.5
, wai >= 0.4 && < 0.5
, wai-extra >= 0.4.1 && < 0.5
, time >= 1.1.4
, bytestring >= 0.9.1.4 && < 0.12
, text >= 0.9 && < 0.12
, template-haskell
, path-pieces >= 0.0 && < 0.1
, hamlet >= 0.10 && < 0.11
, shakespeare >= 0.10 && < 0.11
, shakespeare-js >= 0.10 && < 0.11
, shakespeare-css >= 0.10 && < 0.11
, blaze-builder >= 0.2.1 && < 0.4
, clientsession >= 0.7.3.1 && < 0.8
, random >= 1.0.0.2 && < 1.1
, old-locale >= 1.0.0.2 && < 1.1
, failure >= 0.1 && < 0.2
, containers >= 0.2 && < 0.5
, monad-control >= 0.2 && < 0.3
, cookie >= 0.3 && < 0.4
, blaze-html >= 0.4.1.3 && < 0.5
, http-types >= 0.6.5 && < 0.7
, case-insensitive >= 0.2 && < 0.4
, parsec >= 2.0 && < 3.2
, directory >= 1.0 && < 1.2
, data-object >= 0.3 && < 0.4
, data-object-yaml >= 0.3 && < 0.4
, strict-concurrency >= 0.2.4 && < 0.2.5
, vector >= 0.9 && < 0.10
, aeson >= 0.3
ghc-options: -Wall
test-suite tests
type: exitcode-stdio-1.0
main-is: main.hs
hs-source-dirs:
test
yesod-core/test
yesod-static/test
if flag(ghc7)
type: exitcode-stdio-1.0
build-depends: base >= 4.3 && < 5
cpp-options: -DGHC7
main-is: test.hs
else
type: exitcode-stdio-1.0
build-depends: base >= 4 && < 4.3
main-is: test.hs
cpp-options: -DTEST
build-depends: yesod-mega
,hspec >= 0.8 && < 0.10
,wai-test >= 0.1.2 && < 0.2
,wai
,bytestring
,hamlet
,shakespeare-css
,shakespeare-js
,text
,http-types
, random
,HUnit
,QuickCheck >= 2 && < 3
, enumerator
ghc-options: -Wall
source-repository head
type: git
location: git://github.com/yesodweb/yesod.git

View File

@ -1,5 +1,5 @@
name: yesod-newsfeed name: yesod-newsfeed
version: 0.3.1 version: 0.3.2
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman, Patrick Brisbin author: Michael Snoyman, Patrick Brisbin
@ -13,12 +13,12 @@ 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.
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 0.9 && < 0.10 , yesod-core >= 0.9 && < 0.10
, time >= 1.1.4 && < 1.3 , time >= 1.1.4
, hamlet >= 0.10 && < 0.11 , hamlet >= 0.10 && < 0.11
, bytestring >= 0.9 && < 0.10 , bytestring >= 0.9.1.4 && < 0.10
, text >= 0.9 && < 1.0 , text >= 0.9 && < 0.12
exposed-modules: Yesod.AtomFeed exposed-modules: Yesod.AtomFeed
, Yesod.RssFeed , Yesod.RssFeed
, Yesod.Feed , Yesod.Feed

View File

@ -1,5 +1,5 @@
name: yesod-persistent name: yesod-persistent
version: 0.2.1 version: 0.2.2
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -18,7 +18,7 @@ library
, persistent >= 0.6 && < 0.7 , persistent >= 0.6 && < 0.7
, persistent-template >= 0.6 && < 0.7 , persistent-template >= 0.6 && < 0.7
, failure >= 0.1 && < 0.2 , failure >= 0.1 && < 0.2
, transformers >= 0.2 && < 0.3 , transformers >= 0.2.2 && < 0.3
exposed-modules: Yesod.Persist exposed-modules: Yesod.Persist
ghc-options: -Wall ghc-options: -Wall

View File

@ -1,5 +1,5 @@
name: yesod-sitemap name: yesod-sitemap
version: 0.2.1 version: 0.2.2
license: BSD3 license: BSD3
license-file: LICENSE license-file: LICENSE
author: Michael Snoyman <michael@snoyman.com> author: Michael Snoyman <michael@snoyman.com>
@ -15,7 +15,7 @@ description: Generate XML sitemaps.
library library
build-depends: base >= 4 && < 5 build-depends: base >= 4 && < 5
, yesod-core >= 0.9 && < 0.10 , yesod-core >= 0.9 && < 0.10
, time >= 1.1.4 && < 1.3 , time >= 1.1.4
, hamlet >= 0.10 && < 0.11 , hamlet >= 0.10 && < 0.11
exposed-modules: Yesod.Sitemap exposed-modules: Yesod.Sitemap
ghc-options: -Wall ghc-options: -Wall

View File

@ -29,9 +29,13 @@ module Yesod.Static
, embed , embed
-- * Template Haskell helpers -- * Template Haskell helpers
, staticFiles , staticFiles
, staticFilesList
, publicFiles , publicFiles
-- * Hashing -- * Hashing
, base64md5 , base64md5
#ifdef TEST
, getFileListPieces
#endif
) where ) where
import Prelude hiding (FilePath) import Prelude hiding (FilePath)
@ -64,6 +68,9 @@ import qualified Data.ByteString as S
import Network.HTTP.Types (status301) import Network.HTTP.Types (status301)
import System.PosixCompat.Files (getFileStatus, modificationTime) import System.PosixCompat.Files (getFileStatus, modificationTime)
import System.Posix.Types (EpochTime) import System.Posix.Types (EpochTime)
import qualified Data.Enumerator as E
import qualified Data.Enumerator.List as EL
import qualified Data.Enumerator.Binary as EB
import Network.Wai.Application.Static import Network.Wai.Application.Static
( StaticSettings (..) ( StaticSettings (..)
@ -155,6 +162,25 @@ getFileListPieces = flip go id
staticFiles :: Prelude.FilePath -> Q [Dec] staticFiles :: Prelude.FilePath -> Q [Dec]
staticFiles dir = mkStaticFiles dir staticFiles dir = mkStaticFiles dir
-- | Same as 'staticFiles', but takes an explicit list of files to create
-- identifiers for. The files are given relative to the static folder. For
-- example, to get the files \"static/js/jquery.js\" and
-- \"static/css/normalize.css\", you would use:
--
-- > staticFilesList "static" ["js/jquery.js"], ["css/normalize.css"]]
--
-- This can be useful when you have a very large number of static files, but
-- only need to refer to a few of them from Haskell.
staticFilesList :: Prelude.FilePath -> [Prelude.FilePath] -> Q [Dec]
staticFilesList dir fs =
mkStaticFilesList dir (map split fs) "StaticRoute" True
where
split :: Prelude.FilePath -> [String]
split [] = []
split x =
let (a, b) = break (== '/') x
in a : split (drop 1 b)
-- | like staticFiles, but doesn't append an etag to the query string -- | like staticFiles, but doesn't append an etag to the query string
-- This will compile faster, but doesn't achieve as great of caching. -- This will compile faster, but doesn't achieve as great of caching.
-- The browser can avoid downloading the file, but it always needs to send a request with the etag value or the last-modified value to the server to see if its copy is up to dat -- The browser can avoid downloading the file, but it always needs to send a request with the etag value or the last-modified value to the server to see if its copy is up to dat
@ -212,6 +238,15 @@ mkStaticFiles' :: Prelude.FilePath -- ^ static directory
-> Q [Dec] -> Q [Dec]
mkStaticFiles' fp routeConName makeHash = do mkStaticFiles' fp routeConName makeHash = do
fs <- qRunIO $ getFileListPieces fp fs <- qRunIO $ getFileListPieces fp
mkStaticFilesList fp fs routeConName makeHash
mkStaticFilesList
:: Prelude.FilePath -- ^ static directory
-> [[String]] -- ^ list of files to create identifiers for
-> String -- ^ route constructor "StaticRoute"
-> Bool -- ^ append checksum query parameter
-> Q [Dec]
mkStaticFilesList fp fs routeConName makeHash = do
concat `fmap` mapM mkRoute fs concat `fmap` mapM mkRoute fs
where where
replace' c replace' c
@ -233,7 +268,6 @@ mkStaticFiles' fp routeConName makeHash = do
pack' <- [|pack|] pack' <- [|pack|]
qs <- if makeHash qs <- if makeHash
then do hash <- qRunIO $ base64md5File $ pathFromRawPieces fp f then do hash <- qRunIO $ base64md5File $ pathFromRawPieces fp f
-- FIXME hash <- qRunIO . calcHash $ fp ++ '/' : intercalate "/" f
[|[(pack $(lift hash), mempty)]|] [|[(pack $(lift hash), mempty)]|]
else return $ ListE [] else return $ ListE []
return return
@ -243,22 +277,35 @@ mkStaticFiles' fp routeConName makeHash = do
] ]
] ]
-- don't use L.readFile here, since it doesn't close handles quickly enough if
-- there are lots of files in the static folder, it will cause exhausted file
-- descriptors
base64md5File :: Prelude.FilePath -> IO String base64md5File :: Prelude.FilePath -> IO String
base64md5File file = do base64md5File file = do
contents <- L.readFile file bss <- E.run_ $ EB.enumFile file E.$$ EL.consume
return $ base64md5 contents return $ base64md5 $ L.fromChunks bss
-- FIXME I'd like something streaming instead
{-
fmap (base64 . finalize) $ E.run_ $
EB.enumFile file E.$$ EL.fold go (md5InitialContext, "")
where
go (context, prev) next = (md5Update context prev, next)
finalize (context, end) = md5Finalize context end
-}
-- | md5-hashes the given lazy bytestring and returns the hash as -- | md5-hashes the given lazy bytestring and returns the hash as
-- base64url-encoded string. -- base64url-encoded string.
-- --
-- This function returns the first 8 characters of the hash. -- This function returns the first 8 characters of the hash.
base64md5 :: L.ByteString -> String base64md5 :: L.ByteString -> String
base64md5 = map tr base64md5 = base64 . md5
. take 8
. S8.unpack base64 :: MD5Digest -> String
. Data.ByteString.Base64.encode base64 = map tr
. Data.Serialize.encode . take 8
. md5 . S8.unpack
. Data.ByteString.Base64.encode
. Data.Serialize.encode
where where
tr '+' = '-' tr '+' = '-'
tr '/' = '_' tr '/' = '_'

View File

@ -0,0 +1,16 @@
module YesodStaticTest (specs) where
import Test.Hspec
import Test.HUnit ( (@?=) )
import Test.Hspec.HUnit ( )
import Yesod.Static (getFileListPieces)
specs :: [Specs]
specs = [
describe "get file list" [
it "pieces" $ do
x <- getFileListPieces "test/fs"
x @?= [["foo"], ["bar", "baz"]]
]
]

View File

@ -0,0 +1,25 @@
The following license covers this documentation, and the source code, except
where otherwise indicated.
Copyright 2010, Michael Snoyman. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,7 @@
#!/usr/bin/env runhaskell
> module Main where
> import Distribution.Simple
> main :: IO ()
> main = defaultMain

View File

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 891 B

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

Some files were not shown because too many files have changed in this diff Show More