packages feed

yesod-core 0.9.2 → 0.9.3

raw patch · 16 files changed

+784/−53 lines, 16 filesdep +aeson-nativedep +data-objectdep +data-object-yamldep ~hspecdep ~randomPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson-native, data-object, data-object-yaml, vector

Dependency ranges changed: hspec, random

API changes (from Hackage documentation)

+ Yesod.Config: AppConfig :: e -> Int -> Text -> AppConfig e
+ Yesod.Config: appEnv :: AppConfig e -> e
+ Yesod.Config: appPort :: AppConfig e -> Int
+ Yesod.Config: appRoot :: AppConfig e -> Text
+ Yesod.Config: data AppConfig e
+ Yesod.Config: instance Show e => Show (AppConfig e)
+ Yesod.Config: loadConfig :: Show e => e -> IO (AppConfig e)
+ Yesod.Config: withYamlEnvironment :: Show e => FilePath -> e -> (TextObject -> IO a) -> IO a
+ Yesod.Core: gzipCompressFiles :: Yesod a => a -> Bool
+ Yesod.Core: yepnopeJs :: Yesod a => a -> Maybe (Either Text (Route a))
+ Yesod.Internal.TestApi: parseWaiRequest' :: RandomGen g => Request -> [(Text, Text)] -> Maybe a -> g -> Request
+ Yesod.Internal.TestApi: randomString :: RandomGen g => Int -> g -> String

Files

