Add 'yesod-core/' from commit '982d6185bee75b078bee92bd8a2e8743707f1922'
git-subtree-dir: yesod-core git-subtree-mainline:cd5ee0fb12git-subtree-split:982d6185be
This commit is contained in:
commit
2dc10de435
7
yesod-core/.gitignore
vendored
Normal file
7
yesod-core/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
/dist/
|
||||
*.swp
|
||||
client_session_key.aes
|
||||
*.hi
|
||||
*.o
|
||||
blog.db3
|
||||
static/tmp/
|
||||
25
yesod-core/LICENSE
Normal file
25
yesod-core/LICENSE
Normal 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.
|
||||
1
yesod-core/README
Normal file
1
yesod-core/README
Normal file
@ -0,0 +1 @@
|
||||
Learn more at http://docs.yesodweb.com/
|
||||
7
yesod-core/Setup.lhs
Executable file
7
yesod-core/Setup.lhs
Executable file
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env runhaskell
|
||||
|
||||
> module Main where
|
||||
> import Distribution.Simple
|
||||
|
||||
> main :: IO ()
|
||||
> main = defaultMain
|
||||
132
yesod-core/Test/CleanPath.hs
Normal file
132
yesod-core/Test/CleanPath.hs
Normal file
@ -0,0 +1,132 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
module Test.CleanPath (cleanPathTest) where
|
||||
|
||||
import Yesod.Core hiding (Request)
|
||||
import Yesod.Content
|
||||
import Yesod.Dispatch
|
||||
import Yesod.Handler (Route)
|
||||
|
||||
import Test.Framework (defaultMain, testGroup, Test)
|
||||
import Test.Framework.Providers.HUnit
|
||||
import Test.HUnit hiding (Test)
|
||||
import Network.Wai
|
||||
import Network.Wai.Test
|
||||
import Network.HTTP.Types (status200, decodePathSegments)
|
||||
|
||||
import qualified Data.ByteString.Lazy.Char8 as L8
|
||||
import qualified Data.Text as TS
|
||||
|
||||
data Subsite = Subsite
|
||||
getSubsite = const Subsite
|
||||
data SubsiteRoute = SubsiteRoute [TS.Text]
|
||||
deriving (Eq, Show, Read)
|
||||
type instance Route Subsite = SubsiteRoute
|
||||
instance RenderRoute SubsiteRoute where
|
||||
renderRoute (SubsiteRoute x) = (x, [])
|
||||
|
||||
instance YesodDispatch Subsite master where
|
||||
yesodDispatch _ _ pieces _ _ = Just $ const $ return $ responseLBS
|
||||
status200
|
||||
[ ("Content-Type", "SUBSITE")
|
||||
] $ L8.pack $ show pieces
|
||||
|
||||
data Y = Y
|
||||
mkYesod "Y" [$parseRoutes|
|
||||
/foo FooR GET
|
||||
/foo/#String FooStringR GET
|
||||
/bar BarR GET
|
||||
/subsite SubsiteR Subsite getSubsite
|
||||
/plain PlainR GET
|
||||
|]
|
||||
|
||||
instance Yesod Y where
|
||||
approot _ = "http://test"
|
||||
cleanPath _ ["bar", ""] = Right ["bar"]
|
||||
cleanPath _ ["bar"] = Left ["bar", ""]
|
||||
cleanPath _ s =
|
||||
if corrected == s
|
||||
then Right s
|
||||
else Left corrected
|
||||
where
|
||||
corrected = filter (not . TS.null) s
|
||||
|
||||
getFooR = return $ RepPlain "foo"
|
||||
getFooStringR = return . RepPlain . toContent
|
||||
getBarR = return $ RepPlain "bar"
|
||||
getPlainR = return $ RepPlain "plain"
|
||||
|
||||
cleanPathTest :: Test
|
||||
cleanPathTest = testGroup "Test.CleanPath"
|
||||
[ testCase "remove trailing slash" removeTrailingSlash
|
||||
, testCase "noTrailingSlash" noTrailingSlash
|
||||
, testCase "add trailing slash" addTrailingSlash
|
||||
, testCase "has trailing slash" hasTrailingSlash
|
||||
, testCase "/foo/something" fooSomething
|
||||
, testCase "subsite dispatch" subsiteDispatch
|
||||
, testCase "redirect with query string" redQueryString
|
||||
]
|
||||
|
||||
runner f = toWaiApp Y >>= runSession f
|
||||
defaultRequest = Request
|
||||
{ pathInfo = []
|
||||
, requestHeaders = []
|
||||
, queryString = []
|
||||
, rawQueryString = ""
|
||||
, requestMethod = "GET"
|
||||
}
|
||||
|
||||
removeTrailingSlash = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = decodePathSegments "/foo/"
|
||||
}
|
||||
assertStatus 301 res
|
||||
assertHeader "Location" "http://test/foo" res
|
||||
|
||||
noTrailingSlash = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = decodePathSegments "/foo"
|
||||
}
|
||||
assertStatus 200 res
|
||||
assertContentType "text/plain; charset=utf-8" res
|
||||
assertBody "foo" res
|
||||
|
||||
addTrailingSlash = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = decodePathSegments "/bar"
|
||||
}
|
||||
assertStatus 301 res
|
||||
assertHeader "Location" "http://test/bar/" res
|
||||
|
||||
hasTrailingSlash = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = decodePathSegments "/bar/"
|
||||
}
|
||||
assertStatus 200 res
|
||||
assertContentType "text/plain; charset=utf-8" res
|
||||
assertBody "bar" res
|
||||
|
||||
fooSomething = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = decodePathSegments "/foo/something"
|
||||
}
|
||||
assertStatus 200 res
|
||||
assertContentType "text/plain; charset=utf-8" res
|
||||
assertBody "something" res
|
||||
|
||||
subsiteDispatch = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = decodePathSegments "/subsite/1/2/3/"
|
||||
}
|
||||
assertStatus 200 res
|
||||
assertContentType "SUBSITE" res
|
||||
assertBody "[\"1\",\"2\",\"3\",\"\"]" res
|
||||
|
||||
redQueryString = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = decodePathSegments "/plain/"
|
||||
, rawQueryString = "?foo=bar"
|
||||
}
|
||||
assertStatus 301 res
|
||||
assertHeader "Location" "http://test/plain?foo=bar" res
|
||||
47
yesod-core/Test/Exceptions.hs
Normal file
47
yesod-core/Test/Exceptions.hs
Normal file
@ -0,0 +1,47 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
module Test.Exceptions (exceptionsTest) where
|
||||
|
||||
import Yesod.Core hiding (Request)
|
||||
import Yesod.Content
|
||||
import Yesod.Dispatch
|
||||
import Yesod.Handler (Route, ErrorResponse (InternalError))
|
||||
|
||||
import Test.Framework (defaultMain, testGroup, Test)
|
||||
import Test.Framework.Providers.HUnit
|
||||
import Test.HUnit hiding (Test)
|
||||
import Network.Wai
|
||||
import Network.Wai.Test
|
||||
|
||||
import qualified Data.ByteString.Lazy.Char8 as L8
|
||||
|
||||
data Y = Y
|
||||
mkYesod "Y" [$parseRoutes|
|
||||
/ RootR GET
|
||||
|]
|
||||
|
||||
instance Yesod Y where
|
||||
approot _ = "http://test"
|
||||
errorHandler (InternalError e) = return $ chooseRep $ RepPlain $ toContent e
|
||||
errorHandler x = defaultErrorHandler x
|
||||
|
||||
getRootR = error "FOOBAR" >> return ()
|
||||
|
||||
exceptionsTest :: Test
|
||||
exceptionsTest = testGroup "Test.Exceptions"
|
||||
[ testCase "500" case500
|
||||
]
|
||||
|
||||
runner f = toWaiApp Y >>= runSession f
|
||||
defaultRequest = Request
|
||||
{ pathInfo = []
|
||||
, requestHeaders = []
|
||||
, queryString = []
|
||||
, requestMethod = "GET"
|
||||
}
|
||||
|
||||
case500 = runner $ do
|
||||
res <- request defaultRequest
|
||||
assertStatus 500 res
|
||||
assertBody "FOOBAR" res
|
||||
42
yesod-core/Test/Links.hs
Normal file
42
yesod-core/Test/Links.hs
Normal file
@ -0,0 +1,42 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
module Test.Links (linksTest) where
|
||||
|
||||
import Yesod.Core hiding (Request)
|
||||
import Text.Hamlet
|
||||
|
||||
import Test.Framework (defaultMain, testGroup, Test)
|
||||
import Test.Framework.Providers.HUnit
|
||||
import Test.HUnit hiding (Test)
|
||||
import Network.Wai
|
||||
import Network.Wai.Test
|
||||
|
||||
import qualified Data.ByteString.Lazy.Char8 as L8
|
||||
|
||||
data Y = Y
|
||||
mkYesod "Y" [$parseRoutes|
|
||||
/ RootR GET
|
||||
|]
|
||||
|
||||
instance Yesod Y where
|
||||
approot _ = ""
|
||||
|
||||
getRootR = defaultLayout $ addHamlet [$hamlet|<a href=@{RootR}>|]
|
||||
|
||||
linksTest :: Test
|
||||
linksTest = testGroup "Test.Links"
|
||||
[ testCase "linkToHome" case_linkToHome
|
||||
]
|
||||
|
||||
runner f = toWaiApp Y >>= runSession f
|
||||
defaultRequest = Request
|
||||
{ pathInfo = []
|
||||
, requestHeaders = []
|
||||
, queryString = []
|
||||
, requestMethod = "GET"
|
||||
}
|
||||
|
||||
case_linkToHome = runner $ do
|
||||
res <- request defaultRequest
|
||||
assertBody "<!DOCTYPE html>\n<html><head><title></title></head><body><a href=\"/\"></a></body></html>" res
|
||||
64
yesod-core/Test/Media.hs
Normal file
64
yesod-core/Test/Media.hs
Normal file
@ -0,0 +1,64 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
module Test.Media (mediaTest) where
|
||||
|
||||
import Yesod.Core hiding (Request)
|
||||
|
||||
import Test.Framework (defaultMain, testGroup, Test)
|
||||
import Test.Framework.Providers.HUnit
|
||||
import Test.HUnit hiding (Test)
|
||||
import Network.Wai
|
||||
import Network.Wai.Test
|
||||
import Network.HTTP.Types (status200, decodePathSegments)
|
||||
|
||||
import qualified Data.ByteString.Lazy.Char8 as L8
|
||||
import qualified Data.Text as TS
|
||||
import Text.Lucius
|
||||
|
||||
data Y = Y
|
||||
mkYesod "Y" [$parseRoutes|
|
||||
/ RootR GET
|
||||
/static StaticR GET
|
||||
|]
|
||||
|
||||
instance Yesod Y where
|
||||
approot _ = ""
|
||||
addStaticContent _ _ content = do
|
||||
tm <- getRouteToMaster
|
||||
route <- getCurrentRoute
|
||||
case fmap tm route of
|
||||
Just StaticR -> return $ Just $ Left $
|
||||
if content == "foo2{bar:baz}"
|
||||
then "screen.css"
|
||||
else "all.css"
|
||||
_ -> return Nothing
|
||||
|
||||
getRootR = defaultLayout $ do
|
||||
addCassius [$lucius|foo1{bar:baz}|]
|
||||
addCassiusMedia "screen" [$lucius|foo2{bar:baz}|]
|
||||
addCassius [$lucius|foo3{bar:baz}|]
|
||||
getStaticR = getRootR
|
||||
|
||||
runner f = toWaiApp Y >>= runSession f
|
||||
defaultRequest = Request
|
||||
{ pathInfo = []
|
||||
, requestHeaders = []
|
||||
, queryString = []
|
||||
, requestMethod = "GET"
|
||||
}
|
||||
|
||||
caseMedia = runner $ do
|
||||
res <- request defaultRequest
|
||||
assertStatus 200 res
|
||||
flip assertBody res "<!DOCTYPE html>\n<html><head><title></title><style>foo1{bar:baz}foo3{bar:baz}</style><style media=\"screen\">foo2{bar:baz}</style></head><body></body></html>"
|
||||
|
||||
caseMediaLink = runner $ do
|
||||
res <- request defaultRequest { pathInfo = ["static"] }
|
||||
assertStatus 200 res
|
||||
flip assertBody res "<!DOCTYPE html>\n<html><head><title></title><link rel=\"stylesheet\" href=\"all.css\"><link rel=\"stylesheet\" media=\"screen\" href=\"screen.css\"></head><body></body></html>"
|
||||
|
||||
mediaTest = testGroup "Test.Media"
|
||||
[ testCase "media" caseMedia
|
||||
, testCase "media link" caseMediaLink
|
||||
]
|
||||
50
yesod-core/Test/NoOverloadedStrings.hs
Normal file
50
yesod-core/Test/NoOverloadedStrings.hs
Normal file
@ -0,0 +1,50 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
module Test.NoOverloadedStrings (noOverloadedTest) where
|
||||
|
||||
import Yesod.Core hiding (Request)
|
||||
import Test.Framework (testGroup, Test)
|
||||
import Test.Framework.Providers.HUnit
|
||||
import Network.Wai.Test
|
||||
import Network.Wai
|
||||
import Data.Monoid (mempty)
|
||||
import Data.String (fromString)
|
||||
|
||||
data Subsite = Subsite
|
||||
getSubsite = const Subsite
|
||||
mkYesodSub "Subsite" [] [parseRoutes|
|
||||
/bar BarR GET
|
||||
|]
|
||||
|
||||
getBarR :: GHandler Subsite m ()
|
||||
getBarR = return ()
|
||||
|
||||
data Y = Y
|
||||
mkYesod "Y" [parseRoutes|
|
||||
/ RootR GET
|
||||
/foo FooR GET
|
||||
/subsite SubsiteR Subsite getSubsite
|
||||
|]
|
||||
|
||||
instance Yesod Y where
|
||||
approot _ = fromString ""
|
||||
|
||||
getRootR = return ()
|
||||
getFooR = return ()
|
||||
|
||||
runner f = toWaiApp Y >>= runSession f
|
||||
defaultRequest = Request
|
||||
{ pathInfo = []
|
||||
, requestHeaders = []
|
||||
, queryString = []
|
||||
, requestMethod = fromString "GET"
|
||||
}
|
||||
|
||||
case_sanity = runner $ do
|
||||
res <- request defaultRequest
|
||||
assertBody mempty res
|
||||
|
||||
noOverloadedTest :: Test
|
||||
noOverloadedTest = testGroup "Test.NoOverloadedStrings"
|
||||
[ testCase "sanity" case_sanity
|
||||
]
|
||||
78
yesod-core/Test/Widget.hs
Normal file
78
yesod-core/Test/Widget.hs
Normal file
@ -0,0 +1,78 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
module Test.Widget (widgetTest) where
|
||||
|
||||
import Yesod.Core hiding (Request)
|
||||
import Yesod.Content
|
||||
import Yesod.Dispatch
|
||||
import Yesod.Widget
|
||||
import Text.Julius
|
||||
|
||||
import Test.Framework (defaultMain, testGroup, Test)
|
||||
import Test.Framework.Providers.HUnit
|
||||
import Test.HUnit hiding (Test)
|
||||
import Network.Wai
|
||||
import Network.Wai.Test
|
||||
|
||||
import qualified Data.ByteString.Lazy.Char8 as L8
|
||||
|
||||
data Y = Y
|
||||
|
||||
mkMessage "Y" "test" "en"
|
||||
|
||||
mkYesod "Y" [$parseRoutes|
|
||||
/ RootR GET
|
||||
/foo/*Strings MultiR GET
|
||||
/whamlet WhamletR GET
|
||||
|]
|
||||
|
||||
instance Yesod Y where
|
||||
approot _ = "http://test"
|
||||
|
||||
getRootR = defaultLayout $ addJuliusBody [$julius|<not escaped>|]
|
||||
getMultiR _ = return ()
|
||||
|
||||
data Msg = Hello | Goodbye
|
||||
instance RenderMessage Y Msg where
|
||||
renderMessage _ ("en":_) Hello = "Hello"
|
||||
renderMessage _ ("es":_) Hello = "Hola"
|
||||
renderMessage _ ("en":_) Goodbye = "Goodbye"
|
||||
renderMessage _ ("es":_) Goodbye = "Adios"
|
||||
renderMessage a (_:xs) y = renderMessage a xs y
|
||||
renderMessage a [] y = renderMessage a ["en"] y
|
||||
|
||||
getWhamletR = defaultLayout [$whamlet|
|
||||
<h1>Test
|
||||
<h2>@{WhamletR}
|
||||
<h3>_{Goodbye}
|
||||
<h3>_{MsgAnother}
|
||||
^{embed}
|
||||
|]
|
||||
where
|
||||
embed = [$whamlet|<h4>Embed|]
|
||||
|
||||
widgetTest :: Test
|
||||
widgetTest = testGroup "Test.Widget"
|
||||
[ testCase "addJuliusBody" case_addJuliusBody
|
||||
, testCase "whamlet" case_whamlet
|
||||
]
|
||||
|
||||
runner f = toWaiApp Y >>= runSession f
|
||||
defaultRequest = Request
|
||||
{ pathInfo = []
|
||||
, requestHeaders = []
|
||||
, queryString = []
|
||||
, requestMethod = "GET"
|
||||
}
|
||||
|
||||
case_addJuliusBody = runner $ do
|
||||
res <- request defaultRequest
|
||||
assertBody "<!DOCTYPE html>\n<html><head><title></title></head><body><script><not escaped></script></body></html>" res
|
||||
|
||||
case_whamlet = runner $ do
|
||||
res <- request defaultRequest
|
||||
{ pathInfo = ["whamlet"]
|
||||
, requestHeaders = [("Accept-Language", "es")]
|
||||
}
|
||||
assertBody "<!DOCTYPE html>\n<html><head><title></title></head><body><h1>Test</h1><h2>http://test/whamlet</h2><h3>Adios</h3><h3>String</h3><h4>Embed</h4></body></html>" res
|
||||
232
yesod-core/Yesod/Content.hs
Normal file
232
yesod-core/Yesod/Content.hs
Normal file
@ -0,0 +1,232 @@
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE TypeSynonymInstances #-}
|
||||
{-# LANGUAGE Rank2Types #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Content
|
||||
( -- * Content
|
||||
Content (..)
|
||||
, emptyContent
|
||||
, ToContent (..)
|
||||
-- * Mime types
|
||||
-- ** Data type
|
||||
, ContentType
|
||||
, typeHtml
|
||||
, typePlain
|
||||
, typeJson
|
||||
, typeXml
|
||||
, typeAtom
|
||||
, typeRss
|
||||
, typeJpeg
|
||||
, typePng
|
||||
, typeGif
|
||||
, typeJavascript
|
||||
, typeCss
|
||||
, typeFlv
|
||||
, typeOgv
|
||||
, typeOctet
|
||||
-- * Utilities
|
||||
, simpleContentType
|
||||
-- * Representations
|
||||
, ChooseRep
|
||||
, HasReps (..)
|
||||
, defChooseRep
|
||||
-- ** Specific content types
|
||||
, RepHtml (..)
|
||||
, RepJson (..)
|
||||
, RepHtmlJson (..)
|
||||
, RepPlain (..)
|
||||
, RepXml (..)
|
||||
-- * Utilities
|
||||
, formatW3
|
||||
, formatRFC1123
|
||||
, formatRFC822
|
||||
) where
|
||||
|
||||
import Data.Maybe (mapMaybe)
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Lazy as L
|
||||
import Data.Text.Lazy (Text, pack)
|
||||
import qualified Data.Text as T
|
||||
|
||||
import Data.Time
|
||||
import System.Locale
|
||||
|
||||
import qualified Data.Text.Encoding
|
||||
import qualified Data.Text.Lazy.Encoding
|
||||
|
||||
import Data.Enumerator (Enumerator)
|
||||
import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString)
|
||||
import Data.Monoid (mempty)
|
||||
|
||||
import Text.Hamlet (Html)
|
||||
import Text.Blaze.Renderer.Utf8 (renderHtmlBuilder)
|
||||
import Data.String (IsString (fromString))
|
||||
import Network.Wai (FilePart)
|
||||
|
||||
data Content = ContentBuilder Builder (Maybe Int) -- ^ The content and optional content length.
|
||||
| ContentEnum (forall a. Enumerator Builder IO a)
|
||||
| ContentFile FilePath (Maybe FilePart)
|
||||
|
||||
-- | Zero-length enumerator.
|
||||
emptyContent :: Content
|
||||
emptyContent = ContentBuilder mempty $ Just 0
|
||||
|
||||
instance IsString Content where
|
||||
fromString = toContent
|
||||
|
||||
-- | Anything which can be converted into 'Content'. Most of the time, you will
|
||||
-- want to use the 'ContentBuilder' constructor. An easier approach will be to use
|
||||
-- a pre-defined 'toContent' function, such as converting your data into a lazy
|
||||
-- bytestring and then calling 'toContent' on that.
|
||||
--
|
||||
-- Please note that the built-in instances for lazy data structures ('String',
|
||||
-- lazy 'L.ByteString', lazy 'Text' and 'Html') will not automatically include
|
||||
-- the content length for the 'ContentBuilder' constructor.
|
||||
class ToContent a where
|
||||
toContent :: a -> Content
|
||||
|
||||
instance ToContent Builder where
|
||||
toContent = flip ContentBuilder Nothing
|
||||
instance ToContent B.ByteString where
|
||||
toContent bs = ContentBuilder (fromByteString bs) $ Just $ B.length bs
|
||||
instance ToContent L.ByteString where
|
||||
toContent = flip ContentBuilder Nothing . fromLazyByteString
|
||||
instance ToContent T.Text where
|
||||
toContent = toContent . Data.Text.Encoding.encodeUtf8
|
||||
instance ToContent Text where
|
||||
toContent = toContent . Data.Text.Lazy.Encoding.encodeUtf8
|
||||
instance ToContent String where
|
||||
toContent = toContent . pack
|
||||
instance ToContent Html where
|
||||
toContent bs = ContentBuilder (renderHtmlBuilder bs) Nothing
|
||||
|
||||
-- | A function which gives targetted representations of content based on the
|
||||
-- content-types the user accepts.
|
||||
type ChooseRep =
|
||||
[ContentType] -- ^ list of content-types user accepts, ordered by preference
|
||||
-> IO (ContentType, Content)
|
||||
|
||||
-- | Any type which can be converted to representations.
|
||||
class HasReps a where
|
||||
chooseRep :: a -> ChooseRep
|
||||
|
||||
-- | A helper method for generating 'HasReps' instances.
|
||||
--
|
||||
-- This function should be given a list of pairs of content type and conversion
|
||||
-- functions. If none of the content types match, the first pair is used.
|
||||
defChooseRep :: [(ContentType, a -> IO Content)] -> a -> ChooseRep
|
||||
defChooseRep reps a ts = do
|
||||
let (ct, c) =
|
||||
case mapMaybe helper ts of
|
||||
(x:_) -> x
|
||||
[] -> case reps of
|
||||
[] -> error "Empty reps to defChooseRep"
|
||||
(x:_) -> x
|
||||
c' <- c a
|
||||
return (ct, c')
|
||||
where
|
||||
helper ct = do
|
||||
c <- lookup ct reps
|
||||
return (ct, c)
|
||||
|
||||
instance HasReps ChooseRep where
|
||||
chooseRep = id
|
||||
|
||||
instance HasReps () where
|
||||
chooseRep = defChooseRep [(typePlain, const $ return $ toContent B.empty)]
|
||||
|
||||
instance HasReps (ContentType, Content) where
|
||||
chooseRep = const . return
|
||||
|
||||
instance HasReps [(ContentType, Content)] where
|
||||
chooseRep a cts = return $
|
||||
case filter (\(ct, _) -> go ct `elem` map go cts) a of
|
||||
((ct, c):_) -> (ct, c)
|
||||
_ -> case a of
|
||||
(x:_) -> x
|
||||
_ -> error "chooseRep [(ContentType, Content)] of empty"
|
||||
where
|
||||
go = simpleContentType
|
||||
|
||||
newtype RepHtml = RepHtml Content
|
||||
instance HasReps RepHtml where
|
||||
chooseRep (RepHtml c) _ = return (typeHtml, c)
|
||||
newtype RepJson = RepJson Content
|
||||
instance HasReps RepJson where
|
||||
chooseRep (RepJson c) _ = return (typeJson, c)
|
||||
data RepHtmlJson = RepHtmlJson Content Content
|
||||
instance HasReps RepHtmlJson where
|
||||
chooseRep (RepHtmlJson html json) = chooseRep
|
||||
[ (typeHtml, html)
|
||||
, (typeJson, json)
|
||||
]
|
||||
newtype RepPlain = RepPlain Content
|
||||
instance HasReps RepPlain where
|
||||
chooseRep (RepPlain c) _ = return (typePlain, c)
|
||||
newtype RepXml = RepXml Content
|
||||
instance HasReps RepXml where
|
||||
chooseRep (RepXml c) _ = return (typeXml, c)
|
||||
|
||||
type ContentType = B.ByteString -- FIXME Text?
|
||||
|
||||
typeHtml :: ContentType
|
||||
typeHtml = "text/html; charset=utf-8"
|
||||
|
||||
typePlain :: ContentType
|
||||
typePlain = "text/plain; charset=utf-8"
|
||||
|
||||
typeJson :: ContentType
|
||||
typeJson = "application/json; charset=utf-8"
|
||||
|
||||
typeXml :: ContentType
|
||||
typeXml = "text/xml"
|
||||
|
||||
typeAtom :: ContentType
|
||||
typeAtom = "application/atom+xml"
|
||||
|
||||
typeRss :: ContentType
|
||||
typeRss = "application/rss+xml"
|
||||
|
||||
typeJpeg :: ContentType
|
||||
typeJpeg = "image/jpeg"
|
||||
|
||||
typePng :: ContentType
|
||||
typePng = "image/png"
|
||||
|
||||
typeGif :: ContentType
|
||||
typeGif = "image/gif"
|
||||
|
||||
typeJavascript :: ContentType
|
||||
typeJavascript = "text/javascript; charset=utf-8"
|
||||
|
||||
typeCss :: ContentType
|
||||
typeCss = "text/css; charset=utf-8"
|
||||
|
||||
typeFlv :: ContentType
|
||||
typeFlv = "video/x-flv"
|
||||
|
||||
typeOgv :: ContentType
|
||||
typeOgv = "video/ogg"
|
||||
|
||||
typeOctet :: ContentType
|
||||
typeOctet = "application/octet-stream"
|
||||
|
||||
-- | Removes \"extra\" information at the end of a content type string. In
|
||||
-- particular, removes everything after the semicolon, if present.
|
||||
--
|
||||
-- For example, \"text/html; charset=utf-8\" is commonly used to specify the
|
||||
-- character encoding for HTML data. This function would return \"text/html\".
|
||||
simpleContentType :: ContentType -> ContentType
|
||||
simpleContentType = fst . B.breakByte 59 -- 59 == ;
|
||||
|
||||
-- | Format a 'UTCTime' in W3 format.
|
||||
formatW3 :: UTCTime -> T.Text
|
||||
formatW3 = T.pack . formatTime defaultTimeLocale "%FT%X-00:00"
|
||||
|
||||
-- | Format as per RFC 1123.
|
||||
formatRFC1123 :: UTCTime -> T.Text
|
||||
formatRFC1123 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %X %Z"
|
||||
|
||||
-- | Format as per RFC 822.
|
||||
formatRFC822 :: UTCTime -> T.Text
|
||||
formatRFC822 = T.pack . formatTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S %z"
|
||||
75
yesod-core/Yesod/Core.hs
Normal file
75
yesod-core/Yesod/Core.hs
Normal file
@ -0,0 +1,75 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module Yesod.Core
|
||||
( -- * Type classes
|
||||
Yesod (..)
|
||||
, YesodDispatch (..)
|
||||
, RenderRoute (..)
|
||||
-- ** Breadcrumbs
|
||||
, YesodBreadcrumbs (..)
|
||||
, breadcrumbs
|
||||
-- * Utitlities
|
||||
, maybeAuthorized
|
||||
, widgetToPageContent
|
||||
-- * Defaults
|
||||
, defaultErrorHandler
|
||||
-- * Data types
|
||||
, AuthResult (..)
|
||||
-- * Logging
|
||||
, LogLevel (..)
|
||||
, formatLogMessage
|
||||
, logDebug
|
||||
, logInfo
|
||||
, logWarn
|
||||
, logError
|
||||
, logOther
|
||||
-- * Misc
|
||||
, yesodVersion
|
||||
, yesodRender
|
||||
-- * Re-exports
|
||||
, module Yesod.Content
|
||||
, module Yesod.Dispatch
|
||||
, module Yesod.Handler
|
||||
, module Yesod.Request
|
||||
, module Yesod.Widget
|
||||
, module Yesod.Message
|
||||
) where
|
||||
|
||||
import Yesod.Internal.Core
|
||||
import Yesod.Content
|
||||
import Yesod.Dispatch
|
||||
import Yesod.Handler
|
||||
import Yesod.Request
|
||||
import Yesod.Widget
|
||||
import Yesod.Message
|
||||
|
||||
import Language.Haskell.TH.Syntax
|
||||
import Data.Text (Text)
|
||||
|
||||
logTH :: LogLevel -> Q Exp
|
||||
logTH level =
|
||||
[|messageLoggerHandler $(qLocation >>= liftLoc) $(lift level)|]
|
||||
where
|
||||
liftLoc :: Loc -> Q Exp
|
||||
liftLoc (Loc a b c d e) = [|Loc $(lift a) $(lift b) $(lift c) $(lift d) $(lift e)|]
|
||||
|
||||
-- | Generates a function that takes a 'Text' and logs a 'LevelDebug' message. Usage:
|
||||
--
|
||||
-- > $(logDebug) "This is a debug log message"
|
||||
logDebug :: Q Exp
|
||||
logDebug = logTH LevelDebug
|
||||
|
||||
-- | See 'logDebug'
|
||||
logInfo :: Q Exp
|
||||
logInfo = logTH LevelInfo
|
||||
-- | See 'logDebug'
|
||||
logWarn :: Q Exp
|
||||
logWarn = logTH LevelWarn
|
||||
-- | See 'logDebug'
|
||||
logError :: Q Exp
|
||||
logError = logTH LevelError
|
||||
|
||||
-- | Generates a function that takes a 'Text' and logs a 'LevelOther' message. Usage:
|
||||
--
|
||||
-- > $(logOther "My new level") "This is a log message"
|
||||
logOther :: Text -> Q Exp
|
||||
logOther = logTH . LevelOther
|
||||
180
yesod-core/Yesod/Dispatch.hs
Normal file
180
yesod-core/Yesod/Dispatch.hs
Normal file
@ -0,0 +1,180 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Dispatch
|
||||
( -- * Quasi-quoted routing
|
||||
parseRoutes
|
||||
, parseRoutesFile
|
||||
, mkYesod
|
||||
, mkYesodSub
|
||||
-- ** More fine-grained
|
||||
, mkYesodData
|
||||
, mkYesodSubData
|
||||
, mkYesodDispatch
|
||||
, mkYesodSubDispatch
|
||||
-- ** Path pieces
|
||||
, SinglePiece (..)
|
||||
, MultiPiece (..)
|
||||
, Texts
|
||||
-- * Convert to WAI
|
||||
, toWaiApp
|
||||
, toWaiAppPlain
|
||||
) where
|
||||
|
||||
import Data.Functor ((<$>))
|
||||
import Data.Either (partitionEithers)
|
||||
import Prelude hiding (exp)
|
||||
import Yesod.Internal.Core
|
||||
import Yesod.Handler
|
||||
import Yesod.Internal.Dispatch
|
||||
|
||||
import Web.PathPieces (SinglePiece (..), MultiPiece (..))
|
||||
import Yesod.Internal.RouteParsing (THResource, Pieces (..), createRoutes, createRender, Resource (..), parseRoutes, parseRoutesFile)
|
||||
import Language.Haskell.TH.Syntax
|
||||
|
||||
import qualified Network.Wai as W
|
||||
import Network.Wai.Middleware.Jsonp
|
||||
import Network.Wai.Middleware.Gzip
|
||||
import Network.Wai.Middleware.Autohead
|
||||
|
||||
import Data.ByteString.Lazy.Char8 ()
|
||||
|
||||
import Web.ClientSession
|
||||
import Data.Char (isUpper)
|
||||
import Data.Text (Text)
|
||||
|
||||
type Texts = [Text]
|
||||
|
||||
-- | Generates URL datatype and site function for the given 'Resource's. This
|
||||
-- is used for creating sites, /not/ subsites. See 'mkYesodSub' for the latter.
|
||||
-- Use 'parseRoutes' to create the 'Resource's.
|
||||
mkYesod :: String -- ^ name of the argument datatype
|
||||
-> [Resource]
|
||||
-> Q [Dec]
|
||||
mkYesod name = fmap (uncurry (++)) . mkYesodGeneral name [] [] False
|
||||
|
||||
-- | Generates URL datatype and site function for the given 'Resource's. This
|
||||
-- is used for creating subsites, /not/ sites. See 'mkYesod' for the latter.
|
||||
-- Use 'parseRoutes' to create the 'Resource's. In general, a subsite is not
|
||||
-- executable by itself, but instead provides functionality to
|
||||
-- be embedded in other sites.
|
||||
mkYesodSub :: String -- ^ name of the argument datatype
|
||||
-> Cxt
|
||||
-> [Resource]
|
||||
-> Q [Dec]
|
||||
mkYesodSub name clazzes =
|
||||
fmap (uncurry (++)) . mkYesodGeneral name' rest clazzes True
|
||||
where
|
||||
(name':rest) = words name
|
||||
|
||||
-- | Sometimes, you will want to declare your routes in one file and define
|
||||
-- your handlers elsewhere. For example, this is the only way to break up a
|
||||
-- monolithic file into smaller parts. Use this function, paired with
|
||||
-- 'mkYesodDispatch', to do just that.
|
||||
mkYesodData :: String -> [Resource] -> Q [Dec]
|
||||
mkYesodData name res = mkYesodDataGeneral name [] False res
|
||||
|
||||
mkYesodSubData :: String -> Cxt -> [Resource] -> Q [Dec]
|
||||
mkYesodSubData name clazzes res = mkYesodDataGeneral name clazzes True res
|
||||
|
||||
mkYesodDataGeneral :: String -> Cxt -> Bool -> [Resource] -> Q [Dec]
|
||||
mkYesodDataGeneral name clazzes isSub res = do
|
||||
let (name':rest) = words name
|
||||
(x, _) <- mkYesodGeneral name' rest clazzes isSub res
|
||||
let rname = mkName $ "resources" ++ name
|
||||
eres <- lift res
|
||||
let y = [ SigD rname $ ListT `AppT` ConT ''Resource
|
||||
, FunD rname [Clause [] (NormalB eres) []]
|
||||
]
|
||||
return $ x ++ y
|
||||
|
||||
-- | See 'mkYesodData'.
|
||||
mkYesodDispatch :: String -> [Resource] -> Q [Dec]
|
||||
mkYesodDispatch name = fmap snd . mkYesodGeneral name [] [] False
|
||||
|
||||
mkYesodSubDispatch :: String -> Cxt -> [Resource] -> Q [Dec]
|
||||
mkYesodSubDispatch name clazzes = fmap snd . mkYesodGeneral name' rest clazzes True
|
||||
where (name':rest) = words name
|
||||
|
||||
mkYesodGeneral :: String -- ^ foundation name
|
||||
-> [String] -- ^ parameters for foundation
|
||||
-> Cxt -- ^ classes
|
||||
-> Bool -- ^ is subsite?
|
||||
-> [Resource]
|
||||
-> Q ([Dec], [Dec])
|
||||
mkYesodGeneral name args clazzes isSub res = do
|
||||
let name' = mkName name
|
||||
args' = map mkName args
|
||||
arg = foldl AppT (ConT name') $ map VarT args'
|
||||
th' <- mapM thResourceFromResource res
|
||||
let th = map fst th'
|
||||
w' <- createRoutes th
|
||||
let routesName = mkName $ name ++ "Route"
|
||||
let w = DataD [] routesName [] w' [''Show, ''Read, ''Eq]
|
||||
let x = TySynInstD ''Route [arg] $ ConT routesName
|
||||
|
||||
render <- createRender th
|
||||
let x' = InstanceD [] (ConT ''RenderRoute `AppT` ConT routesName)
|
||||
[ FunD (mkName "renderRoute") render
|
||||
]
|
||||
|
||||
let splitter :: (THResource, Maybe String)
|
||||
-> Either
|
||||
(THResource, Maybe String)
|
||||
(THResource, Maybe String)
|
||||
splitter a@((_, SubSite{}), _) = Left a
|
||||
splitter a = Right a
|
||||
let (resSub, resLoc) = partitionEithers $ map splitter th'
|
||||
yd <- mkYesodDispatch' resSub resLoc
|
||||
let master = mkName "master"
|
||||
let ctx = if isSub
|
||||
then ClassP (mkName "Yesod") [VarT master] : clazzes
|
||||
else []
|
||||
let ytyp = if isSub
|
||||
then ConT ''YesodDispatch `AppT` arg `AppT` VarT master
|
||||
else ConT ''YesodDispatch `AppT` arg `AppT` arg
|
||||
let y = InstanceD ctx ytyp [FunD (mkName "yesodDispatch") [yd]]
|
||||
return ([w, x, x'], [y])
|
||||
|
||||
thResourceFromResource :: Resource -> Q (THResource, Maybe String)
|
||||
thResourceFromResource (Resource n ps atts)
|
||||
| all (all isUpper) atts = return ((n, Simple ps atts), Nothing)
|
||||
thResourceFromResource (Resource n ps [stype, toSubArg]) = do
|
||||
let stype' = ConT $ mkName stype
|
||||
parse <- [|error "ssParse"|]
|
||||
dispatch <- [|error "ssDispatch"|]
|
||||
render <- [|renderRoute|]
|
||||
tmg <- [|error "ssToMasterArg"|]
|
||||
return ((n, SubSite
|
||||
{ ssType = ConT ''Route `AppT` stype'
|
||||
, ssParse = parse
|
||||
, ssRender = render
|
||||
, ssDispatch = dispatch
|
||||
, ssToMasterArg = tmg
|
||||
, ssPieces = ps
|
||||
}), Just toSubArg)
|
||||
|
||||
thResourceFromResource (Resource n _ _) =
|
||||
error $ "Invalid attributes for resource: " ++ n
|
||||
|
||||
-- | Convert the given argument into a WAI application, executable with any WAI
|
||||
-- handler. This is the same as 'toWaiAppPlain', except it includes three
|
||||
-- middlewares: GZIP compression, JSON-P and path cleaning. This is the
|
||||
-- recommended approach for most users.
|
||||
toWaiApp :: (Yesod y, YesodDispatch y y) => y -> IO W.Application
|
||||
toWaiApp y = gzip False . jsonp . autohead <$> toWaiAppPlain y
|
||||
|
||||
-- | Convert the given argument into a WAI application, executable with any WAI
|
||||
-- handler. This differs from 'toWaiApp' in that it uses no middlewares.
|
||||
toWaiAppPlain :: (Yesod y, YesodDispatch y y) => y -> IO W.Application
|
||||
toWaiAppPlain a = toWaiApp' a <$> encryptKey a
|
||||
|
||||
|
||||
toWaiApp' :: (Yesod y, YesodDispatch y y)
|
||||
=> y
|
||||
-> Maybe Key
|
||||
-> W.Application
|
||||
toWaiApp' y key' env =
|
||||
case yesodDispatch y key' (W.pathInfo env) y id of
|
||||
Just app -> app env
|
||||
Nothing -> yesodRunner y y id key' Nothing notFound env
|
||||
904
yesod-core/Yesod/Handler.hs
Normal file
904
yesod-core/Yesod/Handler.hs
Normal file
@ -0,0 +1,904 @@
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE TypeSynonymInstances #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE FunctionalDependencies #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
---------------------------------------------------------
|
||||
--
|
||||
-- Module : Yesod.Handler
|
||||
-- Copyright : Michael Snoyman
|
||||
-- License : BSD3
|
||||
--
|
||||
-- Maintainer : Michael Snoyman <michael@snoyman.com>
|
||||
-- Stability : unstable
|
||||
-- Portability : portable
|
||||
--
|
||||
-- Define Handler stuff.
|
||||
--
|
||||
---------------------------------------------------------
|
||||
module Yesod.Handler
|
||||
( -- * Type families
|
||||
Route
|
||||
, YesodSubRoute (..)
|
||||
-- * Handler monad
|
||||
, GHandler
|
||||
, GGHandler
|
||||
-- ** Read information from handler
|
||||
, getYesod
|
||||
, getYesodSub
|
||||
, getUrlRender
|
||||
, getUrlRenderParams
|
||||
, getCurrentRoute
|
||||
, getRouteToMaster
|
||||
, getRequest
|
||||
, waiRequest
|
||||
, runRequestBody
|
||||
-- * Special responses
|
||||
-- ** Redirecting
|
||||
, RedirectType (..)
|
||||
, redirect
|
||||
, redirectParams
|
||||
, redirectString
|
||||
, redirectText
|
||||
, redirectToPost
|
||||
-- ** Errors
|
||||
, notFound
|
||||
, badMethod
|
||||
, permissionDenied
|
||||
, permissionDeniedI
|
||||
, invalidArgs
|
||||
, invalidArgsI
|
||||
-- ** Short-circuit responses.
|
||||
, sendFile
|
||||
, sendFilePart
|
||||
, sendResponse
|
||||
, sendResponseStatus
|
||||
, sendResponseCreated
|
||||
, sendWaiResponse
|
||||
-- * Setting headers
|
||||
, setCookie
|
||||
, deleteCookie
|
||||
, setHeader
|
||||
, setLanguage
|
||||
-- ** Content caching and expiration
|
||||
, cacheSeconds
|
||||
, neverExpires
|
||||
, alreadyExpired
|
||||
, expiresAt
|
||||
-- * Session
|
||||
, SessionMap
|
||||
, lookupSession
|
||||
, getSession
|
||||
, setSession
|
||||
, deleteSession
|
||||
-- ** Ultimate destination
|
||||
, setUltDest
|
||||
, setUltDestString
|
||||
, setUltDestText
|
||||
, setUltDest'
|
||||
, setUltDestReferer
|
||||
, redirectUltDest
|
||||
, clearUltDest
|
||||
-- ** Messages
|
||||
, setMessage
|
||||
, setMessageI
|
||||
, getMessage
|
||||
-- * Helpers for specific content
|
||||
-- ** Hamlet
|
||||
, hamletToContent
|
||||
, hamletToRepHtml
|
||||
-- ** Misc
|
||||
, newIdent
|
||||
, liftIOHandler
|
||||
-- * i18n
|
||||
, getMessageRender
|
||||
-- * Internal Yesod
|
||||
, runHandler
|
||||
, YesodApp (..)
|
||||
, runSubsiteGetter
|
||||
, toMasterHandler
|
||||
, toMasterHandlerDyn
|
||||
, toMasterHandlerMaybe
|
||||
, localNoCurrent
|
||||
, HandlerData
|
||||
, ErrorResponse (..)
|
||||
, YesodAppResult (..)
|
||||
, handlerToYAR
|
||||
, yarToResponse
|
||||
, headerToPair
|
||||
) where
|
||||
|
||||
import Prelude hiding (catch)
|
||||
import Yesod.Internal.Request
|
||||
import Yesod.Internal
|
||||
import Data.Time (UTCTime)
|
||||
|
||||
import Control.Exception hiding (Handler, catch, finally)
|
||||
import qualified Control.Exception as E
|
||||
import Control.Applicative
|
||||
|
||||
import Control.Monad (liftM, join, MonadPlus)
|
||||
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Trans.Class
|
||||
import Control.Monad.Trans.Writer
|
||||
import Control.Monad.Trans.Reader
|
||||
import Control.Monad.Trans.State
|
||||
import Control.Monad.Trans.Error (throwError, ErrorT (..), Error (..))
|
||||
|
||||
import System.IO
|
||||
import qualified Network.Wai as W
|
||||
import qualified Network.HTTP.Types as H
|
||||
import Control.Failure (Failure (failure))
|
||||
|
||||
import Text.Hamlet
|
||||
import qualified Text.Blaze.Renderer.Text
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
|
||||
import Data.Text.Encoding.Error (lenientDecode)
|
||||
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.ByteString as S
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Enumerator (Iteratee (..), run_, ($$))
|
||||
import Network.Wai.Parse (parseHttpAccept)
|
||||
|
||||
import Yesod.Content
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Web.Cookie (SetCookie (..), renderSetCookie)
|
||||
import Control.Arrow (second, (***))
|
||||
import qualified Network.Wai.Parse as NWP
|
||||
import Data.Monoid (mappend, mempty, Endo (..))
|
||||
import qualified Data.ByteString.Char8 as S8
|
||||
import Data.CaseInsensitive (CI)
|
||||
import Blaze.ByteString.Builder (toByteString)
|
||||
import Data.Text (Text)
|
||||
import Yesod.Message (RenderMessage (..))
|
||||
|
||||
import Text.Blaze (toHtml, preEscapedText)
|
||||
|
||||
-- | The type-safe URLs associated with a site argument.
|
||||
type family Route a
|
||||
|
||||
class YesodSubRoute s y where
|
||||
fromSubRoute :: s -> y -> Route s -> Route y
|
||||
|
||||
data HandlerData sub master = HandlerData
|
||||
{ handlerRequest :: Request
|
||||
, handlerSub :: sub
|
||||
, handlerMaster :: master
|
||||
, handlerRoute :: Maybe (Route sub)
|
||||
, handlerRender :: Route master -> [(Text, Text)] -> Text
|
||||
, handlerToMaster :: Route sub -> Route master
|
||||
}
|
||||
|
||||
handlerSubData :: (Route sub -> Route master)
|
||||
-> (master -> sub)
|
||||
-> Route sub
|
||||
-> HandlerData oldSub master
|
||||
-> HandlerData sub master
|
||||
handlerSubData tm ts = handlerSubDataMaybe tm ts . Just
|
||||
|
||||
handlerSubDataMaybe :: (Route sub -> Route master)
|
||||
-> (master -> sub)
|
||||
-> Maybe (Route sub)
|
||||
-> HandlerData oldSub master
|
||||
-> HandlerData sub master
|
||||
handlerSubDataMaybe tm ts route hd = hd
|
||||
{ handlerSub = ts $ handlerMaster hd
|
||||
, handlerToMaster = tm
|
||||
, handlerRoute = route
|
||||
}
|
||||
|
||||
-- | Used internally for promoting subsite handler functions to master site
|
||||
-- handler functions. Should not be needed by users.
|
||||
toMasterHandler :: (Route sub -> Route master)
|
||||
-> (master -> sub)
|
||||
-> Route sub
|
||||
-> GGHandler sub master mo a
|
||||
-> GGHandler sub' master mo a
|
||||
toMasterHandler tm ts route (GHandler h) =
|
||||
GHandler $ withReaderT (handlerSubData tm ts route) h
|
||||
|
||||
toMasterHandlerDyn :: Monad mo
|
||||
=> (Route sub -> Route master)
|
||||
-> GGHandler sub' master mo sub
|
||||
-> Route sub
|
||||
-> GGHandler sub master mo a
|
||||
-> GGHandler sub' master mo a
|
||||
toMasterHandlerDyn tm getSub route (GHandler h) = do
|
||||
sub <- getSub
|
||||
GHandler $ withReaderT (handlerSubData tm (const sub) route) h
|
||||
|
||||
class SubsiteGetter g m s | g -> s where
|
||||
runSubsiteGetter :: g -> m s
|
||||
|
||||
instance (master ~ master'
|
||||
) => SubsiteGetter (master -> sub) (GHandler anySub master') sub where
|
||||
runSubsiteGetter getter = getter <$> getYesod
|
||||
|
||||
instance (anySub ~ anySub'
|
||||
,master ~ master'
|
||||
) => SubsiteGetter (GHandler anySub master sub) (GHandler anySub' master') sub where
|
||||
runSubsiteGetter = id
|
||||
|
||||
toMasterHandlerMaybe :: (Route sub -> Route master)
|
||||
-> (master -> sub)
|
||||
-> Maybe (Route sub)
|
||||
-> GGHandler sub master mo a
|
||||
-> GGHandler sub' master mo a
|
||||
toMasterHandlerMaybe tm ts route (GHandler h) =
|
||||
GHandler $ withReaderT (handlerSubDataMaybe tm ts route) h
|
||||
|
||||
-- | A generic handler monad, which can have a different subsite and master
|
||||
-- site. This monad is a combination of 'ReaderT' for basic arguments, a
|
||||
-- 'WriterT' for headers and session, and an 'MEitherT' monad for handling
|
||||
-- special responses. It is declared as a newtype to make compiler errors more
|
||||
-- readable.
|
||||
newtype GGHandler sub master m a =
|
||||
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)
|
||||
|
||||
data GHState = GHState
|
||||
{ ghsSession :: SessionMap
|
||||
, ghsRBC :: Maybe RequestBodyContents
|
||||
, ghsIdent :: Int
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
-- | An extension of the basic WAI 'W.Application' datatype to provide extra
|
||||
-- features needed by Yesod. Users should never need to use this directly, as
|
||||
-- the 'GHandler' monad and template haskell code should hide it away.
|
||||
newtype YesodApp = YesodApp
|
||||
{ unYesodApp
|
||||
:: (ErrorResponse -> YesodApp)
|
||||
-> Request
|
||||
-> [ContentType]
|
||||
-> SessionMap
|
||||
-> Iteratee ByteString IO YesodAppResult
|
||||
}
|
||||
|
||||
data YesodAppResult
|
||||
= YARWai W.Response
|
||||
| YARPlain H.Status [Header] ContentType Content SessionMap
|
||||
|
||||
data HandlerContents =
|
||||
HCContent H.Status ChooseRep
|
||||
| HCError ErrorResponse
|
||||
| HCSendFile ContentType FilePath (Maybe W.FilePart) -- FIXME replace FilePath with opaque type from system-filepath?
|
||||
| HCRedirect RedirectType Text
|
||||
| HCCreated Text
|
||||
| HCWai W.Response
|
||||
|
||||
instance Error HandlerContents where
|
||||
strMsg = HCError . InternalError . T.pack
|
||||
|
||||
getRequest :: Monad mo => GGHandler s m mo Request
|
||||
getRequest = handlerRequest `liftM` GHandler ask
|
||||
|
||||
instance Monad monad => Failure ErrorResponse (GGHandler sub master monad) where
|
||||
failure = GHandler . lift . throwError . HCError
|
||||
|
||||
runRequestBody :: GHandler s m RequestBodyContents
|
||||
runRequestBody = do
|
||||
x <- GHandler $ lift $ lift $ lift get
|
||||
case ghsRBC x of
|
||||
Just rbc -> return rbc
|
||||
Nothing -> do
|
||||
rr <- waiRequest
|
||||
rbc <- lift $ rbHelper rr
|
||||
GHandler $ lift $ lift $ lift $ put x { ghsRBC = Just rbc }
|
||||
return rbc
|
||||
|
||||
rbHelper :: W.Request -> Iteratee ByteString IO RequestBodyContents
|
||||
rbHelper req =
|
||||
(map fix1 *** map fix2) <$> iter
|
||||
where
|
||||
iter = NWP.parseRequestBody NWP.lbsSink req
|
||||
fix1 = go *** go
|
||||
fix2 (x, NWP.FileInfo a b c) =
|
||||
(go x, FileInfo (go a) (go b) c)
|
||||
go = decodeUtf8With lenientDecode
|
||||
|
||||
-- | Get the sub application argument.
|
||||
getYesodSub :: Monad m => GGHandler sub master m sub
|
||||
getYesodSub = handlerSub `liftM` GHandler ask
|
||||
|
||||
-- | Get the master site appliation argument.
|
||||
getYesod :: Monad m => GGHandler sub master m master
|
||||
getYesod = handlerMaster `liftM` GHandler ask
|
||||
|
||||
-- | Get the URL rendering function.
|
||||
getUrlRender :: Monad m => GGHandler sub master m (Route master -> Text)
|
||||
getUrlRender = do
|
||||
x <- handlerRender `liftM` GHandler ask
|
||||
return $ flip x []
|
||||
|
||||
-- | The URL rendering function with query-string parameters.
|
||||
getUrlRenderParams
|
||||
:: Monad m
|
||||
=> GGHandler sub master m (Route master -> [(Text, Text)] -> Text)
|
||||
getUrlRenderParams = handlerRender `liftM` GHandler ask
|
||||
|
||||
-- | 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'.
|
||||
getCurrentRoute :: Monad m => GGHandler sub master m (Maybe (Route sub))
|
||||
getCurrentRoute = handlerRoute `liftM` GHandler ask
|
||||
|
||||
-- | Get the function to promote a route for a subsite to a route for the
|
||||
-- master site.
|
||||
getRouteToMaster :: Monad m => GGHandler sub master m (Route sub -> Route master)
|
||||
getRouteToMaster = handlerToMaster `liftM` GHandler ask
|
||||
|
||||
-- | Function used internally by Yesod in the process of converting a
|
||||
-- 'GHandler' into an 'W.Application'. Should not be needed by users.
|
||||
runHandler :: HasReps c
|
||||
=> GHandler sub master c
|
||||
-> (Route master -> [(Text, Text)] -> Text)
|
||||
-> Maybe (Route sub)
|
||||
-> (Route sub -> Route master)
|
||||
-> master
|
||||
-> sub
|
||||
-> YesodApp
|
||||
runHandler handler mrender sroute tomr ma sa =
|
||||
YesodApp $ \eh rr cts initSession -> do
|
||||
let toErrorHandler e =
|
||||
case fromException e of
|
||||
Just x -> x
|
||||
Nothing -> InternalError $ T.pack $ show e
|
||||
let hd = HandlerData
|
||||
{ handlerRequest = rr
|
||||
, handlerSub = sa
|
||||
, handlerMaster = ma
|
||||
, handlerRoute = sroute
|
||||
, handlerRender = mrender
|
||||
, handlerToMaster = tomr
|
||||
}
|
||||
let initSession' = GHState initSession Nothing 1
|
||||
((contents', headers), finalSession) <- catchIter (
|
||||
fmap (second ghsSession)
|
||||
$ flip runStateT initSession'
|
||||
$ runWriterT
|
||||
$ runErrorT
|
||||
$ flip runReaderT hd
|
||||
$ unGHandler handler
|
||||
) (\e -> return ((Left $ HCError $ toErrorHandler e, mempty), initSession))
|
||||
let contents = either id (HCContent H.status200 . chooseRep) contents'
|
||||
let handleError e = do
|
||||
yar <- unYesodApp (eh e) safeEh rr cts finalSession
|
||||
case yar of
|
||||
YARPlain _ hs ct c sess ->
|
||||
let hs' = appEndo headers hs
|
||||
in return $ YARPlain (getStatus e) hs' ct c sess
|
||||
YARWai _ -> return yar
|
||||
let sendFile' ct fp p =
|
||||
return $ YARPlain H.status200 (appEndo headers []) ct (ContentFile fp p) finalSession
|
||||
case contents of
|
||||
HCContent status a -> do
|
||||
(ct, c) <- liftIO $ a cts
|
||||
return $ YARPlain status (appEndo headers []) ct c finalSession
|
||||
HCError e -> handleError e
|
||||
HCRedirect rt loc -> do
|
||||
let hs = Header "Location" (encodeUtf8 loc) : appEndo headers []
|
||||
return $ YARPlain
|
||||
(getRedirectStatus rt) hs typePlain emptyContent
|
||||
finalSession
|
||||
HCSendFile ct fp p -> catchIter
|
||||
(sendFile' ct fp p)
|
||||
(handleError . toErrorHandler)
|
||||
HCCreated loc -> do
|
||||
let hs = Header "Location" (encodeUtf8 loc) : appEndo headers []
|
||||
return $ YARPlain
|
||||
H.status201
|
||||
hs
|
||||
typePlain
|
||||
emptyContent
|
||||
finalSession
|
||||
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 er = YesodApp $ \_ _ _ session -> do
|
||||
liftIO $ hPutStrLn stderr $ "Error handler errored out: " ++ show er
|
||||
return $ YARPlain
|
||||
H.status500
|
||||
[]
|
||||
typePlain
|
||||
(toContent ("Internal Server Error" :: S.ByteString))
|
||||
session
|
||||
|
||||
-- | Redirect to the given route.
|
||||
redirect :: Monad mo => RedirectType -> Route master -> GGHandler sub master mo a
|
||||
redirect rt url = redirectParams rt url []
|
||||
|
||||
-- | Redirects to the given route with the associated query-string parameters.
|
||||
redirectParams :: Monad mo
|
||||
=> RedirectType -> Route master -> [(Text, Text)]
|
||||
-> GGHandler sub master mo a
|
||||
redirectParams rt url params = do
|
||||
r <- getUrlRenderParams
|
||||
redirectString rt $ r url params
|
||||
|
||||
-- | Redirect to the given URL.
|
||||
redirectString, redirectText :: Monad mo => RedirectType -> Text -> GGHandler sub master mo a
|
||||
redirectText rt = GHandler . lift . throwError . HCRedirect rt
|
||||
redirectString = redirectText
|
||||
{-# DEPRECATED redirectString "Use redirectText instead" #-}
|
||||
|
||||
ultDestKey :: Text
|
||||
ultDestKey = "_ULT"
|
||||
|
||||
-- | Sets the ultimate destination variable to the given route.
|
||||
--
|
||||
-- An ultimate destination is stored in the user session and can be loaded
|
||||
-- later by 'redirectUltDest'.
|
||||
setUltDest :: Monad mo => Route master -> GGHandler sub master mo ()
|
||||
setUltDest dest = do
|
||||
render <- getUrlRender
|
||||
setUltDestString $ render dest
|
||||
|
||||
-- | Same as 'setUltDest', but use the given string.
|
||||
setUltDestText :: Monad mo => Text -> GGHandler sub master mo ()
|
||||
setUltDestText = setSession ultDestKey
|
||||
|
||||
setUltDestString :: Monad mo => Text -> GGHandler sub master mo ()
|
||||
setUltDestString = setSession ultDestKey
|
||||
{-# DEPRECATED setUltDestString "Use setUltDestText instead" #-}
|
||||
|
||||
-- | Same as 'setUltDest', but uses the current page.
|
||||
--
|
||||
-- If this is a 404 handler, there is no current page, and then this call does
|
||||
-- nothing.
|
||||
setUltDest' :: Monad mo => GGHandler sub master mo ()
|
||||
setUltDest' = do
|
||||
route <- getCurrentRoute
|
||||
case route of
|
||||
Nothing -> return ()
|
||||
Just r -> do
|
||||
tm <- getRouteToMaster
|
||||
gets' <- reqGetParams `liftM` handlerRequest `liftM` GHandler ask
|
||||
render <- getUrlRenderParams
|
||||
setUltDestString $ render (tm r) gets'
|
||||
|
||||
-- | Sets the ultimate destination to the referer request header, if present.
|
||||
--
|
||||
-- This function will not overwrite an existing ultdest.
|
||||
setUltDestReferer :: Monad mo => GGHandler sub master mo ()
|
||||
setUltDestReferer = do
|
||||
mdest <- lookupSession ultDestKey
|
||||
maybe
|
||||
(waiRequest >>= maybe (return ()) setUltDestBS . lookup "referer" . W.requestHeaders)
|
||||
(const $ return ())
|
||||
mdest
|
||||
where
|
||||
setUltDestBS = setUltDestText . T.pack . S8.unpack
|
||||
|
||||
-- | Redirect to the ultimate destination in the user's session. Clear the
|
||||
-- value from the session.
|
||||
--
|
||||
-- The ultimate destination is set with 'setUltDest'.
|
||||
redirectUltDest :: Monad mo
|
||||
=> RedirectType
|
||||
-> Route master -- ^ default destination if nothing in session
|
||||
-> GGHandler sub master mo a
|
||||
redirectUltDest rt def = do
|
||||
mdest <- lookupSession ultDestKey
|
||||
deleteSession ultDestKey
|
||||
maybe (redirect rt def) (redirectText rt) mdest
|
||||
|
||||
-- | Remove a previously set ultimate destination. See 'setUltDest'.
|
||||
clearUltDest :: Monad mo => GGHandler sub master mo ()
|
||||
clearUltDest = deleteSession ultDestKey
|
||||
|
||||
msgKey :: Text
|
||||
msgKey = "_MSG"
|
||||
|
||||
-- | Sets a message in the user's session.
|
||||
--
|
||||
-- See 'getMessage'.
|
||||
setMessage :: Monad mo => Html -> GGHandler sub master mo ()
|
||||
setMessage = setSession msgKey . T.concat . TL.toChunks . Text.Blaze.Renderer.Text.renderHtml
|
||||
|
||||
-- | Sets a message in the user's session.
|
||||
--
|
||||
-- See 'getMessage'.
|
||||
setMessageI :: (RenderMessage y msg, Monad mo) => msg -> GGHandler sub y mo ()
|
||||
setMessageI msg = do
|
||||
mr <- getMessageRender
|
||||
setMessage $ toHtml $ mr msg
|
||||
|
||||
-- | Gets the message in the user's session, if available, and then clears the
|
||||
-- variable.
|
||||
--
|
||||
-- See 'setMessage'.
|
||||
getMessage :: Monad mo => GGHandler sub master mo (Maybe Html)
|
||||
getMessage = do
|
||||
mmsg <- liftM (fmap preEscapedText) $ lookupSession msgKey
|
||||
deleteSession msgKey
|
||||
return mmsg
|
||||
|
||||
-- | Bypass remaining handler code and output the given file.
|
||||
--
|
||||
-- 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.
|
||||
sendFile :: Monad mo => ContentType -> FilePath -> GGHandler sub master mo a
|
||||
sendFile ct fp = GHandler . lift . throwError $ HCSendFile ct fp Nothing
|
||||
|
||||
-- | Same as 'sendFile', but only sends part of a file.
|
||||
sendFilePart :: Monad mo
|
||||
=> ContentType
|
||||
-> FilePath
|
||||
-> Integer -- ^ offset
|
||||
-> Integer -- ^ count
|
||||
-> GGHandler sub master mo a
|
||||
sendFilePart ct fp off count =
|
||||
GHandler . lift . throwError $ HCSendFile ct fp $ Just $ W.FilePart off count
|
||||
|
||||
-- | Bypass remaining handler code and output the given content with a 200
|
||||
-- status code.
|
||||
sendResponse :: (Monad mo, HasReps c) => c -> GGHandler sub master mo a
|
||||
sendResponse = GHandler . lift . throwError . HCContent H.status200
|
||||
. chooseRep
|
||||
|
||||
-- | Bypass remaining handler code and output the given content with the given
|
||||
-- status code.
|
||||
sendResponseStatus :: (Monad mo, HasReps c) => H.Status -> c -> GGHandler s m mo a
|
||||
sendResponseStatus s = GHandler . lift . throwError . HCContent s
|
||||
. chooseRep
|
||||
|
||||
-- | Send a 201 "Created" response with the given route as the Location
|
||||
-- response header.
|
||||
sendResponseCreated :: Monad mo => Route m -> GGHandler s m mo a
|
||||
sendResponseCreated url = do
|
||||
r <- getUrlRender
|
||||
GHandler $ lift $ throwError $ HCCreated $ r url
|
||||
|
||||
-- | Send a 'W.Response'. Please note: this function is rarely
|
||||
-- necessary, and will /disregard/ any changes to response headers and session
|
||||
-- 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,
|
||||
-- you don't.
|
||||
sendWaiResponse :: Monad mo => W.Response -> GGHandler s m mo b
|
||||
sendWaiResponse = GHandler . lift . throwError . HCWai
|
||||
|
||||
-- | Return a 404 not found page. Also denotes no handler available.
|
||||
notFound :: Failure ErrorResponse m => m a
|
||||
notFound = failure NotFound
|
||||
|
||||
-- | Return a 405 method not supported page.
|
||||
badMethod :: Monad mo => GGHandler s m mo a
|
||||
badMethod = do
|
||||
w <- waiRequest
|
||||
failure $ BadMethod $ W.requestMethod w
|
||||
|
||||
-- | Return a 403 permission denied page.
|
||||
permissionDenied :: Failure ErrorResponse m => Text -> m a
|
||||
permissionDenied = failure . PermissionDenied
|
||||
|
||||
-- | Return a 403 permission denied page.
|
||||
permissionDeniedI :: (RenderMessage y msg, Monad mo) => msg -> GGHandler s y mo a
|
||||
permissionDeniedI msg = do
|
||||
mr <- getMessageRender
|
||||
permissionDenied $ mr msg
|
||||
|
||||
-- | Return a 400 invalid arguments page.
|
||||
invalidArgs :: Failure ErrorResponse m => [Text] -> m a
|
||||
invalidArgs = failure . InvalidArgs
|
||||
|
||||
-- | Return a 400 invalid arguments page.
|
||||
invalidArgsI :: (RenderMessage y msg, Monad mo) => [msg] -> GGHandler s y mo a
|
||||
invalidArgsI msg = do
|
||||
mr <- getMessageRender
|
||||
invalidArgs $ map mr msg
|
||||
|
||||
------- Headers
|
||||
-- | Set the cookie on the client.
|
||||
setCookie :: Monad mo
|
||||
=> Int -- ^ minutes to timeout
|
||||
-> H.Ascii -- ^ key
|
||||
-> H.Ascii -- ^ value
|
||||
-> GGHandler sub master mo ()
|
||||
setCookie a b = addHeader . AddCookie a b
|
||||
|
||||
-- | Unset the cookie on the client.
|
||||
deleteCookie :: Monad mo => H.Ascii -> GGHandler sub master mo ()
|
||||
deleteCookie = addHeader . DeleteCookie
|
||||
|
||||
-- | Set the language in the user session. Will show up in 'languages' on the
|
||||
-- next request.
|
||||
setLanguage :: Monad mo => Text -> GGHandler sub master mo ()
|
||||
setLanguage = setSession langKey
|
||||
|
||||
-- | Set an arbitrary response header.
|
||||
setHeader :: Monad mo
|
||||
=> CI H.Ascii -> H.Ascii -> GGHandler sub master mo ()
|
||||
setHeader a = addHeader . Header a
|
||||
|
||||
-- | Set the Cache-Control header to indicate this response should be cached
|
||||
-- for the given number of seconds.
|
||||
cacheSeconds :: Monad mo => Int -> GGHandler s m mo ()
|
||||
cacheSeconds i = setHeader "Cache-Control" $ S8.pack $ concat
|
||||
[ "max-age="
|
||||
, show i
|
||||
, ", public"
|
||||
]
|
||||
|
||||
-- | Set the Expires header to some date in 2037. In other words, this content
|
||||
-- is never (realistically) expired.
|
||||
neverExpires :: Monad mo => GGHandler s m mo ()
|
||||
neverExpires = setHeader "Expires" "Thu, 31 Dec 2037 23:55:55 GMT"
|
||||
|
||||
-- | Set an Expires header in the past, meaning this content should not be
|
||||
-- cached.
|
||||
alreadyExpired :: Monad mo => GGHandler s m mo ()
|
||||
alreadyExpired = setHeader "Expires" "Thu, 01 Jan 1970 05:05:05 GMT"
|
||||
|
||||
-- | Set an Expires header to the given date.
|
||||
expiresAt :: Monad mo => UTCTime -> GGHandler s m mo ()
|
||||
expiresAt = setHeader "Expires" . encodeUtf8 . formatRFC1123
|
||||
|
||||
-- | Set a variable in the user's session.
|
||||
--
|
||||
-- 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
|
||||
-- not tampered with.
|
||||
setSession :: Monad mo
|
||||
=> Text -- ^ key
|
||||
-> Text -- ^ value
|
||||
-> GGHandler sub master mo ()
|
||||
setSession k = GHandler . lift . lift . lift . modify . modSession . Map.insert k
|
||||
|
||||
-- | Unsets a session variable. See 'setSession'.
|
||||
deleteSession :: Monad mo => Text -> GGHandler sub master mo ()
|
||||
deleteSession = GHandler . lift . lift . lift . modify . modSession . Map.delete
|
||||
|
||||
modSession :: (SessionMap -> SessionMap) -> GHState -> GHState
|
||||
modSession f x = x { ghsSession = f $ ghsSession x }
|
||||
|
||||
-- | Internal use only, not to be confused with 'setHeader'.
|
||||
addHeader :: Monad mo => Header -> GGHandler sub master mo ()
|
||||
addHeader = GHandler . lift . lift . tell . Endo . (:)
|
||||
|
||||
getStatus :: ErrorResponse -> H.Status
|
||||
getStatus NotFound = H.status404
|
||||
getStatus (InternalError _) = H.status500
|
||||
getStatus (InvalidArgs _) = H.status400
|
||||
getStatus (PermissionDenied _) = H.status403
|
||||
getStatus (BadMethod _) = H.status405
|
||||
|
||||
getRedirectStatus :: RedirectType -> H.Status
|
||||
getRedirectStatus RedirectPermanent = H.status301
|
||||
getRedirectStatus RedirectTemporary = H.status302
|
||||
getRedirectStatus RedirectSeeOther = H.status303
|
||||
|
||||
-- | Different types of redirects.
|
||||
data RedirectType = RedirectPermanent
|
||||
| RedirectTemporary
|
||||
| RedirectSeeOther
|
||||
deriving (Show, Eq)
|
||||
|
||||
localNoCurrent :: Monad mo => GGHandler s m mo a -> GGHandler s m mo a
|
||||
localNoCurrent =
|
||||
GHandler . local (\hd -> hd { handlerRoute = Nothing }) . unGHandler
|
||||
|
||||
-- | Lookup for session data.
|
||||
lookupSession :: Monad mo => Text -> GGHandler s m mo (Maybe Text)
|
||||
lookupSession n = GHandler $ do
|
||||
m <- liftM ghsSession $ lift $ lift $ lift get
|
||||
return $ Map.lookup n m
|
||||
|
||||
-- | Get all session variables.
|
||||
getSession :: Monad mo => GGHandler s m mo SessionMap
|
||||
getSession = liftM ghsSession $ GHandler $ lift $ lift $ lift get
|
||||
|
||||
handlerToYAR :: (HasReps a, HasReps b)
|
||||
=> m -- ^ master site foundation
|
||||
-> s -- ^ sub site foundation
|
||||
-> (Route s -> Route m)
|
||||
-> (Route m -> [(Text, Text)] -> Text)
|
||||
-> (ErrorResponse -> GHandler s m a)
|
||||
-> Request
|
||||
-> Maybe (Route s)
|
||||
-> SessionMap
|
||||
-> GHandler s m b
|
||||
-> Iteratee ByteString IO YesodAppResult
|
||||
handlerToYAR y s toMasterRoute render errorHandler rr murl sessionMap h =
|
||||
unYesodApp ya eh' rr types sessionMap
|
||||
where
|
||||
ya = runHandler h render murl toMasterRoute y s
|
||||
eh' er = runHandler (errorHandler' er) render murl toMasterRoute y s
|
||||
types = httpAccept $ reqWaiRequest rr
|
||||
errorHandler' = localNoCurrent . errorHandler
|
||||
|
||||
type HeaderRenderer = [Header]
|
||||
-> ContentType
|
||||
-> SessionMap
|
||||
-> [(CI H.Ascii, H.Ascii)]
|
||||
|
||||
yarToResponse :: HeaderRenderer -> YesodAppResult -> W.Response
|
||||
yarToResponse _ (YARWai a) = a
|
||||
yarToResponse renderHeaders (YARPlain s hs ct c sessionFinal) =
|
||||
case c of
|
||||
ContentBuilder b mlen ->
|
||||
let hs' = maybe finalHeaders finalHeaders' mlen
|
||||
in W.ResponseBuilder s hs' b
|
||||
ContentFile fp p -> W.ResponseFile s finalHeaders fp p
|
||||
ContentEnum e ->
|
||||
W.ResponseEnumerator $ \iter -> run_ $ e $$ iter s finalHeaders
|
||||
where
|
||||
finalHeaders = renderHeaders hs ct sessionFinal
|
||||
finalHeaders' len = ("Content-Length", S8.pack $ show len)
|
||||
: finalHeaders
|
||||
{-
|
||||
getExpires m = fromIntegral (m * 60) `addUTCTime` now
|
||||
sessionVal =
|
||||
case key' of
|
||||
Nothing -> B.empty
|
||||
Just key'' -> encodeSession key'' exp' host
|
||||
$ Map.toList
|
||||
$ Map.insert nonceKey (reqNonce rr) sessionFinal
|
||||
hs' =
|
||||
case key' of
|
||||
Nothing -> hs
|
||||
Just _ -> AddCookie
|
||||
(clientSessionDuration y)
|
||||
sessionName
|
||||
(bsToChars sessionVal)
|
||||
: hs
|
||||
hs'' = map (headerToPair getExpires) hs'
|
||||
hs''' = ("Content-Type", charsToBs ct) : hs''
|
||||
-}
|
||||
|
||||
httpAccept :: W.Request -> [ContentType]
|
||||
httpAccept = parseHttpAccept
|
||||
. fromMaybe mempty
|
||||
. lookup "Accept"
|
||||
. W.requestHeaders
|
||||
|
||||
-- | Convert Header to a key/value pair.
|
||||
headerToPair :: S.ByteString -- ^ cookie path
|
||||
-> (Int -> UTCTime) -- ^ minutes -> expiration time
|
||||
-> Header
|
||||
-> (CI H.Ascii, H.Ascii)
|
||||
headerToPair cp getExpires (AddCookie minutes key value) =
|
||||
("Set-Cookie", toByteString $ renderSetCookie $ SetCookie
|
||||
{ setCookieName = key
|
||||
, setCookieValue = value
|
||||
, setCookiePath = Just cp
|
||||
, setCookieExpires =
|
||||
if minutes == 0
|
||||
then Nothing
|
||||
else Just $ getExpires minutes
|
||||
, setCookieDomain = Nothing
|
||||
, setCookieHttpOnly = True
|
||||
})
|
||||
headerToPair cp _ (DeleteCookie key) =
|
||||
( "Set-Cookie"
|
||||
, key `mappend` "=; path=" `mappend` cp `mappend` "; expires=Thu, 01-Jan-1970 00:00:00 GMT"
|
||||
)
|
||||
headerToPair _ _ (Header key value) = (key, value)
|
||||
|
||||
-- | Get a unique identifier.
|
||||
newIdent :: Monad mo => GGHandler sub master mo String -- FIXME use Text
|
||||
newIdent = GHandler $ lift $ lift $ lift $ do
|
||||
x <- get
|
||||
let i' = ghsIdent x + 1
|
||||
put x { ghsIdent = i' }
|
||||
return $ 'h' : show i'
|
||||
|
||||
liftIOHandler :: MonadIO mo
|
||||
=> GGHandler sub master IO a
|
||||
-> GGHandler sub master mo a
|
||||
liftIOHandler m = GHandler $
|
||||
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.
|
||||
--
|
||||
-- This is not technically a redirect; instead, it returns an HTML page with a
|
||||
-- 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
|
||||
-- changes on the server.
|
||||
redirectToPost :: Monad mo => Route master -> GGHandler sub master mo a
|
||||
redirectToPost dest = hamletToRepHtml
|
||||
#if GHC7
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
\<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Redirecting...
|
||||
<body onload="document.getElementById('form').submit()">
|
||||
<form id="form" method="post" action="@{dest}">
|
||||
<noscript>
|
||||
<p>Javascript has been disabled; please click on the button below to be redirected.
|
||||
<input type="submit" value="Continue">
|
||||
|] >>= sendResponse
|
||||
|
||||
-- | Converts the given Hamlet template into 'Content', which can be used in a
|
||||
-- Yesod 'Response'.
|
||||
hamletToContent :: Monad mo
|
||||
=> Hamlet (Route master) -> GGHandler sub master mo Content
|
||||
hamletToContent h = do
|
||||
render <- getUrlRenderParams
|
||||
return $ toContent $ h render
|
||||
|
||||
-- | Wraps the 'Content' generated by 'hamletToContent' in a 'RepHtml'.
|
||||
hamletToRepHtml :: Monad mo
|
||||
=> Hamlet (Route master) -> GGHandler sub master mo RepHtml
|
||||
hamletToRepHtml = liftM RepHtml . hamletToContent
|
||||
|
||||
-- | Get the request\'s 'W.Request' value.
|
||||
waiRequest :: Monad mo => GGHandler sub master mo W.Request
|
||||
waiRequest = reqWaiRequest `liftM` getRequest
|
||||
|
||||
getMessageRender :: (Monad mo, RenderMessage master message) => GGHandler s master mo (message -> Text)
|
||||
getMessageRender = do
|
||||
m <- getYesod
|
||||
l <- reqLangs `liftM` getRequest
|
||||
return $ renderMessage m l
|
||||
128
yesod-core/Yesod/Internal.hs
Normal file
128
yesod-core/Yesod/Internal.hs
Normal file
@ -0,0 +1,128 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
-- | Normal users should never need access to these.
|
||||
module Yesod.Internal
|
||||
( -- * Error responses
|
||||
ErrorResponse (..)
|
||||
-- * Header
|
||||
, Header (..)
|
||||
-- * Cookie names
|
||||
, langKey
|
||||
-- * Widgets
|
||||
, GWData (..)
|
||||
, Location (..)
|
||||
, UniqueList (..)
|
||||
, Script (..)
|
||||
, Stylesheet (..)
|
||||
, Title (..)
|
||||
, Head (..)
|
||||
, Body (..)
|
||||
, locationToHamlet
|
||||
, runUniqueList
|
||||
, toUnique
|
||||
-- * Names
|
||||
, sessionName
|
||||
, nonceKey
|
||||
) where
|
||||
|
||||
import Text.Hamlet (Hamlet, hamlet, Html)
|
||||
import Text.Cassius (Cassius)
|
||||
import Text.Julius (Julius)
|
||||
import Data.Monoid (Monoid (..), Last)
|
||||
import Data.List (nub)
|
||||
|
||||
import Data.Text (Text)
|
||||
|
||||
import Data.Typeable (Typeable)
|
||||
import Control.Exception (Exception)
|
||||
|
||||
import qualified Network.HTTP.Types as H
|
||||
import qualified Network.HTTP.Types as A
|
||||
import Data.CaseInsensitive (CI)
|
||||
import Data.String (IsString)
|
||||
import qualified Data.Map as Map
|
||||
|
||||
#if GHC7
|
||||
#define HAMLET hamlet
|
||||
#else
|
||||
#define HAMLET $hamlet
|
||||
#endif
|
||||
|
||||
-- | Responses to indicate some form of an error occurred. These are different
|
||||
-- from 'SpecialResponse' in that they allow for custom error pages.
|
||||
data ErrorResponse =
|
||||
NotFound
|
||||
| InternalError Text
|
||||
| InvalidArgs [Text]
|
||||
| PermissionDenied Text
|
||||
| BadMethod H.Method
|
||||
deriving (Show, Eq, Typeable)
|
||||
instance Exception ErrorResponse
|
||||
|
||||
----- header stuff
|
||||
-- | Headers to be added to a 'Result'.
|
||||
data Header =
|
||||
AddCookie Int A.Ascii A.Ascii
|
||||
| DeleteCookie A.Ascii
|
||||
| Header (CI A.Ascii) A.Ascii
|
||||
deriving (Eq, Show)
|
||||
|
||||
langKey :: IsString a => a
|
||||
langKey = "_LANG"
|
||||
|
||||
data Location url = Local url | Remote Text
|
||||
deriving (Show, Eq)
|
||||
locationToHamlet :: Location url -> Hamlet url
|
||||
locationToHamlet (Local url) = [HAMLET|\@{url}
|
||||
|]
|
||||
locationToHamlet (Remote s) = [HAMLET|\#{s}
|
||||
|]
|
||||
|
||||
newtype UniqueList x = UniqueList ([x] -> [x])
|
||||
instance Monoid (UniqueList x) where
|
||||
mempty = UniqueList id
|
||||
UniqueList x `mappend` UniqueList y = UniqueList $ x . y
|
||||
runUniqueList :: Eq x => UniqueList x -> [x]
|
||||
runUniqueList (UniqueList x) = nub $ x []
|
||||
toUnique :: x -> UniqueList x
|
||||
toUnique = UniqueList . (:)
|
||||
|
||||
data Script url = Script { scriptLocation :: Location url, scriptAttributes :: [(Text, Text)] }
|
||||
deriving (Show, Eq)
|
||||
data Stylesheet url = Stylesheet { styleLocation :: Location url, styleAttributes :: [(Text, Text)] }
|
||||
deriving (Show, Eq)
|
||||
newtype Title = Title { unTitle :: Html }
|
||||
|
||||
newtype Head url = Head (Hamlet url)
|
||||
deriving Monoid
|
||||
newtype Body url = Body (Hamlet url)
|
||||
deriving Monoid
|
||||
|
||||
nonceKey :: IsString a => a
|
||||
nonceKey = "_NONCE"
|
||||
|
||||
sessionName :: IsString a => a
|
||||
sessionName = "_SESSION"
|
||||
|
||||
data GWData a = GWData
|
||||
!(Body a)
|
||||
!(Last Title)
|
||||
!(UniqueList (Script a))
|
||||
!(UniqueList (Stylesheet a))
|
||||
!(Map.Map (Maybe Text) (Cassius a)) -- media type
|
||||
!(Maybe (Julius a))
|
||||
!(Head a)
|
||||
instance Monoid (GWData a) where
|
||||
mempty = GWData mempty mempty mempty mempty mempty mempty mempty
|
||||
mappend (GWData a1 a2 a3 a4 a5 a6 a7)
|
||||
(GWData b1 b2 b3 b4 b5 b6 b7) = GWData
|
||||
(a1 `mappend` b1)
|
||||
(a2 `mappend` b2)
|
||||
(a3 `mappend` b3)
|
||||
(a4 `mappend` b4)
|
||||
(Map.unionWith mappend a5 b5)
|
||||
(a6 `mappend` b6)
|
||||
(a7 `mappend` b7)
|
||||
575
yesod-core/Yesod/Internal/Core.hs
Normal file
575
yesod-core/Yesod/Internal/Core.hs
Normal file
@ -0,0 +1,575 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
-- | The basic typeclass for a Yesod application.
|
||||
module Yesod.Internal.Core
|
||||
( -- * Type classes
|
||||
Yesod (..)
|
||||
, YesodDispatch (..)
|
||||
, RenderRoute (..)
|
||||
-- ** Breadcrumbs
|
||||
, YesodBreadcrumbs (..)
|
||||
, breadcrumbs
|
||||
-- * Utitlities
|
||||
, maybeAuthorized
|
||||
, widgetToPageContent
|
||||
-- * Defaults
|
||||
, defaultErrorHandler
|
||||
-- * Data types
|
||||
, AuthResult (..)
|
||||
-- * Logging
|
||||
, LogLevel (..)
|
||||
, formatLogMessage
|
||||
, messageLoggerHandler
|
||||
-- * Misc
|
||||
, yesodVersion
|
||||
, yesodRender
|
||||
) where
|
||||
|
||||
import Yesod.Content
|
||||
import Yesod.Handler
|
||||
|
||||
import Control.Arrow ((***))
|
||||
import Control.Monad (forM)
|
||||
import qualified Paths_yesod_core
|
||||
import Data.Version (showVersion)
|
||||
import Yesod.Widget
|
||||
import Yesod.Request
|
||||
import qualified Network.Wai as W
|
||||
import Yesod.Internal
|
||||
import Yesod.Internal.Session
|
||||
import Yesod.Internal.Request
|
||||
import Web.ClientSession (getKey, defaultKeyFile)
|
||||
import qualified Web.ClientSession as CS
|
||||
import qualified Data.ByteString.Char8 as S8
|
||||
import qualified Data.ByteString.Lazy as L
|
||||
import Data.Monoid
|
||||
import Control.Monad.Trans.RWS
|
||||
import Text.Hamlet
|
||||
import Text.Cassius
|
||||
import Text.Julius
|
||||
import Text.Blaze ((!), customAttribute, textTag, toValue)
|
||||
import qualified Text.Blaze.Html5 as TBH
|
||||
import Data.Text.Lazy.Builder (toLazyText)
|
||||
import Data.Text.Lazy.Encoding (encodeUtf8)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Control.Monad.IO.Class (MonadIO (liftIO))
|
||||
import Web.Cookie (parseCookies)
|
||||
import qualified Data.Map as Map
|
||||
import Data.Time
|
||||
import Network.HTTP.Types (encodePath)
|
||||
import qualified Data.Text as TS
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Data.Text.Encoding.Error as TEE
|
||||
import Blaze.ByteString.Builder (Builder, toByteString)
|
||||
import Blaze.ByteString.Builder.Char.Utf8 (fromText)
|
||||
import Data.List (foldl')
|
||||
import qualified Network.HTTP.Types as H
|
||||
import qualified Data.Text.Lazy as TL
|
||||
import qualified Data.Text.Lazy.IO
|
||||
import qualified System.IO
|
||||
import qualified Data.Text.Lazy.Builder as TB
|
||||
import Language.Haskell.TH.Syntax (Loc (..), Lift (..))
|
||||
import Text.Blaze (preEscapedLazyText)
|
||||
|
||||
#if GHC7
|
||||
#define HAMLET hamlet
|
||||
#else
|
||||
#define HAMLET $hamlet
|
||||
#endif
|
||||
|
||||
class Eq u => RenderRoute u where
|
||||
renderRoute :: u -> ([Text], [(Text, Text)])
|
||||
|
||||
-- | This class is automatically instantiated when you use the template haskell
|
||||
-- mkYesod function. You should never need to deal with it directly.
|
||||
class YesodDispatch a master where
|
||||
yesodDispatch
|
||||
:: Yesod master
|
||||
=> a
|
||||
-> Maybe CS.Key
|
||||
-> [Text]
|
||||
-> master
|
||||
-> (Route a -> Route master)
|
||||
-> Maybe W.Application
|
||||
|
||||
yesodRunner :: Yesod master
|
||||
=> a
|
||||
-> master
|
||||
-> (Route a -> Route master)
|
||||
-> Maybe CS.Key -> Maybe (Route a) -> GHandler a master ChooseRep -> W.Application
|
||||
yesodRunner = defaultYesodRunner
|
||||
|
||||
-- | Define settings for a Yesod applications. The only required setting is
|
||||
-- 'approot'; other than that, there are intelligent defaults.
|
||||
class RenderRoute (Route a) => Yesod a where
|
||||
-- | An absolute URL to the root of the application. Do not include
|
||||
-- trailing slash.
|
||||
--
|
||||
-- If you want to be lazy, you can supply an empty string under the
|
||||
-- following conditions:
|
||||
--
|
||||
-- * Your application is served from the root of the domain.
|
||||
--
|
||||
-- * You do not use any features that require absolute URLs, such as Atom
|
||||
-- feeds and XML sitemaps.
|
||||
approot :: a -> Text
|
||||
|
||||
-- | The encryption key to be used for encrypting client sessions.
|
||||
-- Returning 'Nothing' disables sessions.
|
||||
encryptKey :: a -> IO (Maybe CS.Key)
|
||||
encryptKey _ = fmap Just $ getKey defaultKeyFile
|
||||
|
||||
-- | Number of minutes before a client session times out. Defaults to
|
||||
-- 120 (2 hours).
|
||||
clientSessionDuration :: a -> Int
|
||||
clientSessionDuration = const 120
|
||||
|
||||
-- | Output error response pages.
|
||||
errorHandler :: ErrorResponse -> GHandler sub a ChooseRep
|
||||
errorHandler = defaultErrorHandler
|
||||
|
||||
-- | Applies some form of layout to the contents of a page.
|
||||
defaultLayout :: GWidget sub a () -> GHandler sub a RepHtml
|
||||
defaultLayout w = do
|
||||
p <- widgetToPageContent w
|
||||
mmsg <- getMessage
|
||||
hamletToRepHtml [HAMLET|
|
||||
!!!
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>#{pageTitle p}
|
||||
^{pageHead p}
|
||||
<body>
|
||||
$maybe msg <- mmsg
|
||||
<p .message>#{msg}
|
||||
^{pageBody p}
|
||||
|]
|
||||
|
||||
-- | Override the rendering function for a particular URL. One use case for
|
||||
-- this is to offload static hosting to a different domain name to avoid
|
||||
-- sending cookies.
|
||||
urlRenderOverride :: a -> Route a -> Maybe Builder
|
||||
urlRenderOverride _ _ = Nothing
|
||||
|
||||
-- | Determine if a request is authorized or not.
|
||||
--
|
||||
-- Return 'Nothing' is the request is authorized, 'Just' a message if
|
||||
-- unauthorized. If authentication is required, you should use a redirect;
|
||||
-- the Auth helper provides this functionality automatically.
|
||||
isAuthorized :: Route a
|
||||
-> Bool -- ^ is this a write request?
|
||||
-> GHandler s a AuthResult
|
||||
isAuthorized _ _ = return Authorized
|
||||
|
||||
-- | Determines whether the current request is a write request. By default,
|
||||
-- this assumes you are following RESTful principles, and determines this
|
||||
-- from request method. In particular, all except the following request
|
||||
-- methods are considered write: GET HEAD OPTIONS TRACE.
|
||||
--
|
||||
-- This function is used to determine if a request is authorized; see
|
||||
-- 'isAuthorized'.
|
||||
isWriteRequest :: Route a -> GHandler s a Bool
|
||||
isWriteRequest _ = do
|
||||
wai <- waiRequest
|
||||
return $ W.requestMethod wai `notElem`
|
||||
["GET", "HEAD", "OPTIONS", "TRACE"]
|
||||
|
||||
-- | The default route for authentication.
|
||||
--
|
||||
-- Used in particular by 'isAuthorized', but library users can do whatever
|
||||
-- they want with it.
|
||||
authRoute :: a -> Maybe (Route a)
|
||||
authRoute _ = Nothing
|
||||
|
||||
-- | A function used to clean up path segments. It returns 'Right' with a
|
||||
-- clean path or 'Left' with a new set of pieces the user should be
|
||||
-- redirected to. The default implementation enforces:
|
||||
--
|
||||
-- * No double slashes
|
||||
--
|
||||
-- * There is no trailing slash.
|
||||
--
|
||||
-- Note that versions of Yesod prior to 0.7 used a different set of rules
|
||||
-- involing trailing slashes.
|
||||
cleanPath :: a -> [Text] -> Either [Text] [Text]
|
||||
cleanPath _ s =
|
||||
if corrected == s
|
||||
then Right s
|
||||
else Left corrected
|
||||
where
|
||||
corrected = filter (not . TS.null) s
|
||||
|
||||
-- | Join the pieces of a path together into an absolute URL. This should
|
||||
-- be the inverse of 'splitPath'.
|
||||
joinPath :: a
|
||||
-> TS.Text -- ^ application root
|
||||
-> [TS.Text] -- ^ path pieces
|
||||
-> [(TS.Text, TS.Text)] -- ^ query string
|
||||
-> Builder
|
||||
joinPath _ ar pieces' qs' = fromText ar `mappend` encodePath pieces qs
|
||||
where
|
||||
pieces = if null pieces' then [""] else pieces'
|
||||
qs = map (TE.encodeUtf8 *** go) qs'
|
||||
go "" = Nothing
|
||||
go x = Just $ TE.encodeUtf8 x
|
||||
|
||||
-- | This function is used to store some static content to be served as an
|
||||
-- external file. The most common case of this is stashing CSS and
|
||||
-- JavaScript content in an external file; the "Yesod.Widget" module uses
|
||||
-- this feature.
|
||||
--
|
||||
-- The return value is 'Nothing' if no storing was performed; this is the
|
||||
-- default implementation. A 'Just' 'Left' gives the absolute URL of the
|
||||
-- file, whereas a 'Just' 'Right' gives the type-safe URL. The former is
|
||||
-- necessary when you are serving the content outside the context of a
|
||||
-- Yesod application, such as via memcached.
|
||||
addStaticContent :: Text -- ^ filename extension
|
||||
-> Text -- ^ mime-type
|
||||
-> L.ByteString -- ^ content
|
||||
-> GHandler sub a (Maybe (Either Text (Route a, [(Text, Text)])))
|
||||
addStaticContent _ _ _ = return Nothing
|
||||
|
||||
-- | Whether or not to tie a session to a specific IP address. Defaults to
|
||||
-- 'True'.
|
||||
sessionIpAddress :: a -> Bool
|
||||
sessionIpAddress _ = True
|
||||
|
||||
-- | The path value to set for cookies. By default, uses \"\/\", meaning
|
||||
-- cookies will be sent to every page on the current domain.
|
||||
cookiePath :: a -> S8.ByteString
|
||||
cookiePath _ = "/"
|
||||
|
||||
-- | Maximum allowed length of the request body, in bytes.
|
||||
maximumContentLength :: a -> Maybe (Route a) -> Int
|
||||
maximumContentLength _ _ = 2 * 1024 * 1024 -- 2 megabytes
|
||||
|
||||
-- | Send a message to the log. By default, prints to stderr.
|
||||
messageLogger :: a
|
||||
-> Loc -- ^ position in source code
|
||||
-> LogLevel
|
||||
-> Text -- ^ message
|
||||
-> IO ()
|
||||
messageLogger _ loc level msg =
|
||||
formatLogMessage loc level msg >>=
|
||||
Data.Text.Lazy.IO.hPutStrLn System.IO.stderr
|
||||
|
||||
messageLoggerHandler :: (Yesod m, MonadIO mo)
|
||||
=> Loc -> LogLevel -> Text -> GGHandler s m mo ()
|
||||
messageLoggerHandler loc level msg = do
|
||||
y <- getYesod
|
||||
liftIO $ messageLogger y loc level msg
|
||||
|
||||
data LogLevel = LevelDebug | LevelInfo | LevelWarn | LevelError | LevelOther Text
|
||||
deriving (Eq, Show, Read, Ord)
|
||||
|
||||
instance Lift LogLevel where
|
||||
lift LevelDebug = [|LevelDebug|]
|
||||
lift LevelInfo = [|LevelInfo|]
|
||||
lift LevelWarn = [|LevelWarn|]
|
||||
lift LevelError = [|LevelError|]
|
||||
lift (LevelOther x) = [|LevelOther $ TS.pack $(lift $ TS.unpack x)|]
|
||||
|
||||
formatLogMessage :: Loc
|
||||
-> LogLevel
|
||||
-> Text -- ^ message
|
||||
-> IO TL.Text
|
||||
formatLogMessage loc level msg = do
|
||||
now <- getCurrentTime
|
||||
return $ TB.toLazyText $
|
||||
TB.fromText (TS.pack $ show now)
|
||||
`mappend` TB.fromText ": "
|
||||
`mappend` TB.fromText (TS.pack $ show level)
|
||||
`mappend` TB.fromText "@("
|
||||
`mappend` TB.fromText (TS.pack $ loc_filename loc)
|
||||
`mappend` TB.fromText ":"
|
||||
`mappend` TB.fromText (TS.pack $ show $ fst $ loc_start loc)
|
||||
`mappend` TB.fromText ") "
|
||||
`mappend` TB.fromText msg
|
||||
|
||||
defaultYesodRunner :: Yesod master
|
||||
=> a
|
||||
-> master
|
||||
-> (Route a -> Route master)
|
||||
-> Maybe CS.Key
|
||||
-> Maybe (Route a)
|
||||
-> GHandler a master ChooseRep
|
||||
-> W.Application
|
||||
defaultYesodRunner _ m toMaster _ murl _ req
|
||||
| maximumContentLength m (fmap toMaster murl) < len =
|
||||
return $ W.responseLBS
|
||||
(H.Status 413 "Too Large")
|
||||
[("Content-Type", "text/plain")]
|
||||
"Request body too large to be processed."
|
||||
where
|
||||
len = fromMaybe 0 $ lookup "content-length" (W.requestHeaders req) >>= readMay
|
||||
readMay s =
|
||||
case reads $ S8.unpack s of
|
||||
[] -> Nothing
|
||||
(x, _):_ -> Just x
|
||||
defaultYesodRunner s master toMasterRoute mkey murl handler req = do
|
||||
now <- liftIO getCurrentTime
|
||||
let getExpires m = fromIntegral (m * 60) `addUTCTime` now
|
||||
let exp' = getExpires $ clientSessionDuration master
|
||||
let rh = takeWhile (/= ':') $ show $ W.remoteHost req
|
||||
let host = if sessionIpAddress master then S8.pack rh else ""
|
||||
let session' =
|
||||
case mkey of
|
||||
Nothing -> []
|
||||
Just key -> fromMaybe [] $ do
|
||||
raw <- lookup "Cookie" $ W.requestHeaders req
|
||||
val <- lookup sessionName $ parseCookies raw
|
||||
decodeSession key now host val
|
||||
rr <- liftIO $ parseWaiRequest req session' mkey
|
||||
let h = do
|
||||
case murl of
|
||||
Nothing -> handler
|
||||
Just url -> do
|
||||
isWrite <- isWriteRequest $ toMasterRoute url
|
||||
ar <- isAuthorized (toMasterRoute url) isWrite
|
||||
case ar of
|
||||
Authorized -> return ()
|
||||
AuthenticationRequired ->
|
||||
case authRoute master of
|
||||
Nothing ->
|
||||
permissionDenied "Authentication required"
|
||||
Just url' -> do
|
||||
setUltDest'
|
||||
redirect RedirectTemporary url'
|
||||
Unauthorized s' -> permissionDenied s'
|
||||
handler
|
||||
let sessionMap = Map.fromList
|
||||
$ filter (\(x, _) -> x /= nonceKey) session'
|
||||
yar <- handlerToYAR master s toMasterRoute (yesodRender master) errorHandler rr murl sessionMap h
|
||||
let mnonce = reqNonce rr
|
||||
return $ yarToResponse (hr mnonce getExpires host exp') yar
|
||||
where
|
||||
hr mnonce getExpires host exp' hs ct sm =
|
||||
hs'''
|
||||
where
|
||||
sessionVal =
|
||||
case (mkey, mnonce) of
|
||||
(Just key, Just nonce)
|
||||
-> encodeSession key exp' host
|
||||
$ Map.toList
|
||||
$ Map.insert nonceKey nonce sm
|
||||
_ -> mempty
|
||||
hs' =
|
||||
case mkey of
|
||||
Nothing -> hs
|
||||
Just _ -> AddCookie
|
||||
(clientSessionDuration master)
|
||||
sessionName
|
||||
sessionVal
|
||||
: hs
|
||||
hs'' = map (headerToPair (cookiePath master) getExpires) hs'
|
||||
hs''' = ("Content-Type", ct) : hs''
|
||||
|
||||
data AuthResult = Authorized | AuthenticationRequired | Unauthorized Text
|
||||
deriving (Eq, Show, Read)
|
||||
|
||||
-- | A type-safe, concise method of creating breadcrumbs for pages. For each
|
||||
-- resource, you declare the title of the page and the parent resource (if
|
||||
-- present).
|
||||
class YesodBreadcrumbs y where
|
||||
-- | Returns the title and the parent resource, if available. If you return
|
||||
-- a 'Nothing', then this is considered a top-level page.
|
||||
breadcrumb :: Route y -> GHandler sub y (Text , Maybe (Route y))
|
||||
|
||||
-- | Gets the title of the current page and the hierarchy of parent pages,
|
||||
-- along with their respective titles.
|
||||
breadcrumbs :: YesodBreadcrumbs y => GHandler sub y (Text, [(Route y, Text)])
|
||||
breadcrumbs = do
|
||||
x' <- getCurrentRoute
|
||||
tm <- getRouteToMaster
|
||||
let x = fmap tm x'
|
||||
case x of
|
||||
Nothing -> return ("Not found", [])
|
||||
Just y -> do
|
||||
(title, next) <- breadcrumb y
|
||||
z <- go [] next
|
||||
return (title, z)
|
||||
where
|
||||
go back Nothing = return back
|
||||
go back (Just this) = do
|
||||
(title, next) <- breadcrumb this
|
||||
go ((this, title) : back) next
|
||||
|
||||
applyLayout' :: Yesod master
|
||||
=> Html -- ^ title
|
||||
-> Hamlet (Route master) -- ^ body
|
||||
-> GHandler sub master ChooseRep
|
||||
applyLayout' title body = fmap chooseRep $ defaultLayout $ do
|
||||
setTitle title
|
||||
addHamlet body
|
||||
|
||||
-- | The default error handler for 'errorHandler'.
|
||||
defaultErrorHandler :: Yesod y => ErrorResponse -> GHandler sub y ChooseRep
|
||||
defaultErrorHandler NotFound = do
|
||||
r <- waiRequest
|
||||
let path' = TE.decodeUtf8With TEE.lenientDecode $ W.rawPathInfo r
|
||||
applyLayout' "Not Found"
|
||||
#if GHC7
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<h1>Not Found
|
||||
<p>#{path'}
|
||||
|]
|
||||
defaultErrorHandler (PermissionDenied msg) =
|
||||
applyLayout' "Permission Denied"
|
||||
#if GHC7
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<h1>Permission denied
|
||||
<p>#{msg}
|
||||
|]
|
||||
defaultErrorHandler (InvalidArgs ia) =
|
||||
applyLayout' "Invalid Arguments"
|
||||
#if GHC7
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<h1>Invalid Arguments
|
||||
<ul>
|
||||
$forall msg <- ia
|
||||
<li>#{msg}
|
||||
|]
|
||||
defaultErrorHandler (InternalError e) =
|
||||
applyLayout' "Internal Server Error"
|
||||
#if GHC7
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<h1>Internal Server Error
|
||||
<p>#{e}
|
||||
|]
|
||||
defaultErrorHandler (BadMethod m) =
|
||||
applyLayout' "Bad Method"
|
||||
#if GHC7
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
<h1>Method Not Supported
|
||||
<p>Method "#{S8.unpack m}" not supported
|
||||
|]
|
||||
|
||||
-- | Return the same URL if the user is authorized to see it.
|
||||
--
|
||||
-- Built on top of 'isAuthorized'. This is useful for building page that only
|
||||
-- contain links to pages the user is allowed to see.
|
||||
maybeAuthorized :: Yesod a
|
||||
=> Route a
|
||||
-> Bool -- ^ is this a write request?
|
||||
-> GHandler s a (Maybe (Route a))
|
||||
maybeAuthorized r isWrite = do
|
||||
x <- isAuthorized r isWrite
|
||||
return $ if x == Authorized then Just r else Nothing
|
||||
|
||||
-- | Convert a widget to a 'PageContent'.
|
||||
widgetToPageContent :: (Eq (Route master), Yesod master)
|
||||
=> GWidget sub master ()
|
||||
-> GHandler sub master (PageContent (Route master))
|
||||
widgetToPageContent (GWidget w) = do
|
||||
((), _, GWData (Body body) (Last mTitle) scripts' stylesheets' style jscript (Head head')) <- runRWST w () 0
|
||||
let title = maybe mempty unTitle mTitle
|
||||
let scripts = runUniqueList scripts'
|
||||
let stylesheets = runUniqueList stylesheets'
|
||||
let jsToHtml (Javascript b) = preEscapedLazyText $ toLazyText b
|
||||
jelper :: Julius url -> Hamlet url
|
||||
jelper = fmap jsToHtml
|
||||
|
||||
render <- getUrlRenderParams
|
||||
let renderLoc x =
|
||||
case x of
|
||||
Nothing -> Nothing
|
||||
Just (Left s) -> Just s
|
||||
Just (Right (u, p)) -> Just $ render u p
|
||||
css <- forM (Map.toList style) $ \(mmedia, content) -> do
|
||||
let rendered = renderCassius render content
|
||||
x <- addStaticContent "css" "text/css; charset=utf-8"
|
||||
$ encodeUtf8 rendered
|
||||
return (mmedia,
|
||||
case x of
|
||||
Nothing -> Left $ preEscapedLazyText rendered
|
||||
Just y -> Right $ either id (uncurry render) y)
|
||||
jsLoc <-
|
||||
case jscript of
|
||||
Nothing -> return Nothing
|
||||
Just s -> do
|
||||
x <- addStaticContent "js" "text/javascript; charset=utf-8"
|
||||
$ encodeUtf8 $ renderJulius render s
|
||||
return $ renderLoc x
|
||||
|
||||
let addAttr x (y, z) = x ! customAttribute (textTag y) (toValue z)
|
||||
let renderLoc' render' (Local url) = render' url []
|
||||
renderLoc' _ (Remote s) = s
|
||||
let mkScriptTag (Script loc attrs) render' =
|
||||
foldl' addAttr TBH.script (("src", renderLoc' render' loc) : attrs) $ return ()
|
||||
let mkLinkTag (Stylesheet loc attrs) render' =
|
||||
foldl' addAttr TBH.link
|
||||
( ("rel", "stylesheet")
|
||||
: ("href", renderLoc' render' loc)
|
||||
: attrs
|
||||
)
|
||||
let left (Left x) = Just x
|
||||
left _ = Nothing
|
||||
right (Right x) = Just x
|
||||
right _ = Nothing
|
||||
let head'' =
|
||||
#if GHC7
|
||||
[hamlet|
|
||||
#else
|
||||
[$hamlet|
|
||||
#endif
|
||||
$forall s <- stylesheets
|
||||
^{mkLinkTag s}
|
||||
$forall s <- css
|
||||
$maybe t <- right $ snd s
|
||||
$maybe media <- fst s
|
||||
<link rel=stylesheet media=#{media} href=#{t}
|
||||
$nothing
|
||||
<link rel=stylesheet href=#{t}
|
||||
$maybe content <- left $ snd s
|
||||
$maybe media <- fst s
|
||||
<style media=#{media}>#{content}
|
||||
$nothing
|
||||
<style>#{content}
|
||||
$forall s <- scripts
|
||||
^{mkScriptTag s}
|
||||
$maybe j <- jscript
|
||||
$maybe s <- jsLoc
|
||||
<script src="#{s}">
|
||||
$nothing
|
||||
<script>^{jelper j}
|
||||
\^{head'}
|
||||
|]
|
||||
return $ PageContent title head'' body
|
||||
|
||||
yesodVersion :: String
|
||||
yesodVersion = showVersion Paths_yesod_core.version
|
||||
|
||||
yesodRender :: Yesod y
|
||||
=> y
|
||||
-> Route y
|
||||
-> [(Text, Text)]
|
||||
-> Text
|
||||
yesodRender y u qs =
|
||||
TE.decodeUtf8 $ toByteString $
|
||||
fromMaybe
|
||||
(joinPath y (approot y) ps
|
||||
$ qs ++ qs')
|
||||
(urlRenderOverride y u)
|
||||
where
|
||||
(ps, qs') = renderRoute u
|
||||
322
yesod-core/Yesod/Internal/Dispatch.hs
Normal file
322
yesod-core/Yesod/Internal/Dispatch.hs
Normal file
@ -0,0 +1,322 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
-- | A bunch of Template Haskell used in the Yesod.Dispatch module.
|
||||
module Yesod.Internal.Dispatch
|
||||
( mkYesodDispatch'
|
||||
) where
|
||||
|
||||
import Prelude hiding (exp)
|
||||
import Language.Haskell.TH.Syntax
|
||||
import Web.PathPieces
|
||||
import Yesod.Internal.RouteParsing
|
||||
import Control.Monad (foldM)
|
||||
import Yesod.Handler (badMethod)
|
||||
import Yesod.Content (chooseRep)
|
||||
import qualified Network.Wai as W
|
||||
import Yesod.Internal.Core (yesodRunner, yesodDispatch)
|
||||
import Data.List (foldl')
|
||||
import Data.Char (toLower)
|
||||
import qualified Data.ByteString as S
|
||||
import Yesod.Internal.Core (Yesod (joinPath, approot, cleanPath))
|
||||
import Network.HTTP.Types (status301)
|
||||
import Data.Text (Text)
|
||||
import Data.Monoid (mappend)
|
||||
import qualified Blaze.ByteString.Builder
|
||||
import qualified Data.ByteString.Char8 as S8
|
||||
import qualified Data.Text
|
||||
|
||||
{-|
|
||||
|
||||
Alright, let's explain how routing works. We want to take a [String] and found
|
||||
out which route it applies to. For static pieces, we need to ensure an exact
|
||||
match against the segment. For a single or multi piece, we need to check the
|
||||
result of fromSinglePiece/fromMultiPiece, respectively.
|
||||
|
||||
We want to create a tree of case statements basically resembling:
|
||||
|
||||
case testRoute1 of
|
||||
Just app -> Just app
|
||||
Nothing ->
|
||||
case testRoute2 of
|
||||
Just app -> Just app
|
||||
Nothing ->
|
||||
case testRoute3 of
|
||||
Just app -> Just app
|
||||
Nothing -> Nothing
|
||||
|
||||
Each testRoute* will look something like this (example of parsing a route /name/#String/age/#Int):
|
||||
|
||||
case segments of
|
||||
"name" : as ->
|
||||
case as of
|
||||
[] -> Nothing
|
||||
b:bs ->
|
||||
case fromSinglePiece b of
|
||||
Left _ -> Nothing
|
||||
Right name ->
|
||||
case bs of
|
||||
"age":cs ->
|
||||
case cs of
|
||||
[] -> Nothing
|
||||
d:ds ->
|
||||
case fromSinglePiece d of
|
||||
Left _ -> Nothing
|
||||
Right age ->
|
||||
case ds of
|
||||
[] -> Just $ yesodRunner (PersonR name age) (getPersonR name age)...
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
_ -> Nothing
|
||||
|
||||
Obviously we would never want to write code by hand like this, but generating it is not too bad.
|
||||
|
||||
This function generates a clause for the yesodDispatch function based on a set of routes.
|
||||
|
||||
NOTE: We deal with subsites first; if none of those match, we try to apply
|
||||
cleanPath. If that indicates a redirect, we perform it. Otherwise, we match
|
||||
local routes.
|
||||
|
||||
-}
|
||||
|
||||
sendRedirect :: Yesod master => master -> [Text] -> W.Application
|
||||
sendRedirect y segments' env =
|
||||
return $ W.responseLBS status301
|
||||
[ ("Content-Type", "text/plain")
|
||||
, ("Location", Blaze.ByteString.Builder.toByteString dest')
|
||||
] "Redirecting"
|
||||
where
|
||||
dest = joinPath y (approot y) segments' []
|
||||
dest' =
|
||||
if S.null (W.rawQueryString env)
|
||||
then dest
|
||||
else (dest `mappend`
|
||||
Blaze.ByteString.Builder.fromByteString (W.rawQueryString env))
|
||||
|
||||
mkYesodDispatch' :: [((String, Pieces), Maybe String)]
|
||||
-> [((String, Pieces), Maybe String)]
|
||||
-> Q Clause
|
||||
mkYesodDispatch' resSub resLoc = do
|
||||
sub <- newName "sub"
|
||||
master <- newName "master"
|
||||
mkey <- newName "mkey"
|
||||
segments <- newName "segments"
|
||||
segments' <- newName "segmentsClean"
|
||||
toMasterRoute <- newName "toMasterRoute"
|
||||
nothing <- [|Nothing|]
|
||||
bodyLoc <- foldM (go master (VarE sub) (VarE toMasterRoute) mkey segments') nothing resLoc
|
||||
cp <- [|cleanPath|]
|
||||
sr <- [|sendRedirect|]
|
||||
just <- [|Just|]
|
||||
let bodyLoc' =
|
||||
CaseE (cp `AppE` VarE master `AppE` VarE segments)
|
||||
[ Match (ConP (mkName "Left") [VarP segments'])
|
||||
(NormalB $ just `AppE`
|
||||
(sr `AppE` VarE master `AppE` VarE segments'))
|
||||
[]
|
||||
, Match (ConP (mkName "Right") [VarP segments'])
|
||||
(NormalB bodyLoc)
|
||||
[]
|
||||
]
|
||||
body <- foldM (go master (VarE sub) (VarE toMasterRoute) mkey segments) bodyLoc' resSub
|
||||
return $ Clause
|
||||
[VarP sub, VarP mkey, VarP segments, VarP master, VarP toMasterRoute]
|
||||
(NormalB body)
|
||||
[]
|
||||
where
|
||||
go master sub toMasterRoute mkey segments onFail ((constr, SubSite { ssPieces = pieces }), Just toSub) = do
|
||||
test <- mkSubsiteExp segments pieces id (master, sub, toMasterRoute, mkey, constr, VarE $ mkName toSub)
|
||||
app <- newName "app"
|
||||
return $ CaseE test
|
||||
[ Match (ConP (mkName "Nothing") []) (NormalB onFail) []
|
||||
, Match (ConP (mkName "Just") [VarP app]) (NormalB $ VarE app) []
|
||||
]
|
||||
go master sub toMasterRoute mkey segments onFail ((constr, Simple pieces methods), Nothing) = do
|
||||
test <- mkSimpleExp (VarE segments) pieces id (master, sub, toMasterRoute, mkey, constr, methods)
|
||||
just <- [|Just|]
|
||||
app <- newName "app"
|
||||
return $ CaseE test
|
||||
[ Match (ConP (mkName "Nothing") []) (NormalB onFail) []
|
||||
, Match (ConP (mkName "Just") [VarP app]) (NormalB $ just `AppE` VarE app) []
|
||||
]
|
||||
go _ _ _ _ _ _ _ = error "Invalid combination"
|
||||
|
||||
mkSimpleExp :: Exp -- ^ segments
|
||||
-> [Piece]
|
||||
-> ([Exp] -> [Exp]) -- ^ variables already parsed
|
||||
-> (Name, Exp, Exp, Name, String, [String]) -- ^ master, sub, toMasterRoute, mkey, constructor, methods
|
||||
-> Q Exp
|
||||
mkSimpleExp segments [] frontVars (master, sub, toMasterRoute, mkey, constr, methods) = do
|
||||
just <- [|Just|]
|
||||
nothing <- [|Nothing|]
|
||||
onSuccess <- newName "onSuccess"
|
||||
req <- newName "req"
|
||||
badMethod' <- [|badMethod|]
|
||||
rm <- [|S8.unpack . W.requestMethod|]
|
||||
let caseExp = rm `AppE` VarE req
|
||||
yr <- [|yesodRunner|]
|
||||
cr <- [|fmap chooseRep|]
|
||||
eq <- [|(==)|]
|
||||
let url = foldl' AppE (ConE $ mkName constr) $ frontVars []
|
||||
let runHandlerVars h = runHandler' $ cr `AppE` foldl' AppE (VarE $ mkName h) (frontVars [])
|
||||
runHandler' h = yr `AppE` sub
|
||||
`AppE` VarE master
|
||||
`AppE` toMasterRoute
|
||||
`AppE` VarE mkey
|
||||
`AppE` (just `AppE` url)
|
||||
`AppE` h
|
||||
`AppE` VarE req
|
||||
let match :: String -> Q Match
|
||||
match m = do
|
||||
x <- newName "x"
|
||||
return $ Match
|
||||
(VarP x)
|
||||
(GuardedB
|
||||
[ ( NormalG $ InfixE (Just $ VarE x) eq (Just $ LitE $ StringL m) -- FIXME need to pack, right?
|
||||
, runHandlerVars $ map toLower m ++ constr
|
||||
)
|
||||
])
|
||||
[]
|
||||
clauses <-
|
||||
case methods of
|
||||
[] -> return [Clause [VarP req] (NormalB $ runHandlerVars $ "handle" ++ constr) []]
|
||||
_ -> do
|
||||
matches <- mapM match methods
|
||||
return [Clause [VarP req] (NormalB $ CaseE caseExp $ matches ++
|
||||
[Match WildP (NormalB $ runHandler' badMethod') []]) []]
|
||||
let exp = CaseE segments
|
||||
[ Match
|
||||
(ConP (mkName "[]") [])
|
||||
(NormalB $ just `AppE` VarE onSuccess)
|
||||
[FunD onSuccess clauses]
|
||||
, Match
|
||||
WildP
|
||||
(NormalB nothing)
|
||||
[]
|
||||
]
|
||||
return exp
|
||||
mkSimpleExp segments (StaticPiece s:pieces) frontVars x = do
|
||||
srest <- newName "segments"
|
||||
innerExp <- mkSimpleExp (VarE srest) pieces frontVars x
|
||||
nothing <- [|Nothing|]
|
||||
y <- newName "y"
|
||||
pack <- [|Data.Text.pack|]
|
||||
eq <- [|(==)|]
|
||||
let exp = CaseE segments
|
||||
[ Match
|
||||
(InfixP (VarP y) (mkName ":") (VarP srest))
|
||||
(GuardedB
|
||||
[ ( NormalG $ InfixE (Just $ VarE y) eq (Just $ pack `AppE` (LitE $ StringL s))
|
||||
, innerExp
|
||||
)
|
||||
])
|
||||
[]
|
||||
, Match WildP (NormalB nothing) []
|
||||
]
|
||||
return exp
|
||||
mkSimpleExp segments (SinglePiece _:pieces) frontVars x = do
|
||||
srest <- newName "segments"
|
||||
next' <- newName "next'"
|
||||
innerExp <- mkSimpleExp (VarE srest) pieces (frontVars . (:) (VarE next')) x
|
||||
nothing <- [|Nothing|]
|
||||
next <- newName "next"
|
||||
fsp <- [|fromSinglePiece|]
|
||||
let exp' = CaseE (fsp `AppE` VarE next)
|
||||
[ Match
|
||||
(ConP (mkName "Nothing") [])
|
||||
(NormalB nothing)
|
||||
[]
|
||||
, Match
|
||||
(ConP (mkName "Just") [VarP next'])
|
||||
(NormalB innerExp)
|
||||
[]
|
||||
]
|
||||
let exp = CaseE segments
|
||||
[ Match
|
||||
(InfixP (VarP next) (mkName ":") (VarP srest))
|
||||
(NormalB exp')
|
||||
[]
|
||||
, Match WildP (NormalB nothing) []
|
||||
]
|
||||
return exp
|
||||
mkSimpleExp segments [MultiPiece _] frontVars x = do
|
||||
next' <- newName "next'"
|
||||
srest <- [|[]|]
|
||||
innerExp <- mkSimpleExp srest [] (frontVars . (:) (VarE next')) x
|
||||
nothing <- [|Nothing|]
|
||||
fmp <- [|fromMultiPiece|]
|
||||
let exp = CaseE (fmp `AppE` segments)
|
||||
[ Match
|
||||
(ConP (mkName "Nothing") [])
|
||||
(NormalB nothing)
|
||||
[]
|
||||
, Match
|
||||
(ConP (mkName "Just") [VarP next'])
|
||||
(NormalB innerExp)
|
||||
[]
|
||||
]
|
||||
return exp
|
||||
mkSimpleExp _ (MultiPiece _:_) _ _ = error "MultiPiece must be last piece"
|
||||
|
||||
mkSubsiteExp :: Name -- ^ segments
|
||||
-> [Piece]
|
||||
-> ([Exp] -> [Exp]) -- ^ variables already parsed
|
||||
-> (Name, Exp, Exp, Name, String, Exp) -- ^ master, sub, toMasterRoute, mkey, constructor, toSub
|
||||
-> Q Exp
|
||||
mkSubsiteExp segments [] frontVars (master, sub, toMasterRoute, mkey, constr, toSub) = do
|
||||
yd <- [|yesodDispatch|]
|
||||
dot <- [|(.)|]
|
||||
let con = InfixE (Just toMasterRoute) dot $ Just $ foldl' AppE (ConE $ mkName constr) $ frontVars []
|
||||
-- proper handling for sub-subsites
|
||||
let sub' = foldl' AppE (toSub `AppE` sub) $ frontVars []
|
||||
let app = yd `AppE` sub'
|
||||
`AppE` VarE mkey
|
||||
`AppE` VarE segments
|
||||
`AppE` VarE master
|
||||
`AppE` con
|
||||
just <- [|Just|]
|
||||
return $ just `AppE` app
|
||||
mkSubsiteExp _ (MultiPiece _:_) _ _ = error "Subsites cannot have MultiPiece"
|
||||
mkSubsiteExp segments (StaticPiece s:pieces) frontVars x = do
|
||||
srest <- newName "segments"
|
||||
innerExp <- mkSubsiteExp srest pieces frontVars x
|
||||
nothing <- [|Nothing|]
|
||||
y <- newName "y"
|
||||
pack <- [|Data.Text.pack|]
|
||||
eq <- [|(==)|]
|
||||
let exp = CaseE (VarE segments)
|
||||
[ Match
|
||||
(InfixP (VarP y) (mkName ":") (VarP srest))
|
||||
(GuardedB
|
||||
[ ( NormalG $ InfixE (Just $ VarE y) eq (Just $ pack `AppE` (LitE $ StringL s))
|
||||
, innerExp
|
||||
)
|
||||
])
|
||||
[]
|
||||
, Match WildP (NormalB nothing) []
|
||||
]
|
||||
return exp
|
||||
mkSubsiteExp segments (SinglePiece _:pieces) frontVars x = do
|
||||
srest <- newName "segments"
|
||||
next' <- newName "next'"
|
||||
innerExp <- mkSubsiteExp srest pieces (frontVars . (:) (VarE next')) x
|
||||
nothing <- [|Nothing|]
|
||||
next <- newName "next"
|
||||
fsp <- [|fromSinglePiece|]
|
||||
let exp' = CaseE (fsp `AppE` VarE next)
|
||||
[ Match
|
||||
(ConP (mkName "Nothing") [])
|
||||
(NormalB nothing)
|
||||
[]
|
||||
, Match
|
||||
(ConP (mkName "Just") [VarP next'])
|
||||
(NormalB innerExp)
|
||||
[]
|
||||
]
|
||||
let exp = CaseE (VarE segments)
|
||||
[ Match
|
||||
(InfixP (VarP next) (mkName ":") (VarP srest))
|
||||
(NormalB exp')
|
||||
[]
|
||||
, Match WildP (NormalB nothing) []
|
||||
]
|
||||
return exp
|
||||
86
yesod-core/Yesod/Internal/Request.hs
Normal file
86
yesod-core/Yesod/Internal/Request.hs
Normal file
@ -0,0 +1,86 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Internal.Request
|
||||
( parseWaiRequest
|
||||
, Request (..)
|
||||
, RequestBodyContents
|
||||
, FileInfo (..)
|
||||
) where
|
||||
|
||||
import Control.Arrow (first, second)
|
||||
import qualified Network.Wai.Parse as NWP
|
||||
import Yesod.Internal
|
||||
import qualified Network.Wai as W
|
||||
import System.Random (randomR, newStdGen)
|
||||
import Web.Cookie (parseCookiesText)
|
||||
import Data.Monoid (mempty)
|
||||
import qualified Data.ByteString.Char8 as S8
|
||||
import Data.Text (Text, pack)
|
||||
import Network.HTTP.Types (queryToQueryText)
|
||||
import Control.Monad (join)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.ByteString.Lazy as L
|
||||
|
||||
-- | The parsed request information.
|
||||
data Request = Request
|
||||
{ reqGetParams :: [(Text, Text)]
|
||||
, reqCookies :: [(Text, Text)]
|
||||
, reqWaiRequest :: W.Request
|
||||
-- | Languages which the client supports.
|
||||
, reqLangs :: [Text]
|
||||
-- | A random, session-specific nonce used to prevent CSRF attacks.
|
||||
, reqNonce :: Maybe Text
|
||||
}
|
||||
|
||||
parseWaiRequest :: W.Request
|
||||
-> [(Text, Text)] -- ^ session
|
||||
-> Maybe a
|
||||
-> IO Request
|
||||
parseWaiRequest env session' key' = do
|
||||
let gets' = queryToQueryText $ W.queryString env
|
||||
let reqCookie = fromMaybe mempty $ lookup "Cookie"
|
||||
$ W.requestHeaders env
|
||||
cookies' = parseCookiesText reqCookie
|
||||
acceptLang = lookup "Accept-Language" $ W.requestHeaders env
|
||||
langs = map (pack . S8.unpack) $ maybe [] NWP.parseHttpAccept acceptLang
|
||||
langs' = case lookup langKey session' of
|
||||
Nothing -> langs
|
||||
Just x -> x : langs
|
||||
langs'' = case lookup langKey cookies' of
|
||||
Nothing -> langs'
|
||||
Just x -> x : langs'
|
||||
langs''' = case join $ lookup langKey gets' of
|
||||
Nothing -> langs''
|
||||
Just x -> x : langs''
|
||||
nonce <- case (key', lookup nonceKey session') of
|
||||
(Nothing, _) -> return Nothing
|
||||
(_, Just x) -> return $ Just x
|
||||
(_, Nothing) -> do
|
||||
g <- newStdGen
|
||||
return $ Just $ pack $ fst $ randomString 10 g
|
||||
let gets'' = map (second $ fromMaybe "") gets'
|
||||
return $ Request gets'' cookies' env langs''' nonce
|
||||
where
|
||||
randomString len =
|
||||
first (map toChar) . sequence' (replicate len (randomR (0, 61)))
|
||||
sequence' [] g = ([], g)
|
||||
sequence' (f:fs) g =
|
||||
let (f', g') = f g
|
||||
(fs', g'') = sequence' fs g'
|
||||
in (f' : fs', g'')
|
||||
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.
|
||||
type RequestBodyContents =
|
||||
( [(Text, Text)]
|
||||
, [(Text, FileInfo)]
|
||||
)
|
||||
|
||||
data FileInfo = FileInfo
|
||||
{ fileName :: Text
|
||||
, fileContentType :: Text
|
||||
, fileContent :: L.ByteString
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
349
yesod-core/Yesod/Internal/RouteParsing.hs
Normal file
349
yesod-core/Yesod/Internal/RouteParsing.hs
Normal file
@ -0,0 +1,349 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
module Yesod.Internal.RouteParsing
|
||||
( createRoutes
|
||||
, createRender
|
||||
, createParse
|
||||
, createDispatch
|
||||
, Pieces (..)
|
||||
, THResource
|
||||
, parseRoutes
|
||||
, parseRoutesFile
|
||||
, parseRoutesNoCheck
|
||||
, parseRoutesFileNoCheck
|
||||
, Resource (..)
|
||||
, Piece (..)
|
||||
) where
|
||||
|
||||
import Web.PathPieces
|
||||
import Language.Haskell.TH.Syntax
|
||||
import Data.Maybe
|
||||
import Data.Either
|
||||
import Data.List
|
||||
import Data.Char (toLower)
|
||||
import qualified Data.Text
|
||||
import Language.Haskell.TH.Syntax
|
||||
import Language.Haskell.TH.Quote
|
||||
import Data.Data
|
||||
import Data.Maybe
|
||||
import qualified System.IO as SIO
|
||||
|
||||
data Pieces =
|
||||
SubSite
|
||||
{ ssType :: Type
|
||||
, ssParse :: Exp
|
||||
, ssRender :: Exp
|
||||
, ssDispatch :: Exp
|
||||
, ssToMasterArg :: Exp
|
||||
, ssPieces :: [Piece]
|
||||
}
|
||||
| Simple [Piece] [String] -- ^ methods
|
||||
deriving Show
|
||||
type THResource = (String, Pieces)
|
||||
|
||||
createRoutes :: [THResource] -> Q [Con]
|
||||
createRoutes res =
|
||||
return $ map go res
|
||||
where
|
||||
go (n, SubSite{ssType = s, ssPieces = pieces}) =
|
||||
NormalC (mkName n) $ mapMaybe go' pieces ++ [(NotStrict, s)]
|
||||
go (n, Simple pieces _) = NormalC (mkName n) $ mapMaybe go' pieces
|
||||
go' (SinglePiece x) = Just (NotStrict, ConT $ mkName x)
|
||||
go' (MultiPiece x) = Just (NotStrict, ConT $ mkName x)
|
||||
go' (StaticPiece _) = Nothing
|
||||
|
||||
-- | Generates the set of clauses necesary to parse the given 'Resource's. See 'quasiParse'.
|
||||
createParse :: [THResource] -> Q [Clause]
|
||||
createParse res = do
|
||||
final' <- final
|
||||
clauses <- mapM go res
|
||||
return $ if areResourcesComplete res
|
||||
then clauses
|
||||
else clauses ++ [final']
|
||||
where
|
||||
cons x y = ConP (mkName ":") [x, y]
|
||||
go (constr, SubSite{ssParse = p, ssPieces = ps}) = do
|
||||
ri <- [|Right|]
|
||||
be <- [|ape|]
|
||||
(pat', parse) <- mkPat' be ps $ ri `AppE` ConE (mkName constr)
|
||||
|
||||
x <- newName "x"
|
||||
let pat = init pat' ++ [VarP x]
|
||||
|
||||
--let pat = foldr (\a b -> cons [LitP (StringL a), b]) (VarP x) pieces
|
||||
let eitherSub = p `AppE` VarE x
|
||||
let bod = be `AppE` parse `AppE` eitherSub
|
||||
--let bod = fmape' `AppE` ConE (mkName constr) `AppE` eitherSub
|
||||
return $ Clause [foldr1 cons pat] (NormalB bod) []
|
||||
go (n, Simple ps _) = do
|
||||
ri <- [|Right|]
|
||||
be <- [|ape|]
|
||||
(pat, parse) <- mkPat' be ps $ ri `AppE` ConE (mkName n)
|
||||
return $ Clause [foldr1 cons pat] (NormalB parse) []
|
||||
final = do
|
||||
no <- [|Left "Invalid URL"|]
|
||||
return $ Clause [WildP] (NormalB no) []
|
||||
mkPat' :: Exp -> [Piece] -> Exp -> Q ([Pat], Exp)
|
||||
mkPat' be [MultiPiece s] parse = do
|
||||
v <- newName $ "var" ++ s
|
||||
fmp <- [|fromMultiPiece|]
|
||||
let parse' = InfixE (Just parse) be $ Just $ fmp `AppE` VarE v
|
||||
return ([VarP v], parse')
|
||||
mkPat' _ (MultiPiece _:_) _parse = error "MultiPiece must be last"
|
||||
mkPat' be (StaticPiece s:rest) parse = do
|
||||
(x, parse') <- mkPat' be rest parse
|
||||
let sp = LitP $ StringL s
|
||||
return (sp : x, parse')
|
||||
mkPat' be (SinglePiece s:rest) parse = do
|
||||
fsp <- [|fromSinglePiece|]
|
||||
v <- newName $ "var" ++ s
|
||||
let parse' = InfixE (Just parse) be $ Just $ fsp `AppE` VarE v
|
||||
(x, parse'') <- mkPat' be rest parse'
|
||||
return (VarP v : x, parse'')
|
||||
mkPat' _ [] parse = return ([ListP []], parse)
|
||||
|
||||
-- | 'ap' for 'Either'
|
||||
ape :: Either String (a -> b) -> Either String a -> Either String b
|
||||
ape (Left e) _ = Left e
|
||||
ape (Right _) (Left e) = Left e
|
||||
ape (Right f) (Right a) = Right $ f a
|
||||
|
||||
-- | Generates the set of clauses necesary to render the given 'Resource's. See
|
||||
-- 'quasiRender'.
|
||||
createRender :: [THResource] -> Q [Clause]
|
||||
createRender = mapM go
|
||||
where
|
||||
go (n, Simple ps _) = do
|
||||
let ps' = zip [1..] ps
|
||||
let pat = ConP (mkName n) $ mapMaybe go' ps'
|
||||
bod <- mkBod ps'
|
||||
return $ Clause [pat] (NormalB $ TupE [bod, ListE []]) []
|
||||
go (n, SubSite{ssRender = r, ssPieces = pieces}) = do
|
||||
cons' <- [|\a (b, c) -> (a ++ b, c)|]
|
||||
let cons a b = cons' `AppE` a `AppE` b
|
||||
x <- newName "x"
|
||||
let r' = r `AppE` VarE x
|
||||
let pieces' = zip [1..] pieces
|
||||
let pat = ConP (mkName n) $ mapMaybe go' pieces' ++ [VarP x]
|
||||
bod <- mkBod pieces'
|
||||
return $ Clause [pat] (NormalB $ cons bod r') []
|
||||
go' (_, StaticPiece _) = Nothing
|
||||
go' (i, _) = Just $ VarP $ mkName $ "var" ++ show (i :: Int)
|
||||
mkBod :: (Show t) => [(t, Piece)] -> Q Exp
|
||||
mkBod [] = lift ([] :: [String])
|
||||
mkBod ((_, StaticPiece x):xs) = do
|
||||
x' <- lift x
|
||||
pack <- [|Data.Text.pack|]
|
||||
xs' <- mkBod xs
|
||||
return $ ConE (mkName ":") `AppE` (pack `AppE` x') `AppE` xs'
|
||||
mkBod ((i, SinglePiece _):xs) = do
|
||||
let x' = VarE $ mkName $ "var" ++ show i
|
||||
tsp <- [|toSinglePiece|]
|
||||
let x'' = tsp `AppE` x'
|
||||
xs' <- mkBod xs
|
||||
return $ ConE (mkName ":") `AppE` x'' `AppE` xs'
|
||||
mkBod ((i, MultiPiece _):_) = do
|
||||
let x' = VarE $ mkName $ "var" ++ show i
|
||||
tmp <- [|toMultiPiece|]
|
||||
return $ tmp `AppE` x'
|
||||
|
||||
-- | Whether the set of resources cover all possible URLs.
|
||||
areResourcesComplete :: [THResource] -> Bool
|
||||
areResourcesComplete res =
|
||||
let (slurps, noSlurps) = partitionEithers $ mapMaybe go res
|
||||
in case slurps of
|
||||
[] -> False
|
||||
_ -> let minSlurp = minimum slurps
|
||||
in helper minSlurp $ reverse $ sort noSlurps
|
||||
where
|
||||
go :: THResource -> Maybe (Either Int Int)
|
||||
go (_, Simple ps _) =
|
||||
case reverse ps of
|
||||
[] -> Just $ Right 0
|
||||
(MultiPiece _:rest) -> go' Left rest
|
||||
x -> go' Right x
|
||||
go (n, SubSite{ssPieces = ps}) =
|
||||
go (n, Simple (ps ++ [MultiPiece ""]) [])
|
||||
go' b x = if all isSingle x then Just (b $ length x) else Nothing
|
||||
helper 0 _ = True
|
||||
helper _ [] = False
|
||||
helper m (i:is)
|
||||
| i >= m = helper m is
|
||||
| i + 1 == m = helper i is
|
||||
| otherwise = False
|
||||
isSingle (SinglePiece _) = True
|
||||
isSingle _ = False
|
||||
|
||||
notStatic :: Piece -> Bool
|
||||
notStatic StaticPiece{} = False
|
||||
notStatic _ = True
|
||||
|
||||
createDispatch :: Exp -- ^ modify a master handler
|
||||
-> Exp -- ^ convert a subsite handler to a master handler
|
||||
-> [THResource]
|
||||
-> Q [Clause]
|
||||
createDispatch modMaster toMaster = mapM go
|
||||
where
|
||||
go :: (String, Pieces) -> Q Clause
|
||||
go (n, Simple ps methods) = do
|
||||
meth <- newName "method"
|
||||
xs <- mapM newName $ replicate (length $ filter notStatic ps) "x"
|
||||
let pat = [ ConP (mkName n) $ map VarP xs
|
||||
, if null methods then WildP else VarP meth
|
||||
]
|
||||
bod <- go' n meth xs methods
|
||||
return $ Clause pat (NormalB bod) []
|
||||
go (n, SubSite{ssDispatch = d, ssToMasterArg = tma, ssPieces = ps}) = do
|
||||
meth <- newName "method"
|
||||
x <- newName "x"
|
||||
xs <- mapM newName $ replicate (length $ filter notStatic ps) "x"
|
||||
let pat = [ConP (mkName n) $ map VarP xs ++ [VarP x], VarP meth]
|
||||
let bod = d `AppE` VarE x `AppE` VarE meth
|
||||
fmap' <- [|fmap|]
|
||||
let routeToMaster = foldl AppE (ConE (mkName n)) $ map VarE xs
|
||||
tma' = foldl AppE tma $ map VarE xs
|
||||
let toMaster' = toMaster `AppE` routeToMaster `AppE` tma' `AppE` VarE x
|
||||
let bod' = InfixE (Just toMaster') fmap' (Just bod)
|
||||
let bod'' = InfixE (Just modMaster) fmap' (Just bod')
|
||||
return $ Clause pat (NormalB bod'') []
|
||||
go' n _ xs [] = do
|
||||
jus <- [|Just|]
|
||||
let bod = foldl AppE (VarE $ mkName $ "handle" ++ n) $ map VarE xs
|
||||
return $ jus `AppE` (modMaster `AppE` bod)
|
||||
go' n meth xs methods = do
|
||||
noth <- [|Nothing|]
|
||||
j <- [|Just|]
|
||||
let noMatch = Match WildP (NormalB noth) []
|
||||
return $ CaseE (VarE meth) $ map (go'' n xs j) methods ++ [noMatch]
|
||||
go'' n xs j method =
|
||||
let pat = LitP $ StringL method
|
||||
func = map toLower method ++ n
|
||||
bod = foldl AppE (VarE $ mkName func) $ map VarE xs
|
||||
in Match pat (NormalB $ j `AppE` (modMaster `AppE` bod)) []
|
||||
|
||||
-- | A quasi-quoter to parse a string into a list of 'Resource's. Checks for
|
||||
-- overlapping routes, failing if present; use 'parseRoutesNoCheck' to skip the
|
||||
-- checking. See documentation site for details on syntax.
|
||||
parseRoutes :: QuasiQuoter
|
||||
parseRoutes = QuasiQuoter
|
||||
{ quoteExp = x
|
||||
, quotePat = y
|
||||
}
|
||||
where
|
||||
x s = do
|
||||
let res = resourcesFromString s
|
||||
case findOverlaps res of
|
||||
[] -> lift res
|
||||
z -> error $ "Overlapping routes: " ++ unlines (map show z)
|
||||
y = dataToPatQ (const Nothing) . resourcesFromString
|
||||
|
||||
parseRoutesFile :: FilePath -> Q Exp
|
||||
parseRoutesFile fp = do
|
||||
s <- qRunIO $ readUtf8File fp
|
||||
quoteExp parseRoutes s
|
||||
|
||||
parseRoutesFileNoCheck :: FilePath -> Q Exp
|
||||
parseRoutesFileNoCheck fp = do
|
||||
s <- qRunIO $ readUtf8File fp
|
||||
quoteExp parseRoutesNoCheck s
|
||||
|
||||
readUtf8File :: FilePath -> IO String
|
||||
readUtf8File fp = do
|
||||
h <- SIO.openFile fp SIO.ReadMode
|
||||
SIO.hSetEncoding h SIO.utf8_bom
|
||||
SIO.hGetContents h
|
||||
|
||||
-- | Same as 'parseRoutes', but performs no overlap checking.
|
||||
parseRoutesNoCheck :: QuasiQuoter
|
||||
parseRoutesNoCheck = QuasiQuoter
|
||||
{ quoteExp = x
|
||||
, quotePat = y
|
||||
}
|
||||
where
|
||||
x = lift . resourcesFromString
|
||||
y = dataToPatQ (const Nothing) . resourcesFromString
|
||||
|
||||
instance Lift Resource where
|
||||
lift (Resource s ps h) = do
|
||||
r <- [|Resource|]
|
||||
s' <- lift s
|
||||
ps' <- lift ps
|
||||
h' <- lift h
|
||||
return $ r `AppE` s' `AppE` ps' `AppE` h'
|
||||
|
||||
-- | A single resource pattern.
|
||||
--
|
||||
-- First argument is the name of the constructor, second is the URL pattern to
|
||||
-- match, third is how to dispatch.
|
||||
data Resource = Resource String [Piece] [String]
|
||||
deriving (Read, Show, Eq, Data, Typeable)
|
||||
|
||||
-- | A single piece of a URL, delimited by slashes.
|
||||
--
|
||||
-- In the case of StaticPiece, the argument is the value of the piece; for the
|
||||
-- other constructors, it is the name of the parameter represented by this
|
||||
-- piece. That value is not used here, but may be useful elsewhere.
|
||||
data Piece = StaticPiece String
|
||||
| SinglePiece String
|
||||
| MultiPiece String
|
||||
deriving (Read, Show, Eq, Data, Typeable)
|
||||
|
||||
instance Lift Piece where
|
||||
lift (StaticPiece s) = do
|
||||
c <- [|StaticPiece|]
|
||||
s' <- lift s
|
||||
return $ c `AppE` s'
|
||||
lift (SinglePiece s) = do
|
||||
c <- [|SinglePiece|]
|
||||
s' <- lift s
|
||||
return $ c `AppE` s'
|
||||
lift (MultiPiece s) = do
|
||||
c <- [|MultiPiece|]
|
||||
s' <- lift s
|
||||
return $ c `AppE` s'
|
||||
|
||||
-- | Convert a multi-line string to a set of resources. See documentation for
|
||||
-- the format of this string. This is a partial function which calls 'error' on
|
||||
-- invalid input.
|
||||
resourcesFromString :: String -> [Resource]
|
||||
resourcesFromString =
|
||||
mapMaybe go . lines
|
||||
where
|
||||
go s =
|
||||
case takeWhile (/= "--") $ words s of
|
||||
(pattern:constr:rest) ->
|
||||
let pieces = piecesFromString $ drop1Slash pattern
|
||||
in Just $ Resource constr pieces rest
|
||||
[] -> Nothing
|
||||
_ -> error $ "Invalid resource line: " ++ s
|
||||
|
||||
drop1Slash :: String -> String
|
||||
drop1Slash ('/':x) = x
|
||||
drop1Slash x = x
|
||||
|
||||
piecesFromString :: String -> [Piece]
|
||||
piecesFromString "" = []
|
||||
piecesFromString x =
|
||||
let (y, z) = break (== '/') x
|
||||
in pieceFromString y : piecesFromString (drop1Slash z)
|
||||
|
||||
pieceFromString :: String -> Piece
|
||||
pieceFromString ('#':x) = SinglePiece x
|
||||
pieceFromString ('*':x) = MultiPiece x
|
||||
pieceFromString x = StaticPiece x
|
||||
|
||||
findOverlaps :: [Resource] -> [(Resource, Resource)]
|
||||
findOverlaps = gos . map justPieces
|
||||
where
|
||||
justPieces r@(Resource _ ps _) = (ps, r)
|
||||
gos [] = []
|
||||
gos (x:xs) = mapMaybe (go x) xs ++ gos xs
|
||||
go (StaticPiece x:xs, xr) (StaticPiece y:ys, yr)
|
||||
| x == y = go (xs, xr) (ys, yr)
|
||||
| otherwise = Nothing
|
||||
go (MultiPiece _:_, xr) (_, yr) = Just (xr, yr)
|
||||
go (_, xr) (MultiPiece _:_, yr) = Just (xr, yr)
|
||||
go ([], xr) ([], yr) = Just (xr, yr)
|
||||
go ([], _) (_, _) = Nothing
|
||||
go (_, _) ([], _) = Nothing
|
||||
go (_:xs, xr) (_:ys, yr) = go (xs, xr) (ys, yr)
|
||||
55
yesod-core/Yesod/Internal/Session.hs
Normal file
55
yesod-core/Yesod/Internal/Session.hs
Normal file
@ -0,0 +1,55 @@
|
||||
module Yesod.Internal.Session
|
||||
( encodeSession
|
||||
, decodeSession
|
||||
) where
|
||||
|
||||
import qualified Web.ClientSession as CS
|
||||
import Data.Serialize
|
||||
import Data.Time
|
||||
import Data.ByteString (ByteString)
|
||||
import Control.Monad (guard)
|
||||
import Data.Text (Text, pack, unpack)
|
||||
import Control.Arrow ((***))
|
||||
|
||||
encodeSession :: CS.Key
|
||||
-> UTCTime -- ^ expire time
|
||||
-> ByteString -- ^ remote host
|
||||
-> [(Text, Text)] -- ^ session
|
||||
-> ByteString -- ^ cookie value
|
||||
encodeSession key expire rhost session' =
|
||||
CS.encrypt key $ encode $ SessionCookie expire rhost session'
|
||||
|
||||
decodeSession :: CS.Key
|
||||
-> UTCTime -- ^ current time
|
||||
-> ByteString -- ^ remote host field
|
||||
-> ByteString -- ^ cookie value
|
||||
-> Maybe [(Text, Text)]
|
||||
decodeSession key now rhost encrypted = do
|
||||
decrypted <- CS.decrypt key encrypted
|
||||
SessionCookie expire rhost' session' <-
|
||||
either (const Nothing) Just $ decode decrypted
|
||||
guard $ expire > now
|
||||
guard $ rhost' == rhost
|
||||
return session'
|
||||
|
||||
data SessionCookie = SessionCookie UTCTime ByteString [(Text, Text)]
|
||||
deriving (Show, Read)
|
||||
instance Serialize SessionCookie where
|
||||
put (SessionCookie a b c) = putTime a >> put b >> put (map (unpack *** unpack) c)
|
||||
get = do
|
||||
a <- getTime
|
||||
b <- get
|
||||
c <- map (pack *** pack) `fmap` get
|
||||
return $ SessionCookie a b c
|
||||
|
||||
putTime :: Putter UTCTime
|
||||
putTime t@(UTCTime d _) = do
|
||||
put $ toModifiedJulianDay d
|
||||
let ndt = diffUTCTime t $ UTCTime d 0
|
||||
put $ toRational ndt
|
||||
|
||||
getTime :: Get UTCTime
|
||||
getTime = do
|
||||
d <- get
|
||||
ndt <- get
|
||||
return $ fromRational ndt `addUTCTime` UTCTime (ModifiedJulianDay d) 0
|
||||
250
yesod-core/Yesod/Message.hs
Normal file
250
yesod-core/Yesod/Message.hs
Normal file
@ -0,0 +1,250 @@
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE TypeSynonymInstances #-}
|
||||
module Yesod.Message
|
||||
( mkMessage
|
||||
, RenderMessage (..)
|
||||
, ToMessage (..)
|
||||
) where
|
||||
|
||||
import Language.Haskell.TH.Syntax
|
||||
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 (Deref (..), Ident (..), parseHash, derefToExp)
|
||||
import Text.ParserCombinators.Parsec (parse, many, eof, many1, noneOf, (<|>))
|
||||
import Control.Arrow ((***))
|
||||
import Data.Monoid (mempty, mappend)
|
||||
|
||||
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)
|
||||
101
yesod-core/Yesod/Request.hs
Normal file
101
yesod-core/Yesod/Request.hs
Normal file
@ -0,0 +1,101 @@
|
||||
---------------------------------------------------------
|
||||
--
|
||||
-- Module : Yesod.Request
|
||||
-- Copyright : Michael Snoyman
|
||||
-- License : BSD3
|
||||
--
|
||||
-- Maintainer : Michael Snoyman <michael@snoyman.com>
|
||||
-- Stability : Stable
|
||||
-- Portability : portable
|
||||
--
|
||||
-- | Provides a parsed version of the raw 'W.Request' data.
|
||||
--
|
||||
---------------------------------------------------------
|
||||
module Yesod.Request
|
||||
(
|
||||
-- * Request datatype
|
||||
RequestBodyContents
|
||||
, Request (..)
|
||||
, FileInfo (..)
|
||||
-- * Convenience functions
|
||||
, languages
|
||||
-- * Lookup parameters
|
||||
, lookupGetParam
|
||||
, lookupPostParam
|
||||
, lookupCookie
|
||||
, lookupFile
|
||||
-- ** Multi-lookup
|
||||
, lookupGetParams
|
||||
, lookupPostParams
|
||||
, lookupCookies
|
||||
, lookupFiles
|
||||
) where
|
||||
|
||||
import Yesod.Internal.Request
|
||||
import Yesod.Handler
|
||||
import Control.Monad (liftM)
|
||||
import Control.Monad.Instances () -- I'm missing the instance Monad ((->) r
|
||||
import Data.Maybe (listToMaybe)
|
||||
import Data.Text (Text)
|
||||
|
||||
-- | Get the list of supported languages supplied by the user.
|
||||
--
|
||||
-- Languages are determined based on the following three (in descending order
|
||||
-- of preference):
|
||||
--
|
||||
-- * The _LANG get parameter.
|
||||
--
|
||||
-- * The _LANG cookie.
|
||||
--
|
||||
-- * The _LANG user session variable.
|
||||
--
|
||||
-- * Accept-Language HTTP header.
|
||||
--
|
||||
-- This is handled by parseWaiRequest (not exposed).
|
||||
languages :: Monad mo => GGHandler s m mo [Text]
|
||||
languages = reqLangs `liftM` getRequest
|
||||
|
||||
lookup' :: Eq a => a -> [(a, b)] -> [b]
|
||||
lookup' a = map snd . filter (\x -> a == fst x)
|
||||
|
||||
-- | Lookup for GET parameters.
|
||||
lookupGetParams :: Monad mo => Text -> GGHandler s m mo [Text]
|
||||
lookupGetParams pn = do
|
||||
rr <- getRequest
|
||||
return $ lookup' pn $ reqGetParams rr
|
||||
|
||||
-- | Lookup for GET parameters.
|
||||
lookupGetParam :: Monad mo => Text -> GGHandler s m mo (Maybe Text)
|
||||
lookupGetParam = liftM listToMaybe . lookupGetParams
|
||||
|
||||
-- | Lookup for POST parameters.
|
||||
lookupPostParams :: Text -> GHandler s m [Text]
|
||||
lookupPostParams pn = do
|
||||
(pp, _) <- runRequestBody
|
||||
return $ lookup' pn pp
|
||||
|
||||
lookupPostParam :: Text
|
||||
-> GHandler s m (Maybe Text)
|
||||
lookupPostParam = liftM listToMaybe . lookupPostParams
|
||||
|
||||
-- | Lookup for POSTed files.
|
||||
lookupFile :: Text
|
||||
-> GHandler s m (Maybe FileInfo)
|
||||
lookupFile = liftM listToMaybe . lookupFiles
|
||||
|
||||
-- | Lookup for POSTed files.
|
||||
lookupFiles :: Text
|
||||
-> GHandler s m [FileInfo]
|
||||
lookupFiles pn = do
|
||||
(_, files) <- runRequestBody
|
||||
return $ lookup' pn files
|
||||
|
||||
-- | Lookup for cookie data.
|
||||
lookupCookie :: Monad mo => Text -> GGHandler s m mo (Maybe Text)
|
||||
lookupCookie = liftM listToMaybe . lookupCookies
|
||||
|
||||
-- | Lookup for cookie data.
|
||||
lookupCookies :: Monad mo => Text -> GGHandler s m mo [Text]
|
||||
lookupCookies pn = do
|
||||
rr <- getRequest
|
||||
return $ lookup' pn $ reqCookies rr
|
||||
267
yesod-core/Yesod/Widget.hs
Normal file
267
yesod-core/Yesod/Widget.hs
Normal file
@ -0,0 +1,267 @@
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
-- | Widgets combine HTML with JS and CSS dependencies with a unique identifier
|
||||
-- generator, allowing you to create truly modular HTML components.
|
||||
module Yesod.Widget
|
||||
( -- * Datatype
|
||||
GWidget
|
||||
, GGWidget (..)
|
||||
, PageContent (..)
|
||||
-- * Special Hamlet quasiquoter/TH for Widgets
|
||||
, whamlet
|
||||
, whamletFile
|
||||
, ihamletToRepHtml
|
||||
-- * Creating
|
||||
-- ** Head of page
|
||||
, setTitle
|
||||
, setTitleI
|
||||
, addHamletHead
|
||||
, addHtmlHead
|
||||
-- ** Body
|
||||
, addHamlet
|
||||
, addHtml
|
||||
, addWidget
|
||||
, addSubWidget
|
||||
-- ** CSS
|
||||
, addCassius
|
||||
, addCassiusMedia
|
||||
, addLucius
|
||||
, addLuciusMedia
|
||||
, addStylesheet
|
||||
, addStylesheetAttrs
|
||||
, addStylesheetRemote
|
||||
, addStylesheetRemoteAttrs
|
||||
, addStylesheetEither
|
||||
-- ** Javascript
|
||||
, addJulius
|
||||
, addJuliusBody
|
||||
, addCoffee
|
||||
, addCoffeeBody
|
||||
, addScript
|
||||
, addScriptAttrs
|
||||
, addScriptRemote
|
||||
, addScriptRemoteAttrs
|
||||
, addScriptEither
|
||||
-- * Utilities
|
||||
, extractBody
|
||||
) where
|
||||
|
||||
import Data.Monoid
|
||||
import Control.Monad.Trans.RWS
|
||||
import qualified Text.Blaze.Html5 as H
|
||||
import Text.Hamlet
|
||||
import Text.Cassius
|
||||
import Text.Lucius (Lucius)
|
||||
import Text.Julius
|
||||
import Text.Coffee
|
||||
import Yesod.Handler
|
||||
(Route, GHandler, GGHandler, YesodSubRoute(..), toMasterHandlerMaybe, getYesod
|
||||
, getMessageRender, getUrlRenderParams
|
||||
)
|
||||
import Yesod.Message (RenderMessage)
|
||||
import Yesod.Content (RepHtml (..), toContent)
|
||||
import Control.Applicative (Applicative)
|
||||
import Control.Monad.IO.Class (MonadIO (liftIO))
|
||||
import Control.Monad.Trans.Class (MonadTrans (lift))
|
||||
import Yesod.Internal
|
||||
import Control.Monad (liftM)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Map as Map
|
||||
import Language.Haskell.TH.Quote (QuasiQuoter)
|
||||
import Language.Haskell.TH.Syntax (Q, Exp (InfixE, VarE, LamE), Pat (VarP), newName)
|
||||
|
||||
import Control.Monad.IO.Control (MonadControlIO)
|
||||
import qualified Text.Hamlet as NP
|
||||
import Data.Text.Lazy.Builder (fromLazyText)
|
||||
import Text.Blaze (toHtml, preEscapedLazyText)
|
||||
|
||||
-- | A generic widget, allowing specification of both the subsite and master
|
||||
-- site datatypes. This is basically a large 'WriterT' stack keeping track of
|
||||
-- dependencies along with a 'StateT' to track unique identifiers.
|
||||
newtype GGWidget m monad a = GWidget { unGWidget :: GWInner m monad a }
|
||||
deriving (Functor, Applicative, Monad, MonadIO, MonadControlIO)
|
||||
|
||||
instance MonadTrans (GGWidget m) where
|
||||
lift = GWidget . lift
|
||||
|
||||
type GWidget s m = GGWidget m (GHandler s m)
|
||||
type GWInner master = RWST () (GWData (Route master)) Int
|
||||
|
||||
instance (Monad monad, a ~ ()) => Monoid (GGWidget master monad a) where
|
||||
mempty = return ()
|
||||
mappend x y = x >> y
|
||||
|
||||
addSubWidget :: (YesodSubRoute sub master) => sub -> GWidget sub master a -> GWidget sub' master a
|
||||
addSubWidget sub (GWidget w) = do
|
||||
master <- lift getYesod
|
||||
let sr = fromSubRoute sub master
|
||||
s <- GWidget get
|
||||
(a, s', w') <- lift $ toMasterHandlerMaybe sr (const sub) Nothing $ runRWST w () s
|
||||
GWidget $ put s'
|
||||
GWidget $ tell w'
|
||||
return a
|
||||
|
||||
-- | Set the page title. Calling 'setTitle' multiple times overrides previously
|
||||
-- set values.
|
||||
setTitle :: Monad m => Html -> GGWidget master m ()
|
||||
setTitle x = GWidget $ tell $ GWData mempty (Last $ Just $ Title x) mempty mempty mempty mempty mempty
|
||||
|
||||
-- | Set the page title. Calling 'setTitle' multiple times overrides previously
|
||||
-- set values.
|
||||
setTitleI :: (RenderMessage master msg, Monad m) => msg -> GGWidget master (GGHandler sub master m) ()
|
||||
setTitleI msg = do
|
||||
mr <- lift getMessageRender
|
||||
setTitle $ toHtml $ mr msg
|
||||
|
||||
-- | Add a 'Hamlet' to the head tag.
|
||||
addHamletHead :: Monad m => Hamlet (Route master) -> GGWidget master m ()
|
||||
addHamletHead = GWidget . tell . GWData mempty mempty mempty mempty mempty mempty . Head
|
||||
|
||||
-- | Add a 'Html' to the head tag.
|
||||
addHtmlHead :: Monad m => Html -> GGWidget master m ()
|
||||
addHtmlHead = addHamletHead . const
|
||||
|
||||
-- | Add a 'Hamlet' to the body tag.
|
||||
addHamlet :: Monad m => Hamlet (Route master) -> GGWidget master m ()
|
||||
addHamlet x = GWidget $ tell $ GWData (Body x) mempty mempty mempty mempty mempty mempty
|
||||
|
||||
-- | Add a 'Html' to the body tag.
|
||||
addHtml :: Monad m => Html -> GGWidget master m ()
|
||||
addHtml = addHamlet . const
|
||||
|
||||
-- | Add another widget. This is defined as 'id', by can help with types, and
|
||||
-- makes widget blocks look more consistent.
|
||||
addWidget :: Monad mo => GGWidget m mo () -> GGWidget m mo ()
|
||||
addWidget = id
|
||||
|
||||
-- | Add some raw CSS to the style tag. Applies to all media types.
|
||||
addCassius :: Monad m => Cassius (Route master) -> GGWidget master m ()
|
||||
addCassius x = GWidget $ tell $ GWData mempty mempty mempty mempty (Map.singleton Nothing x) mempty mempty
|
||||
|
||||
-- | Identical to 'addCassius'.
|
||||
addLucius :: Monad m => Lucius (Route master) -> GGWidget master m ()
|
||||
addLucius = addCassius
|
||||
|
||||
-- | Add some raw CSS to the style tag, for a specific media type.
|
||||
addCassiusMedia :: Monad m => Text -> Cassius (Route master) -> GGWidget master m ()
|
||||
addCassiusMedia m x = GWidget $ tell $ GWData mempty mempty mempty mempty (Map.singleton (Just m) x) mempty mempty
|
||||
|
||||
-- | Identical to 'addCassiusMedia'.
|
||||
addLuciusMedia :: Monad m => Text -> Lucius (Route master) -> GGWidget master m ()
|
||||
addLuciusMedia = addCassiusMedia
|
||||
|
||||
-- | Link to the specified local stylesheet.
|
||||
addStylesheet :: Monad m => Route master -> GGWidget master m ()
|
||||
addStylesheet = flip addStylesheetAttrs []
|
||||
|
||||
-- | Link to the specified local stylesheet.
|
||||
addStylesheetAttrs :: Monad m => Route master -> [(Text, Text)] -> GGWidget master m ()
|
||||
addStylesheetAttrs x y = GWidget $ tell $ GWData mempty mempty mempty (toUnique $ Stylesheet (Local x) y) mempty mempty mempty
|
||||
|
||||
-- | Link to the specified remote stylesheet.
|
||||
addStylesheetRemote :: Monad m => Text -> GGWidget master m ()
|
||||
addStylesheetRemote = flip addStylesheetRemoteAttrs []
|
||||
|
||||
-- | Link to the specified remote stylesheet.
|
||||
addStylesheetRemoteAttrs :: Monad m => Text -> [(Text, Text)] -> GGWidget master m ()
|
||||
addStylesheetRemoteAttrs x y = GWidget $ tell $ GWData mempty mempty mempty (toUnique $ Stylesheet (Remote x) y) mempty mempty mempty
|
||||
|
||||
addStylesheetEither :: Monad m => Either (Route master) Text -> GGWidget master m ()
|
||||
addStylesheetEither = either addStylesheet addStylesheetRemote
|
||||
|
||||
addScriptEither :: Monad m => Either (Route master) Text -> GGWidget master m ()
|
||||
addScriptEither = either addScript addScriptRemote
|
||||
|
||||
-- | Link to the specified local script.
|
||||
addScript :: Monad m => Route master -> GGWidget master m ()
|
||||
addScript = flip addScriptAttrs []
|
||||
|
||||
-- | Link to the specified local script.
|
||||
addScriptAttrs :: Monad m => Route master -> [(Text, Text)] -> GGWidget master m ()
|
||||
addScriptAttrs x y = GWidget $ tell $ GWData mempty mempty (toUnique $ Script (Local x) y) mempty mempty mempty mempty
|
||||
|
||||
-- | Link to the specified remote script.
|
||||
addScriptRemote :: Monad m => Text -> GGWidget master m ()
|
||||
addScriptRemote = flip addScriptRemoteAttrs []
|
||||
|
||||
-- | Link to the specified remote script.
|
||||
addScriptRemoteAttrs :: Monad m => Text -> [(Text, Text)] -> GGWidget master m ()
|
||||
addScriptRemoteAttrs x y = GWidget $ tell $ GWData mempty mempty (toUnique $ Script (Remote x) y) mempty mempty mempty mempty
|
||||
|
||||
-- | Include raw Javascript in the page's script tag.
|
||||
addJulius :: Monad m => Julius (Route master) -> GGWidget master m ()
|
||||
addJulius x = GWidget $ tell $ GWData mempty mempty mempty mempty mempty (Just x) mempty
|
||||
|
||||
-- | Add a new script tag to the body with the contents of this 'Julius'
|
||||
-- template.
|
||||
addJuliusBody :: Monad m => Julius (Route master) -> GGWidget master m ()
|
||||
addJuliusBody j = addHamlet $ \r -> H.script $ preEscapedLazyText $ renderJulius r j
|
||||
|
||||
-- | Add Coffesscript to the page's script tag. Requires the coffeescript
|
||||
-- executable to be present at runtime.
|
||||
addCoffee :: MonadIO m => Coffee (Route master) -> GGWidget master (GGHandler sub master m) ()
|
||||
addCoffee c = do
|
||||
render <- lift getUrlRenderParams
|
||||
t <- liftIO $ renderCoffee render c
|
||||
addJulius $ const $ Javascript $ fromLazyText t
|
||||
|
||||
-- | Add a new script tag to the body with the contents of this Coffesscript
|
||||
-- template. Requires the coffeescript executable to be present at runtime.
|
||||
addCoffeeBody :: MonadIO m => Coffee (Route master) -> GGWidget master (GGHandler sub master m) ()
|
||||
addCoffeeBody c = do
|
||||
render <- lift getUrlRenderParams
|
||||
t <- liftIO $ renderCoffee render c
|
||||
addJuliusBody $ const $ Javascript $ fromLazyText t
|
||||
|
||||
-- | Pull out the HTML tag contents and return it. Useful for performing some
|
||||
-- manipulations. It can be easier to use this sometimes than 'wrapWidget'.
|
||||
extractBody :: Monad mo => GGWidget m mo () -> GGWidget m mo (Hamlet (Route m))
|
||||
extractBody (GWidget w) =
|
||||
GWidget $ mapRWST (liftM go) w
|
||||
where
|
||||
go ((), s, GWData (Body h) b c d e f g) = (h, s, GWData (Body mempty) b c d e f g)
|
||||
|
||||
-- | Content for a web page. By providing this datatype, we can easily create
|
||||
-- generic site templates, which would have the type signature:
|
||||
--
|
||||
-- > PageContent url -> Hamlet url
|
||||
data PageContent url = PageContent
|
||||
{ pageTitle :: Html
|
||||
, pageHead :: Hamlet url
|
||||
, pageBody :: Hamlet url
|
||||
}
|
||||
|
||||
whamlet :: QuasiQuoter
|
||||
whamlet = NP.hamletWithSettings rules NP.defaultHamletSettings
|
||||
|
||||
whamletFile :: FilePath -> Q Exp
|
||||
whamletFile = NP.hamletFileWithSettings rules NP.defaultHamletSettings
|
||||
|
||||
rules :: Q NP.HamletRules
|
||||
rules = do
|
||||
ah <- [|addHtml|]
|
||||
let helper qg f = do
|
||||
x <- newName "urender"
|
||||
e <- f $ VarE x
|
||||
let e' = LamE [VarP x] e
|
||||
g <- qg
|
||||
bind <- [|(>>=)|]
|
||||
return $ InfixE (Just g) bind (Just e')
|
||||
let ur f = do
|
||||
let env = NP.Env
|
||||
(Just $ helper [|lift getUrlRenderParams|])
|
||||
(Just $ helper [|liftM (toHtml .) $ lift getMessageRender|])
|
||||
f env
|
||||
return $ NP.HamletRules ah ur $ \_ b -> return b
|
||||
|
||||
-- | Wraps the 'Content' generated by 'hamletToContent' in a 'RepHtml'.
|
||||
ihamletToRepHtml :: (Monad mo, RenderMessage master message)
|
||||
=> NP.IHamlet message (Route master)
|
||||
-> GGHandler sub master mo RepHtml
|
||||
ihamletToRepHtml ih = do
|
||||
urender <- getUrlRenderParams
|
||||
mrender <- getMessageRender
|
||||
return $ RepHtml $ toContent $ ih (toHtml . mrender) urender
|
||||
40
yesod-core/helloworld.hs
Normal file
40
yesod-core/helloworld.hs
Normal file
@ -0,0 +1,40 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
import Yesod.Core
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
import Data.Text (unpack)
|
||||
|
||||
data Subsite = Subsite String
|
||||
|
||||
mkYesodSub "Subsite" [] [$parseRoutes|
|
||||
/ SubRootR GET
|
||||
/multi/*Strings SubMultiR
|
||||
|]
|
||||
|
||||
getSubRootR :: Yesod m => GHandler Subsite m RepPlain
|
||||
getSubRootR = do
|
||||
Subsite s <- getYesodSub
|
||||
tm <- getRouteToMaster
|
||||
render <- getUrlRender
|
||||
$(logDebug) "I'm in SubRootR"
|
||||
return $ RepPlain $ toContent $ "Hello Sub World: " ++ s ++ ". " ++ unpack (render (tm SubRootR))
|
||||
|
||||
handleSubMultiR :: Yesod m => Strings -> GHandler Subsite m RepPlain
|
||||
handleSubMultiR x = do
|
||||
Subsite y <- getYesodSub
|
||||
$(logInfo) "In SubMultiR"
|
||||
return . RepPlain . toContent . show $ (x, y)
|
||||
|
||||
data HelloWorld = HelloWorld { getSubsite :: String -> Subsite }
|
||||
mkYesod "HelloWorld" [$parseRoutes|
|
||||
/ RootR GET
|
||||
/subsite/#String SubsiteR Subsite getSubsite
|
||||
|]
|
||||
instance Yesod HelloWorld where approot _ = ""
|
||||
-- getRootR :: GHandler HelloWorld HelloWorld RepPlain -- FIXME remove type sig
|
||||
getRootR = do
|
||||
$(logOther "HAHAHA") "Here I am"
|
||||
return $ RepPlain "Hello World"
|
||||
main = toWaiApp (HelloWorld Subsite) >>= run 3000
|
||||
17
yesod-core/runtests.hs
Normal file
17
yesod-core/runtests.hs
Normal file
@ -0,0 +1,17 @@
|
||||
import Test.Framework (defaultMain)
|
||||
import Test.CleanPath
|
||||
import Test.Exceptions
|
||||
import Test.Widget
|
||||
import Test.Media
|
||||
import Test.Links
|
||||
import Test.NoOverloadedStrings
|
||||
|
||||
main :: IO ()
|
||||
main = defaultMain
|
||||
[ cleanPathTest
|
||||
, exceptionsTest
|
||||
, widgetTest
|
||||
, mediaTest
|
||||
, linksTest
|
||||
, noOverloadedTest
|
||||
]
|
||||
3
yesod-core/static/script.js
Normal file
3
yesod-core/static/script.js
Normal file
@ -0,0 +1,3 @@
|
||||
$(function(){
|
||||
$("p.noscript").hide();
|
||||
});
|
||||
12
yesod-core/static/style.css
Normal file
12
yesod-core/static/style.css
Normal file
@ -0,0 +1,12 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
width: 760px;
|
||||
margin: 1em auto;
|
||||
border: 2px solid #000;
|
||||
padding: 0.5em;
|
||||
background: #fff;
|
||||
}
|
||||
3
yesod-core/static/style2.css
Normal file
3
yesod-core/static/style2.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
1
yesod-core/test/en.msg
Normal file
1
yesod-core/test/en.msg
Normal file
@ -0,0 +1 @@
|
||||
Another: String
|
||||
79
yesod-core/widget-benchmark.hs
Normal file
79
yesod-core/widget-benchmark.hs
Normal file
@ -0,0 +1,79 @@
|
||||
-- | BigTable benchmark implemented using Hamlet.
|
||||
--
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
module Main where
|
||||
|
||||
import Criterion.Main
|
||||
import Text.Hamlet
|
||||
import Numeric (showInt)
|
||||
import qualified Data.ByteString.Lazy as L
|
||||
import qualified Text.Blaze.Renderer.Utf8 as Utf8
|
||||
import Data.Monoid (mconcat)
|
||||
import Text.Blaze.Html5 (table, tr, td)
|
||||
import Yesod.Widget
|
||||
import Control.Monad.Trans.Writer
|
||||
import Control.Monad.Trans.RWS
|
||||
import Data.Functor.Identity
|
||||
import Yesod.Internal
|
||||
|
||||
main = defaultMain
|
||||
[ bench "bigTable html" $ nf bigTableHtml bigTableData
|
||||
, bench "bigTable hamlet" $ nf bigTableHamlet bigTableData
|
||||
, bench "bigTable widget" $ nf bigTableWidget bigTableData
|
||||
, bench "bigTable blaze" $ nf bigTableBlaze bigTableData
|
||||
]
|
||||
where
|
||||
rows :: Int
|
||||
rows = 1000
|
||||
|
||||
bigTableData :: [[Int]]
|
||||
bigTableData = replicate rows [1..10]
|
||||
{-# NOINLINE bigTableData #-}
|
||||
|
||||
bigTableHtml rows = L.length $ renderHtml [$hamlet|
|
||||
<table
|
||||
$forall row <- rows
|
||||
<tr
|
||||
$forall cell <- row
|
||||
<td>#{show cell}
|
||||
|]
|
||||
|
||||
bigTableHamlet rows = L.length $ renderHamlet id [$hamlet|
|
||||
<table
|
||||
$forall row <- rows
|
||||
<tr
|
||||
$forall cell <- row
|
||||
<td>#{show cell}
|
||||
|]
|
||||
|
||||
bigTableWidget rows = L.length $ renderHtml $ (run [$hamlet|
|
||||
<table
|
||||
$forall row <- rows
|
||||
<tr
|
||||
$forall cell <- row
|
||||
<td>#{show cell}
|
||||
|]) (\_ _ -> "foo")
|
||||
where
|
||||
run (GWidget w) =
|
||||
let (_, _, GWData (Body x) _ _ _ _ _ _) = runRWS w () 0
|
||||
in x
|
||||
{-
|
||||
run (GWidget w) = runIdentity $ do
|
||||
w' <- flip evalStateT 0
|
||||
$ runWriterT $ runWriterT $ runWriterT $ runWriterT
|
||||
$ runWriterT $ runWriterT $ runWriterT w
|
||||
let ((((((((),
|
||||
Body body),
|
||||
_),
|
||||
_),
|
||||
_),
|
||||
_),
|
||||
_),
|
||||
_) = w'
|
||||
|
||||
return body
|
||||
-}
|
||||
|
||||
bigTableBlaze t = L.length $ renderHtml $ table $ mconcat $ map row t
|
||||
where
|
||||
row r = tr $ mconcat $ map (td . string . show) r
|
||||
94
yesod-core/yesod-core.cabal
Normal file
94
yesod-core/yesod-core.cabal
Normal file
@ -0,0 +1,94 @@
|
||||
name: yesod-core
|
||||
version: 0.9.0
|
||||
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:
|
||||
Yesod is a framework designed to foster creation of RESTful web application that have strong compile-time guarantees of correctness. It also affords space efficient code and portability to many deployment backends, from CGI to stand-alone serving.
|
||||
.
|
||||
The Yesod documentation site <http://www.yesodweb.com/> has much more information, tutorials and information on some of the supporting packages, like Hamlet and Persistent.
|
||||
category: Web, Yesod
|
||||
stability: Stable
|
||||
cabal-version: >= 1.6
|
||||
build-type: Simple
|
||||
homepage: http://www.yesodweb.com/
|
||||
|
||||
flag test
|
||||
description: Build the executable to run unit tests
|
||||
default: False
|
||||
|
||||
flag ghc7
|
||||
|
||||
library
|
||||
if flag(ghc7)
|
||||
build-depends: base >= 4.3 && < 5
|
||||
cpp-options: -DGHC7
|
||||
else
|
||||
build-depends: base >= 4 && < 4.3
|
||||
build-depends: time >= 1.1.4 && < 1.3
|
||||
, wai >= 0.4 && < 0.5
|
||||
, wai-extra >= 0.4 && < 0.5
|
||||
, bytestring >= 0.9.1.4 && < 0.10
|
||||
, text >= 0.5 && < 0.12
|
||||
, template-haskell
|
||||
, path-pieces >= 0.0 && < 0.1
|
||||
, hamlet >= 0.9 && < 0.10
|
||||
, blaze-builder >= 0.2.1 && < 0.4
|
||||
, transformers >= 0.2 && < 0.3
|
||||
, clientsession >= 0.6 && < 0.7
|
||||
, random >= 1.0.0.2 && < 1.1
|
||||
, cereal >= 0.2 && < 0.4
|
||||
, old-locale >= 1.0.0.2 && < 1.1
|
||||
, failure >= 0.1 && < 0.2
|
||||
, containers >= 0.2 && < 0.5
|
||||
, monad-control >= 0.2 && < 0.3
|
||||
, enumerator >= 0.4.7 && < 0.5
|
||||
, cookie >= 0.3 && < 0.4
|
||||
, blaze-html >= 0.4 && < 0.5
|
||||
, http-types >= 0.6 && < 0.7
|
||||
, case-insensitive >= 0.2 && < 0.4
|
||||
, parsec >= 2 && < 3.2
|
||||
, directory >= 1 && < 1.2
|
||||
exposed-modules: Yesod.Content
|
||||
Yesod.Core
|
||||
Yesod.Dispatch
|
||||
Yesod.Handler
|
||||
Yesod.Request
|
||||
Yesod.Widget
|
||||
Yesod.Message
|
||||
other-modules: Yesod.Internal
|
||||
Yesod.Internal.Core
|
||||
Yesod.Internal.Session
|
||||
Yesod.Internal.Request
|
||||
Yesod.Internal.Dispatch
|
||||
Yesod.Internal.RouteParsing
|
||||
Paths_yesod_core
|
||||
ghc-options: -Wall
|
||||
if flag(test)
|
||||
Buildable: False
|
||||
|
||||
executable runtests
|
||||
if flag(ghc7)
|
||||
build-depends: base >= 4.3 && < 5
|
||||
cpp-options: -DGHC7
|
||||
else
|
||||
build-depends: base >= 4 && < 4.3
|
||||
if flag(test)
|
||||
Buildable: True
|
||||
cpp-options: -DTEST
|
||||
build-depends: test-framework,
|
||||
test-framework-quickcheck2,
|
||||
test-framework-hunit,
|
||||
HUnit,
|
||||
wai-test,
|
||||
QuickCheck >= 2 && < 3
|
||||
else
|
||||
Buildable: False
|
||||
ghc-options: -Wall
|
||||
main-is: runtests.hs
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/snoyberg/yesod-core.git
|
||||
Loading…
Reference in New Issue
Block a user