+ Yesod/Config.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE OverloadedStrings #-}+module Yesod.Config+    ( AppConfig(..)+    , loadConfig+    , withYamlEnvironment+    ) where++import Control.Monad (join)+import Data.Maybe (fromMaybe)+import Data.Object+import Data.Object.Yaml++import Data.Text (Text)+import qualified Data.Text as T++-- | Dynamic per-environment configuration which can be loaded at+--   run-time negating the need to recompile between environments.+data AppConfig e = AppConfig+    { appEnv  :: e+    , appPort :: Int+    , appRoot :: Text+    } deriving (Show)++-- | Load an @'AppConfig'@ from @config\/settings.yml@.+--+--   Some examples:+--+--   > -- typical local development+--   > Development:+--   >   host: localhost+--   >   port: 3000+--   >+--   >   -- ssl: will default false+--   >   -- approot: will default to "http://localhost:3000"+--+--   > -- typical outward-facing production box+--   > Production:+--   >   host: www.example.com+--   >+--   >   -- ssl: will default false+--   >   -- port: will default 80+--   >   -- approot: will default "http://www.example.com"+--+--   > -- maybe you're reverse proxying connections to the running app+--   > -- on some other port+--   > Production:+--   >   port: 8080+--   >   approot: "http://example.com"+--   >+--   > -- approot is specified so that the non-80 port is not appended+--   > -- automatically.+--+loadConfig :: Show e => e -> IO (AppConfig e)+loadConfig env = withYamlEnvironment "config/settings.yml" env $ \e' -> do+    e <- maybe (fail "Expected map") return $ fromMapping e'+    let mssl     = lookupScalar "ssl"     e+    let mhost    = lookupScalar "host"    e+    let mport    = lookupScalar "port"    e+    let mapproot = lookupScalar "approot" e++    -- set some default arguments+    let ssl = maybe False toBool mssl+    port <- safeRead "port" $ fromMaybe (if ssl then "443" else "80") mport++    approot <- case (mhost, mapproot) of+        (_        , Just ar) -> return ar+        (Just host, _      ) -> return $ T.concat+            [ if ssl then "https://" else "http://"+            , host+            , addPort ssl port+            ]+        _                    -> fail "You must supply either a host or approot"++    return $ AppConfig+        { appEnv  = env+        , appPort = port+        , appRoot = approot+        }++    where+        toBool :: Text -> Bool+        toBool = (`elem` ["true", "TRUE", "yes", "YES", "Y", "1"])++        addPort :: Bool -> Int -> Text+        addPort True  443 = ""+        addPort False 80  = ""+        addPort _     p   = T.pack $ ':' : show p++-- | Loads the configuration block in the passed file named by the+--   passed environment, yeilds to the passed function as a mapping.+--+--   Errors in the case of a bad load or if your function returns+--   @Nothing@.+withYamlEnvironment :: Show e+                    => FilePath -- ^ the yaml file+                    -> e        -- ^ the environment you want to load+                    -> (TextObject -> IO a) -- ^ what to do with the mapping+                    -> IO a+withYamlEnvironment fp env f = do+    obj <- join $ decodeFile fp+    envs <- fromMapping obj+    conf <- maybe (fail $ "Could not find environment: " ++ show env) return+          $ lookup (T.pack $ show env) envs+    f conf++-- | Returns 'fail' if read fails+safeRead :: Monad m => String -> Text -> m Int+safeRead name t = case reads s of+    (i, _):_ -> return i+    []       -> fail $ concat ["Invalid value for ", name, ": ", s]+  where+    s = T.unpack t
Yesod/Core.hs view
@@ -33,6 +33,7 @@     , module Yesod.Request     , module Yesod.Widget     , module Yesod.Message+    , module Yesod.Config     ) where  import Yesod.Internal.Core@@ -42,6 +43,7 @@ import Yesod.Request import Yesod.Widget import Yesod.Message+import Yesod.Config  import Language.Haskell.TH.Syntax import Data.Text (Text)
Yesod/Dispatch.hs view
@@ -173,10 +173,10 @@  -- | 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+-- middlewares: GZIP compression, JSON-P and autohead. 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+toWaiApp y = gzip (gzipCompressFiles y) . 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.
Yesod/Internal/Core.hs view
@@ -52,7 +52,7 @@ import Text.Hamlet import Text.Cassius import Text.Julius-import Text.Blaze ((!), customAttribute, textTag, toValue)+import Text.Blaze ((!), customAttribute, textTag, toValue, unsafeLazyByteString) import qualified Text.Blaze.Html5 as TBH import Data.Text.Lazy.Builder (toLazyText) import Data.Text.Lazy.Encoding (encodeUtf8)@@ -75,6 +75,9 @@ import qualified Data.Text.Lazy.Builder as TB import Language.Haskell.TH.Syntax (Loc (..), Lift (..)) import Text.Blaze (preEscapedLazyText)+import Data.Aeson (Value (Array, String))+import Data.Aeson.Encode (encode)+import qualified Data.Vector as Vector  #if GHC7 #define HAMLET hamlet@@ -205,8 +208,9 @@       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'.+    -- | Builds an absolute URL by concatenating the application root with the  +    -- pieces of a path and a query string, if any. +    -- Note that the pieces of the path have been previously cleaned up by 'cleanPath'.     joinPath :: a              -> TS.Text -- ^ application root              -> [TS.Text] -- ^ path pieces@@ -259,6 +263,15 @@         formatLogMessage loc level msg >>=         Data.Text.Lazy.IO.putStrLn +    -- | Apply gzip compression to files. Default is false.+    gzipCompressFiles :: a -> Bool+    gzipCompressFiles _ = False++    -- | Location of yepnope.js, if any. If one is provided, then all+    -- Javascript files will be loaded asynchronously.+    yepnopeJs :: a -> Maybe (Either Text (Route a))+    yepnopeJs _ = Nothing+ messageLoggerHandler :: (Yesod m, MonadIO mo)                      => Loc -> LogLevel -> Text -> GGHandler s m mo () messageLoggerHandler loc level msg = do@@ -467,18 +480,22 @@     x <- isAuthorized r isWrite     return $ if x == Authorized then Just r else Nothing +jsToHtml :: Javascript -> Html+jsToHtml (Javascript b) = preEscapedLazyText $ toLazyText b++jelper :: JavascriptUrl url -> HtmlUrl url+jelper = fmap jsToHtml+ -- | Convert a widget to a 'PageContent'. widgetToPageContent :: (Eq (Route master), Yesod master)                     => GWidget sub master ()                     -> GHandler sub master (PageContent (Route master)) widgetToPageContent (GWidget w) = do+    master <- getYesod     ((), _, 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 :: JavascriptUrl url -> HtmlUrl url-        jelper = fmap jsToHtml      render <- getUrlRenderParams     let renderLoc x =@@ -531,16 +548,54 @@             <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}+$maybe _ <- yepnopeJs master+$nothing+    $forall s <- scripts+        ^{mkScriptTag s}+    $maybe j <- jscript+        $maybe s <- jsLoc+            <script src="#{s}">+        $nothing+            <script>^{jelper j} \^{head'} |]-    return $ PageContent title head'' body+    let (mcomplete, ynscripts) = ynHelper render scripts jscript jsLoc+    let bodyYN = [HAMLET|+^{body}+$maybe eyn <- yepnopeJs master+    $maybe yn <- left eyn+        <script src=#{yn}>+    $maybe yn <- right eyn+        <script src=@{yn}>+    $maybe complete <- mcomplete+        <script>yepnope({load:#{ynscripts},complete:function(){^{complete}}})+    $nothing+        <script>yepnope({load:#{ynscripts}})+|]+    return $ PageContent title head'' bodyYN++ynHelper :: (url -> [x] -> Text)+         -> [Script (url)]+         -> Maybe (JavascriptUrl (url))+         -> Maybe Text+         -> (Maybe (HtmlUrl (url)), Html)+ynHelper render scripts jscript jsLoc =+    (mcomplete, unsafeLazyByteString $ encode $ Array $ Vector.fromList $ map String scripts'')+  where+    scripts' = map goScript scripts+    scripts'' =+        case jsLoc of+            Just s -> scripts' ++ [s]+            Nothing -> scripts'+    goScript (Script (Local url) _) = render url []+    goScript (Script (Remote s) _) = s+    mcomplete =+        case jsLoc of+            Just{} -> Nothing+            Nothing ->+                case jscript of+                    Nothing -> Nothing+                    Just j -> Just $ jelper j  yesodVersion :: String yesodVersion = showVersion Paths_yesod_core.version
Yesod/Internal/Request.hs view
@@ -1,23 +1,27 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE CPP #-} module Yesod.Internal.Request     ( parseWaiRequest     , Request (..)     , RequestBodyContents     , FileInfo (..)+    -- The below are exported for testing.+    , randomString+    , parseWaiRequest'     ) where -import Control.Arrow (first, second)+import Control.Applicative ((<$>))+import Control.Arrow (second) import qualified Network.Wai.Parse as NWP import Yesod.Internal import qualified Network.Wai as W-import System.Random (randomR, newStdGen)+import System.Random (RandomGen, newStdGen, randomRs) 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 Data.Maybe (fromMaybe, catMaybes) import qualified Data.ByteString.Lazy as L  -- | The parsed request information.@@ -35,38 +39,42 @@                 -> [(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+parseWaiRequest env session' key' = parseWaiRequest' env session' key' <$> newStdGen++parseWaiRequest' :: RandomGen g+                 => W.Request+                 -> [(Text, Text)] -- ^ session+                 -> Maybe a+                 -> g+                 -> Request+parseWaiRequest' env session' key' gen = 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'')+    gets' = queryToQueryText $ W.queryString env+    gets'' = map (second $ fromMaybe "") gets'+    reqCookie = lookup "Cookie" $ W.requestHeaders env+    cookies' = maybe [] parseCookiesText reqCookie+    acceptLang = lookup "Accept-Language" $ W.requestHeaders env+    langs = map (pack . S8.unpack) $ maybe [] NWP.parseHttpAccept acceptLang+    -- The language preferences are prioritized as follows:+    langs' = catMaybes [ join $ lookup langKey gets' -- Query _LANG+                       , lookup langKey cookies'     -- Cookie _LANG+                       , lookup langKey session'     -- Session _LANG+                       ] ++ langs                    -- Accept-Language(s)+    -- If sessions are disabled nonces should not be used (any+    -- nonceKey present in the session is ignored). If sessions+    -- are enabled and a session has no nonceKey a new one is+    -- generated.+    nonce = case (key', lookup nonceKey session') of+                (Nothing, _) -> Nothing+                (_, Just x)  -> Just x+                _            -> Just $ pack $ randomString 10 gen++-- | Generate a random String of alphanumerical characters+-- (a-z, A-Z, and 0-9) of the given length using the given+-- random number generator.+randomString :: RandomGen g => Int -> g -> String+randomString len = take len . map toChar . randomRs (0, 61)+  where     toChar i         | i < 26 = toEnum $ i + fromEnum 'A'         | i < 52 = toEnum $ i + fromEnum 'a' - 26
+ Yesod/Internal/TestApi.hs view
@@ -0,0 +1,11 @@+--+-- | WARNING: This module exposes internal interfaces solely for the+-- purpose of facilitating cabal-driven testing of said interfaces.+-- This module is NOT part of the public Yesod API and should NOT be+-- imported by library users.+--+module Yesod.Internal.TestApi+  ( randomString, parseWaiRequest'+  ) where++import Yesod.Internal.Request (randomString, parseWaiRequest')
+ test/Test/CleanPath.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+module Test.CleanPath (cleanPathTest, Widget) where++import Test.Hspec+import Test.Hspec.HUnit()++import Yesod.Core hiding (Request)++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 :: a -> 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 :: Handler RepPlain+getFooR = return $ RepPlain "foo"++getFooStringR :: String -> Handler RepPlain+getFooStringR = return . RepPlain . toContent++getBarR, getPlainR :: Handler RepPlain+getBarR = return $ RepPlain "bar"+getPlainR = return $ RepPlain "plain"++cleanPathTest :: [Spec]+cleanPathTest =+  describe "Test.CleanPath"+    [ it "remove trailing slash" removeTrailingSlash+    , it "noTrailingSlash" noTrailingSlash+    , it "add trailing slash" addTrailingSlash+    , it "has trailing slash" hasTrailingSlash+    , it "/foo/something" fooSomething+    , it "subsite dispatch" subsiteDispatch+    , it "redirect with query string" redQueryString+    ]++runner :: Session () -> IO ()+runner f = toWaiApp Y >>= runSession f++removeTrailingSlash :: IO ()+removeTrailingSlash = runner $ do+    res <- request defaultRequest+                { pathInfo = decodePathSegments "/foo/"+                }+    assertStatus 301 res+    assertHeader "Location" "http://test/foo" res++noTrailingSlash :: IO ()+noTrailingSlash = runner $ do+    res <- request defaultRequest+                { pathInfo = decodePathSegments "/foo"+                }+    assertStatus 200 res+    assertContentType "text/plain; charset=utf-8" res+    assertBody "foo" res++addTrailingSlash :: IO ()+addTrailingSlash = runner $ do+    res <- request defaultRequest+                { pathInfo = decodePathSegments "/bar"+                }+    assertStatus 301 res+    assertHeader "Location" "http://test/bar/" res++hasTrailingSlash :: IO ()+hasTrailingSlash = runner $ do+    res <- request defaultRequest+                { pathInfo = decodePathSegments "/bar/"+                }+    assertStatus 200 res+    assertContentType "text/plain; charset=utf-8" res+    assertBody "bar" res++fooSomething :: IO ()+fooSomething = runner $ do+    res <- request defaultRequest+                { pathInfo = decodePathSegments "/foo/something"+                }+    assertStatus 200 res+    assertContentType "text/plain; charset=utf-8" res+    assertBody "something" res++subsiteDispatch :: IO ()+subsiteDispatch = runner $ do+    res <- request defaultRequest+                { pathInfo = decodePathSegments "/subsite/1/2/3/"+                }+    assertStatus 200 res+    assertContentType "SUBSITE" res+    assertBody "[\"1\",\"2\",\"3\",\"\"]" res++redQueryString :: IO ()+redQueryString = runner $ do+    res <- request defaultRequest+                { pathInfo = decodePathSegments "/plain/"+                , rawQueryString = "?foo=bar"+                }+    assertStatus 301 res+    assertHeader "Location" "http://test/plain?foo=bar" res
+ test/Test/Exceptions.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+module Test.Exceptions (exceptionsTest, Widget) where++import Test.Hspec+import Test.Hspec.HUnit ()++import Yesod.Core hiding (Request)+import Network.Wai.Test++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 :: Handler ()+getRootR = error "FOOBAR" >> return ()++exceptionsTest :: [Spec]+exceptionsTest = describe "Test.Exceptions"+    [ it "500" case500+    ]++runner :: Session () -> IO ()+runner f = toWaiApp Y >>= runSession f++case500 :: IO ()+case500 = runner $ do+    res <- request defaultRequest+    assertStatus 500 res+    assertBody "FOOBAR" res
+ test/Test/InternalRequest.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE OverloadedStrings #-}+module Test.InternalRequest (internalRequestTest) where++import Data.List (nub)+import System.Random (StdGen, mkStdGen)++import Network.Wai as W+import Network.Wai.Test+import Yesod.Internal.TestApi (randomString, parseWaiRequest')+import Yesod.Request (Request (..))+import Test.Hspec++randomStringSpecs :: [Spec]+randomStringSpecs = describe "Yesod.Internal.Request.randomString"+  [ it "looks reasonably random" looksRandom+  , it "does not repeat itself" $ noRepeat 10 100+  ]++-- NOTE: this testcase may break on other systems/architectures if+-- mkStdGen is not identical everywhere (is it?).+looksRandom = randomString 20 (mkStdGen 0) == "VH9SkhtptqPs6GqtofVg"++noRepeat len n = length (nub $ map (randomString len . mkStdGen) [1..n]) == n+++-- For convenience instead of "(undefined :: StdGen)".+g :: StdGen+g = undefined+++nonceSpecs :: [Spec]+nonceSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqNonce)"+  [ it "is Nothing if sessions are disabled" noDisabledNonce+  , it "ignores pre-existing nonce if sessions are disabled" ignoreDisabledNonce+  , it "uses preexisting nonce in session" useOldNonce+  , it "generates a new nonce for sessions without nonce" generateNonce+  ]++noDisabledNonce = reqNonce r == Nothing where+  r = parseWaiRequest' defaultRequest [] Nothing g++ignoreDisabledNonce = reqNonce r == Nothing where+  r = parseWaiRequest' defaultRequest [("_NONCE", "old")] Nothing g++useOldNonce = reqNonce r == Just "old" where+  r = parseWaiRequest' defaultRequest [("_NONCE", "old")] (Just undefined) g++generateNonce = reqNonce r /= Nothing where+  r = parseWaiRequest' defaultRequest [("_NONCE", "old")] (Just undefined) g+++langSpecs :: [Spec]+langSpecs = describe "Yesod.Internal.Request.parseWaiRequest (reqLangs)"+  [ it "respects Accept-Language" respectAcceptLangs+  , it "respects sessions" respectSessionLang+  , it "respects cookies" respectCookieLang+  , it "respects queries" respectQueryLang+  , it "prioritizes correctly" prioritizeLangs+  ]++respectAcceptLangs = reqLangs r == ["accept1", "accept2"] where+  r = parseWaiRequest' defaultRequest+        { requestHeaders = [("Accept-Language", "accept1, accept2")] } [] Nothing g++respectSessionLang = reqLangs r == ["session"] where+  r = parseWaiRequest' defaultRequest [("_LANG", "session")] Nothing g++respectCookieLang = reqLangs r == ["cookie"] where+  r = parseWaiRequest' defaultRequest+        { requestHeaders = [("Cookie", "_LANG=cookie")]+        } [] Nothing g++respectQueryLang = reqLangs r == ["query"] where+  r = parseWaiRequest' defaultRequest { queryString = [("_LANG", Just "query")] } [] Nothing g++prioritizeLangs = reqLangs r == ["query", "cookie", "session", "accept1", "accept2"] where+  r = parseWaiRequest' defaultRequest+        { requestHeaders = [ ("Accept-Language", "accept1, accept2")+                           , ("Cookie", "_LANG=cookie")+                           ]+        , queryString = [("_LANG", Just "query")]+        } [("_LANG", "session")] Nothing g+++internalRequestTest :: [Spec]+internalRequestTest = descriptions [ randomStringSpecs+                                   , nonceSpecs+                                   , langSpecs+                                   ]++main = hspec internalRequestTest
+ test/Test/Links.hs view
@@ -0,0 +1,35 @@+{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+module Test.Links (linksTest, Widget) where++import Test.Hspec+import Test.Hspec.HUnit ()++import Yesod.Core hiding (Request)+import Text.Hamlet+import Network.Wai.Test++data Y = Y+mkYesod "Y" [parseRoutes|+/ RootR GET+|]++instance Yesod Y where+    approot _ = ""++getRootR :: Handler RepHtml+getRootR = defaultLayout $ addHamlet [hamlet|<a href=@{RootR}>|]++linksTest :: [Spec]+linksTest = describe "Test.Links"+    [ it "linkToHome" case_linkToHome+    ]++runner :: Session () -> IO ()+runner f = toWaiApp Y >>= runSession f++case_linkToHome :: IO ()+case_linkToHome = runner $ do+    res <- request defaultRequest+    assertBody "<!DOCTYPE html>\n<html><head><title></title></head><body><a href=\"/\"></a></body></html>" res
+ test/Test/Media.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+module Test.Media (mediaTest, Widget) where++import Test.Hspec+import Test.Hspec.HUnit ()+import Yesod.Core hiding (Request)+import Network.Wai+import Network.Wai.Test+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 :: Handler RepHtml+getRootR = defaultLayout $ do+    addCassius [lucius|foo1{bar:baz}|]+    addCassiusMedia "screen" [lucius|foo2{bar:baz}|]+    addCassius [lucius|foo3{bar:baz}|]++getStaticR :: Handler RepHtml+getStaticR = getRootR++runner :: Session () -> IO ()+runner f = toWaiApp Y >>= runSession f++caseMedia :: IO ()+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 :: IO ()+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 :: [Spec]+mediaTest = describe "Test.Media"+    [ it "media" caseMedia+    , it "media link" caseMediaLink+    ]
+ test/Test/NoOverloadedStrings.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleInstances #-}+module Test.NoOverloadedStrings (noOverloadedTest, Widget) where++import Test.Hspec+import Test.Hspec.HUnit ()++import Yesod.Core hiding (Request)+import Network.Wai.Test+import Data.Monoid (mempty)+import Data.String (fromString)++data Subsite = Subsite++getSubsite :: a -> 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 :: Handler ()+getRootR = return ()++getFooR :: Handler ()+getFooR = return ()++runner :: Session () -> IO ()+runner f = toWaiApp Y >>= runSession f++case_sanity :: IO ()+case_sanity = runner $ do+    res <- request defaultRequest+    assertBody mempty res++noOverloadedTest :: [Spec]+noOverloadedTest = describe "Test.NoOverloadedStrings"+    [ it "sanity" case_sanity+    ]
+ test/Test/Widget.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell, MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+module Test.Widget (widgetTest) where++import Test.Hspec+import Test.Hspec.HUnit ()++import Yesod.Core hiding (Request)+import Text.Julius+import Text.Lucius+import Text.Hamlet++import Network.Wai+import Network.Wai.Test++data Y = Y++mkMessage "Y" "test" "en"++type Strings = [String]++mkYesod "Y" [parseRoutes|+/ RootR GET+/foo/*Strings MultiR GET+/whamlet WhamletR GET+/towidget TowidgetR GET+|]++instance Yesod Y where+    approot _ = "http://test"++getRootR :: Handler RepHtml+getRootR = defaultLayout $ toWidgetBody [julius|<not escaped>|]++getMultiR :: [String] -> Handler ()+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++getTowidgetR :: Handler RepHtml+getTowidgetR = defaultLayout $ do+    toWidget [julius|foo|] :: Widget+    toWidgetHead [julius|foo|]+    toWidgetBody [julius|foo|]++    toWidget [lucius|foo{bar:baz}|]+    toWidgetHead [lucius|foo{bar:baz}|]++    toWidget [hamlet|<foo>|] :: Widget+    toWidgetHead [hamlet|<foo>|]+    toWidgetBody [hamlet|<foo>|]++getWhamletR :: Handler RepHtml+getWhamletR = defaultLayout [whamlet|+<h1>Test+<h2>@{WhamletR}+<h3>_{Goodbye}+<h3>_{MsgAnother}+^{embed}+|]+  where+    embed = [whamlet|<h4>Embed|]++widgetTest :: [Spec]+widgetTest = describe "Test.Widget"+    [ it "addJuliusBody" case_addJuliusBody+    , it "whamlet" case_whamlet+    ]++runner :: Session () -> IO ()+runner f = toWaiApp Y >>= runSession f++case_addJuliusBody :: IO ()+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 :: IO ()+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
+ test/en.msg view
@@ -0,0 +1,1 @@+Another: String
+ test/main.hs view
@@ -0,0 +1,20 @@+import Test.Hspec++import Test.CleanPath+import Test.Exceptions+import Test.Widget+import Test.Media+import Test.Links+import Test.NoOverloadedStrings+import Test.InternalRequest++main :: IO ()+main = hspecX $ descriptions $+    [ cleanPathTest+    , exceptionsTest+    , widgetTest+    , mediaTest+    , linksTest+    , noOverloadedTest+    , internalRequestTest+    ]
yesod-core.cabal view
@@ -1,5 +1,5 @@ name:            yesod-core-version:         0.9.2+version:         0.9.3 license:         BSD3 license-file:    LICENSE author:          Michael Snoyman <michael@snoyman.com>@@ -14,6 +14,16 @@ cabal-version:   >= 1.8 build-type:      Simple homepage:        http://www.yesodweb.com/+extra-source-files:+  test/en.msg+  test/Test/NoOverloadedStrings.hs+  test/Test/Media.hs+  test/Test/Exceptions.hs+  test/Test/Widget.hs+  test/Test/CleanPath.hs+  test/Test/Links.hs+  test/Test/InternalRequest.hs+  test/main.hs  flag test   description: Build the executable to run unit tests@@ -54,8 +64,12 @@                    , case-insensitive          >= 0.2      && < 0.4                    , parsec                    >= 2        && < 3.2                    , directory                 >= 1        && < 1.2+                   , data-object               >= 0.3      && < 0.4+                   , data-object-yaml          >= 0.3      && < 0.4                    -- for logger. Probably logger should be a separate package                    , strict-concurrency        >= 0.2.4    && < 0.2.5+                   , vector                    >= 0.9      && < 0.10+                   , aeson-native              >= 0.3.3.1  && < 0.4      exposed-modules: Yesod.Content                      Yesod.Core@@ -65,6 +79,8 @@                      Yesod.Request                      Yesod.Widget                      Yesod.Message+                     Yesod.Config+                     Yesod.Internal.TestApi     other-modules:   Yesod.Internal                      Yesod.Internal.Core                      Yesod.Internal.Session@@ -91,7 +107,7 @@         build-depends:   base                      >= 4        && < 4.3         main-is:         main.hs     cpp-options:   -DTEST-    build-depends: hspec >= 0.6.1 && < 0.7+    build-depends: hspec >= 0.8 && < 0.9                   ,wai-test                   ,wai                   ,yesod-core@@ -101,6 +117,7 @@                   ,shakespeare-js                   ,text                   ,http-types+                  , random                   ,HUnit                   ,QuickCheck >= 2 && < 3     ghc-options:     -Wall